Hash Killer I 2 3 題解(hash+構造)

Hash Killer I 題目鏈接

題目大意

就是出一組數據卡掉hash ull的自然溢出

題目鏈接

首先明白兩點:

1.卡hash的關鍵在於構造兩個不同的串對應的hash值相同。

2.爆u64相當於對2^64這個數取模。

如果base是偶數

那麼a…aaa(>64個a)與ba…aa(a的數量爲前面那麼串a的數量-1),這兩個串長度相同,hash值相同,顯然串是不同的,這樣就卡掉了。

如果base是奇數

說明

1:現在只考慮a、b兩個字母。

2:a \ b表示a能整除b。

3:設數學上的函數not(S)表示把字符串S中每個位置的’a’變成’b’,把’b’變成’a’後形成的字符串。比如not(“ababaa”) = “bababb"strA . strB代表字符串連接。如"娃” . “哈哈” = “娃哈哈”

4:|str|表示字符串str的長度。

正文

設字符串序列{orzstr[i]},orzstr[1] = “a”, orzstr[i] = orzstr[i - 1] . not(orzstr[i - 1])
那麼|orzstr[i]| = |orzstr[i - 1]| * 2。顯然這是等比數列,得到:|orzstr[i]| = |orzstr[1]| . 2 ^ (i - 1) = 2 ^ (i - 1)
設hash(str)爲str的哈希值。
則:
hash(orzstr[i]) = hash(orzstr[i - 1]) * base ^ |not(orzstr[i - 1])| + hash(not(orzstr[i - 1]))
= hash(orzstr[i - 1]) * base ^ (2 ^ (i - 2)) + hash(not(orzstr[i - 1]))
hash(not(orzstr[i])) = hash(not(orzstr[i - 1])) * base ^ (2 ^ (i - 2)) + hash(orzstr[i - 1])
兩式相減:
hash(orzstr[i]) - hash(not(orzstr[i]))
= (hash(orzstr[i - 1]) * base ^ (2 ^ (i - 2)) + hash(not(orzstr[i - 1]))) - (hash(not(orzstr[i - 1])) * base ^ (2 ^ (i - 2)) + hash(orzstr[i - 1]))
= (hash(orzstr[i - 1]) - hash(not(orzstr[i - 1]))) * (base ^ (2 ^ (i - 2)) - 1)
這讓我們發現,hash(orzstr[i]) - hash(not(orzstr[i]))似乎是個神奇的東西。而我們的目的實際上是要找兩個字符串strA, strB使得
hash(strA) % 2^64 = hash(strB) % 2^64
相當與
2^64 \ hash(strA) - hash(strB)
設數列{f[i]},f[i] = hash(orzstr[i]) - hash(not(orzstr[i]))
這樣就有:
f[i] = f[i - 1] * (base ^ (2 ^ (i - 2)) - 1)
還是有點不爽啊……我們再設數列{g[i]},g[i] = base ^ (2 ^ (i - 1)) - 1
於是能寫成:
f[i] = f[i - 1] * g[i - 1]
則f[i] = f[1] * g[1] * g[2] * … * g[i - 1]
然後發現一個神奇的事情?
base是奇數,則base的任意正整數次方也一定是奇數。所以對於任意的i必有g[i]爲偶數,所以2 ^ (i - 1) \ f[i]
問題是不是結束了呢……發現沒有……這樣的話我們要使2 ^ 64 \ f[i],至少得讓i = 65……然後發現|orzstr[65]|是個天文數字。
發現我們剛纔那樣分析太坑爹了……
i > 1時有:
g[i] = base ^ (2 ^ (i - 1)) - 1 = (base ^ (2 ^ (i - 2)) - 1) * (base ^ (2 ^ (i - 2)) + 1) = g[i - 1] * 一個偶數
而g[1]顯然是偶數吧……
那麼4 \ g[2],8 \ g[3]…
也就是說2 ^ i \ g[i]
所以f[i] 實際上有:
(2 ^ 1) * (2 ^ 2) * (2 ^ 3) * … * (2 ^ (i - 1)) \ f[i]
2 ^ (i * (i - 1) / 2) \ f[i]
當i取12時,就有66個2了喲!
這就是卡base爲奇數時的方法。orzstr[12]和not(orzstr[12])即爲所求。

而讀入中base既有奇數又有偶數,直接在奇數構造的字符串後面加64個a就可以了。

代碼

#include <iostream>
#include <cstring>
using namespace std;
char s[10000];
int main(){
	cout<<(1<<12)+65<<" "<<(1<<11)<<endl;
//這就是卡base爲奇數時的方法。orzstr[12]和not(orzstr[12])即爲所求。
	int now=1;
	s[1]='a';
	for (int i=1;i<=12;i++){
		for (int j=1;j<=now;j++){
            s[now+j]=s[j]=='a'?'b':'a';//這個寫法要學習一下
		}
		now<<=1;
	}
	for (int i=1;i<=now;i++)
		printf("%c",s[i]);
	for (int i=1;i<=65;i++)
		printf("a");
	printf("\n");
	return 0;
}

Hash Killer 2 題目鏈接

題目大意

構造數據卡掉1e9+7的hash

題目思路

生日攻擊:如果你在n個數中隨機選數,那麼最多選sqrt(n)次就能選到相同的數

同樣的,這題的Hash值在0到1000000007.那就要選差不多10^5次

唯一注意的是l要取大,使得方案數超過Mod否則就不可能有2個數有相同的Hash值

公式爲
具體的神仙博客

代碼

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cstdlib>
using namespace std;
int main(){
    printf("100000 20\n");
    for(int i = 1;i <= 100000;i++){
        printf("%c",(rand() % 26 + 'a'));
    }
    printf("\n");
    return 0;
}

Hash Killer 3 題目鏈接

題目思路

雙hash無解

hash的總結

那麼我們選擇什麼進制比較好?

首先不要把任意字符對應到數字0,比如假如把a對應到數字0,那麼將不能只從Hash結果上區分ab和b(雖然可以額外判斷字符串長度,但不把任意字符對應到數字0更加省事且沒有任何副作用),一般而言,把a-z對應到數字1-26比較合適。

關於進制的選擇實際上非常自由,大於所有字符對應的數字的最大值,不要含有模數的質因子(那還模什麼),比如一個字符集是a到z的題目,選擇27、233、19260817 都是可以的。

模數的選擇(儘量還是要選擇質數):

絕大多數情況下,不要選擇一個1e9的質數級別的數,因爲這樣隨機數據都會有Hash衝突,根據生日悖論,隨便找上sqrt(1e9)
個串就有大概率出現至少一對Hash 值相等的串(參見BZOJ 3098 Hash Killer II)。

最穩妥的辦法是選擇兩個1e9 級別的質數,只有模這兩個數都相等才判斷相等,但常數略大,代碼相對難寫,目前暫時沒有辦法卡掉這種寫法(除了卡時間讓它超時)(參見BZOJ 3099 Hash Killer III)。

如果能背過或在考場上找出一個10^18級別的質數(Miller-Rabin),也相對靠譜,主要用於前一種擔心會超時,後一種擔心被卡。

偷懶的寫法就是直接使用unsigned long long,不手動進行取模,它溢出時會自動對2^64進行取模,如果出題人比較良心,這種做法也不會被卡,但這個是完全可以卡的,卡的方法參見BZOJ 3097 Hash Killer I。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章