首先,Hash Killer I、II、III是BZOJ上面三道很經典的字符串哈希破解題。當時關於II,本人還琢磨了好久,但一直不明白為啥別人AC的代碼都才0.3kb左右,直到CYG神犇說可以直接隨機水過去,遂恍然大悟。。。
於是,本人今天也做了下實驗——假設現在有一個字符串題:輸入N,接下來N行輸入N個長度一樣的由大寫字母組成的字符串,求一共有多少種不同的字符串。此題有些類似於Hash Killer上面的原題。首先分析此題本身,兩種常規辦法:1.建立一棵字典樹,然后可以相當方便快捷的判重,對於字符串長度均為M的數據,復雜度O(NM) 2.字符串哈希,選取一對質數pa和pb,哈希值為Sigma((ord(s1[i])-64)*pa^i) mod pb,然后通過哈希值排個序完事
接下來開始——字典樹肯定能保證正確這個毫無疑問,但是更加毫無疑問的是哈希是相當容易被卡掉的(HansBug:尤其像Hash Killer II這樣素數的神選取我也是醉了),但更加更加毫無疑問的是雙取模哈希似乎還比較小強,於是我就此展開實驗
1.寫出一個數據生成器,負責隨機生成N個長度為M的大寫字母字符串,然后立刻用Trie樹求出答案作為標准輸出數據
1 type 2 point=^node; 3 node=record 4 ex:longint; 5 next:array['A'..'Z'] of point; 6 end; 7 var 8 i,j,k,l,m,n,ans:longint; 9 head:point; 10 s1,s2:ansistring; 11 function getpoint:point;inline; 12 var p:point;c1:char; 13 begin 14 new(p);p^.ex:=0; 15 for c1:='A' to 'Z' do p^.next[c1]:=nil; 16 exit(p); 17 end; 18 function check(s1:ansistring):longint;inline; 19 var i:longint;p:point; 20 begin 21 p:=head; 22 for i:=1 to length(s1) do 23 begin 24 if p^.next[s1[i]]=nil then 25 p^.next[s1[i]]:=getpoint; 26 p:=p^.next[s1[i]]; 27 end; 28 if p^.ex=0 then 29 begin 30 inc(ans); 31 p^.ex:=ans; 32 end; 33 exit(p^.ex); 34 end; 35 begin 36 readln(n,m); 37 head:=getpoint;ans:=0; 38 RANDOMIZE; 39 assign(output,'hs.in'); 40 rewrite(output); 41 writeln(n); 42 for i:=1 to n do 43 begin 44 s1:=''; 45 for j:=1 to m do s1:=s1+chr(random(26)+65); 46 writeln(s1); 47 check(s1); 48 end; 49 close(output); 50 assign(output,'hss.out'); 51 rewrite(output); 52 writeln(ans); 53 close(output); 54 end.
2.接下來,開始寫哈希,也不難,而且代碼貌似還略短(這里面兩個素數采用互換使用的模式,本程序是雙取模的哈希,如果需要改成單值哈希的話直接把第50行去掉即可)
1 const pa=314159;pb=951413; 2 var 3 i,j,k,l,m,n:longint; 4 ap,bp:array[0..100000] of int64; 5 a:array[0..200000,1..2] of int64; 6 a1,a2,a3,a4:int64; 7 s1,s2:ansistring; 8 function fc(a1,a2,a3,a4:int64):longint;inline; 9 begin 10 if a1<>a3 then 11 if a1>a3 then exit(1) else exit(-1) 12 else 13 if a2<>a4 then 14 if a2>a4 then exit(1) else exit(-1) 15 else exit(0); 16 end; 17 procedure sort(l,r:longint); 18 var i,j:longint;x,y,z:int64; 19 begin 20 i:=l;j:=r;x:=a[(l+r) div 2,1];y:=a[(l+r) div 2,2]; 21 repeat 22 while fc(a[i,1],a[i,2],x,y)=-1 do inc(i); 23 while fc(x,y,a[J,1],a[J,2])=-1 do dec(j); 24 if i<=j then 25 begin 26 z:=a[i,1];a[i,1]:=a[j,1];a[j,1]:=z; 27 z:=a[i,2];a[i,2]:=a[j,2];a[j,2]:=z; 28 inc(i);dec(j); 29 end; 30 until i>j; 31 if i<r then sort(i,r); 32 if l<j then sort(l,j); 33 end; 34 35 begin 36 ap[0]:=1;bp[0]:=1; 37 for i:=1 to 100000 do 38 begin 39 ap[i]:=(ap[i-1]*pa) mod pb; 40 bp[i]:=(bp[i-1]*pb) mod pa; 41 end; 42 readln(n); 43 for i:=1 to n do 44 begin 45 readln(s1); 46 a[i,1]:=0;a[i,2]:=0; 47 for j:=1 to length(s1) do 48 begin 49 a[i,1]:=(a[i,1]+ap[j]*(ord(s1[j])-64)) mod pb; 50 a[i,2]:=(a[i,2]+bp[j]*(ord(s1[j])-64)) mod pa; //刪除此行即可改為單值哈希 51 end; 52 end; 53 sort(1,n);m:=0; 54 a[0,1]:=-maxlongint; 55 for i:=1 to n do if fc(a[i-1,1],a[i-1,2],a[i,1],a[i,2])<>0 then inc(m); 56 writeln(m); 57 readln; 58 end. 59
於是開始愉快的用bat來對拍:
1.當N=100000 M=3時,很令人吃驚——單雙值的哈希都問題不大(隨機跑了403組數據均全部通過)
2.當N=100000 M=100是,果不其然——單值的哈希成功而華麗的實現了0%的命中率,而雙值的哈希依然100%(HansBug:實測6001組數據,跑了快兩小時有木有啊啊啊啊 wnjxyk:What Ghost? HansBug:我家電腦渣不解釋^_^)
(HansBug:呵呵噠BZOJ3098這題我居然上來就WA了,現在看來這究竟是什么樣的神人品啊)
結果已經了然,而且從bat上運行的時間來看,當N=100000 M=100時,哈希的速度比trie樹看樣子明顯快——估計是雖然trie樹可以達到O(NM),但是假如需要新建大量的點的話,那樣勢必相當費時,多半慢在這上面了,而哈希就是該怎么玩怎么玩——更重要的是——哈希,絕對不等同於非得開一個巨大的數組瞎搞,比如這個例子中直接排個序就完事啦。更重要的是雙值哈希的穩定性還是相當不錯滴!!!^_^
后記:以前我曾經一度認為hash算法一定就是必然伴隨着一個碩大的數組(HansBug:搞不好還MLE有木有TT bx2k:那是必然),其實它的靈活性遠遠超出了我的預想,今天也算是大長了見識;還有祝願BZOJ3099(Hash Killer III)永遠不要有人AC!!!否則那就基本意味着哈希算法的終結了TT
附:對拍用的bat模板,純手寫的哦,如有雷同絕無可能么么噠
1 @echo off 2 set /a s=0 3 :1 4 set /a s=s+1 5 echo Test %s% 6 rem 此處兩個數分別代表N和M,手動修改下即可 7 echo 10000 100|hs.exe 8 copy hs.in hash\hs%s%.in >nul 9 copy hsS.out hash\hs%s%.out >nul 10 echo.|time 11 type hash\hs%s%.in|hash.exe >hash\hs%s%.ou 12 echo.|time 13 fc hash\hs%s%.ou hash\hs%s%.out >hash\res%s%.txt 14 fc hash\hs%s%.ou hash\hs%s%.out 15 goto 1
