轉載自:遠航休息棧php
Hash是什麼意思呢?某度翻譯告訴咱們:算法
hash 英[hæʃ] 美[hæʃ]
n. 剁碎的食物; #號; 蔬菜肉丁;
vt. 把…弄亂; 切碎; 反覆推敲; 搞糟;sublime-text
我以爲Hash是引伸出 把...弄亂 的意思。api
今天就來談談Hash的一種——字符串hash。
據個人理解,Hash就是一個像函數同樣的東西,你放進去一個值,它給你輸出來一個值。輸出的值就是Hash值。通常Hash值會比原來的值更好儲存(更小)或比較。數組
那字符串Hash就很是好理解了。就是把字符串轉換成一個整數的函數。並且要儘可能作到使字符串對應惟一的Hash值。函數
字符串Hash的種類仍是有不少種的,不過在信息學競賽中只會用到一種名爲「BKDR Hash」的字符串Hash算法。優化
它的主要思路是選取恰當的進制,能夠把字符串中的字符當作一個大數字中的每一位數字,不過比較字符串和比較大數字的複雜度並無什麼區別(高精數的比較也是O(n)O(n)的),但只要把它對一個數取模,而後認爲取模後的結果相等原數就相等,那麼就能夠在必定的錯誤率的基礎上O(1)O(1)進行判斷了。ui
那麼咱們選擇什麼進制比較好?atom
首先不要把任意字符對應到數字0,好比假如把a對應到數字0,那麼將不能只從Hash結果上區分ab和b(雖然能夠額外判斷字符串長度,但不把任意字符對應到數字0更加省事且沒有任何反作用),通常而言,把a-z對應到數字1-26比較合適。spa
關於進制的選擇實際上很是自由,大於全部字符對應的數字的最大值,不要含有模數的質因子(那還模什麼),好比一個字符集是a到z的題目,選擇2七、23三、19260817都是能夠的。
模數的選擇(儘可能仍是要選擇質數):
絕大多數狀況下,不要選擇一個109109級別的數,由於這樣隨機數據都會有Hash衝突,根據生日悖論,隨便找上109−−−√109個串就有大機率出現至少一對Hash 值相等的串(參見BZOJ 3098 Hash Killer II)。
最穩妥的辦法是選擇兩個109109級別的質數,只有模這兩個數都相等才判斷相等,但常數略大,代碼相對難寫,目前暫時沒有辦法卡掉這種寫法(除了卡時間讓它超時)(參見BZOJ 3099 Hash Killer III)。
若是能背過或在考場上找出一個10181018級別的質數(Miller-Rabin),也相對靠譜,主要用於前一種擔憂會超時,後一種擔憂被卡。
偷懶的寫法就是直接使用unsigned long long,不手動進行取模,它溢出時會自動對264264進行取模,若是出題人比較良心,這種作法也不會被卡,但這個是徹底能夠卡的,卡的方法參見BZOJ 3097 Hash Killer I。
用luogu P3370爲例。
這是天然溢出hash(100)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
ull base=131;
ull a[10010];
char s[10010];
int n,ans=1;
ull hashs(char s[])
{
int len=strlen(s);
ull ans=0;
for (int i=0;i<len;i++)
ans=ans*base+(ull)s[i];
return ans&0x7fffffff;
}
main()
{
scanf("%d",&n);
for (int i=1;i<=n;i++)
{
scanf("%s",s);
a[i]=hashs(s);
}
sort(a+1,a+n+1);
for (int i=2;i<=n;i++)
if (a[i]!=a[i-1])
ans++;
printf("%d\n",ans);
}
|
這是單模數hash(80)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
ull base=131;
ull a[10010];
char s[10010];
int n,ans=1;
ull mod=19260817;
ull hashs(char s[])
{
int len=strlen(s);
ull ans=0;
for (int i=0;i<len;i++)
ans=(ans*base+(ull)s[i])%mod;
return ans;
}
main()
{
scanf("%d",&n);
for (int i=1;i<=n;i++)
{
scanf("%s",s);
a[i]=hashs(s);
}
sort(a+1,a+n+1);
for (int i=2;i<=n;i++)
if (a[i]!=a[i-1])
ans++;
printf("%d\n",ans);
}
|
這是雙hash(100)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
ull base=131;
struct data
{
ull x,y;
}a[10010];
char s[10010];
int n,ans=1;
ull mod1=19260817;
ull mod2=19660813;
ull hash1(char s[])
{
int len=strlen(s);
ull ans=0;
for (int i=0;i<len;i++)
ans=(ans*base+(ull)s[i])%mod1;
return ans;
}
ull hash2(char s[])
{
int len=strlen(s);
ull ans=0;
for (int i=0;i<len;i++)
ans=(ans*base+(ull)s[i])%mod2;
return ans;
}
bool comp(data a,data b)
{
return a.x<b.x;
}
main()
{
scanf("%d",&n);
for (int i=1;i<=n;i++)
{
scanf("%s",s);
a[i].x=hash1(s);
a[i].y=hash2(s);
}
sort(a+1,a+n+1,comp);
for (int i=2;i<=n;i++)
if (a[i].x!=a[i-1].x || a[i-1].y!=a[i].y)
ans++;
printf("%d\n",ans);
}
|
這是隻用一個10^18質數的hash(100)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
ull base=131;
ull a[10010];
char s[10010];
int n,ans=1;
ull mod=212370440130137957ll;
ull hashs(char s[])
{
int len=strlen(s);
ull ans=0;
for (int i=0;i<len;i++)
ans=(ans*base+(ull)s[i])%mod;
return ans;
}
main()
{
scanf("%d",&n);
for (int i=1;i<=n;i++)
{
scanf("%s",s);
a[i]=hashs(s);
}
sort(a+1,a+n+1);
for (int i=2;i<=n;i++)
if (a[i]!=a[i-1])
ans++;
printf("%d\n",ans);
}
|
Hash還有一方面,就是它能夠處理子串信息。對於一個字符串,咱們能夠預處理它1−l1−l的hash值,這樣l+1l+1的hash值就能夠O(1)O(1)的遞推出來。
對於一個字符串l−rl−r的子串,咱們能夠用f[r]−br−l+1f[l−1]f[r]−br−l+1f[l−1]來求出來,其中b表示進制。
這樣的話hash就能夠水過字符串匹配的題目
【題目描述】
法國做家喬治·佩雷克(Georges Perec,1936-1982)曾經寫過一本書,《敏感字母》(La disparition),全篇沒有一個字母‘e’。他是烏力波小組(Oulipo Group)的一員。下面是他書中的一段話:
Tout avait Pair normal, mais tout s’affirmait faux. Tout avait Fair normal, d’abord, puis surgissait l’inhumain, l’affolant. Il aurait voulu savoir où s’articulait l’association qui l’unissait au roman : stir son tapis, assaillant à tout instant son imagination, l’intuition d’un tabou, la vision d’un mal obscur, d’un quoi vacant, d’un non-dit : la vision, l’avision d’un oubli commandant tout, où s’abolissait la raison : tout avait l’air normal mais…
佩雷克極可能在下面的比賽中獲得高分(固然,也有多是低分)。在這個比賽中,人們被要求針對一個主題寫出甚至是意味深長的文章,而且讓一個給定的「單詞」出現次數儘可能少。咱們的任務是給評委會編寫一個程序來數單詞出現了幾回,用以得出參賽者最終的排名。參賽者常常會寫一長串廢話,例如500000個連續的‘T’。而且他們不用空格。
所以咱們想要儘快找到一個單詞出現的頻數,即一個給定的字符串在文章中出現了幾回。更加正式地,給出字母表{'A','B','C',...,'Z'}和兩個僅有字母表中字母組成的有限字符串:單詞W和文章T,找到W在T中出現的次數。這裏「出現」意味着W中全部的連續字符都必須對應T中的連續字符。T中出現的兩個W可能會部分重疊。
【輸入格式】
輸入包含多組數據。
輸入文件的第一行有一個整數,表明數據組數。接下來是這些數據,以以下格式給出:
第一行是單詞W,一個由{'A','B','C',...,'Z'}中字母組成的字符串,保證1<=|W|<=10000(|W|表明字符串W的長度)
第二行是文章T,一個由{'A','B','C',...,'Z'}中字母組成的字符串,保證|W|<=|T|<=1000000。
【輸出格式】
對每組數據輸出一行一個整數,即W在T中出現的次數。
【樣例輸入】
3
BAPC
BAPC
AZA
AZAZAZA
VERDI
AVERDXIVYERDIAN
【樣例輸出】
1
3
0
代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
ull base=131;
ull po[100010],hs[100010*100];
char s1[100010],s2[100010*100];
int n,ans=1,T;
ull geth(int l,int r)
{
return (ull)hs[r]-po[r-l+1]*hs[l-1];
}
main()
{
freopen("oulipo.in","r",stdin);
freopen("oulipo.out","w",stdout);
po[0]=1;
for (int i=1;i<=10010-5;i++)
po[i]=po[i-1]*base;
scanf("%d",&T);
while(T--)
{
scanf("%s%s",s1+1,s2+1);
int l1=strlen(s1+1),l2=strlen(s2+1);
ull a1=0,ans=0;
for (int i=1;i<=l1;i++)
a1=a1*base+(ull)s1[i];
for (int i=1;i<=l2;i++)
hs[i]=hs[i-1]*base+s2[i];
for (int i=1;i+l1-1<=l2;i++)
if (a1==geth(i,i+l1-1))
ans++;
printf("%d\n",ans);
}
}
|
寫到這裏忽然發現hash好像能夠暴力水過不少字符串算法。。
一、kmp
問題:給兩個字符串S1,S2,求S2是不是S1的子串,並求S2在S1中出現的次數
把S2 Hash出來,在S1裏找全部長度爲|S2||S2|的子串,Hash比較。效率O(|S1|)O(|S1|)
二、AC自動機
問題:給N個單詞串,和一個文章串,求每一個單詞串是不是文章串的子串,並求每一個單詞在文章中出現的次數。
把每個單詞hash成整數,再把文章的每個子串hash成整數,接下來只須要進行整數上的查找便可。
複雜度:O(|A|2+|S|)O(|A|2+|S|)
用AC自動機能夠作到O(|A|+|S|)O(|A|+|S|)的複雜度,|S||S|是單詞串總長,|A||A|是文章長度
三、後綴數組
問題:給兩個字符串S1,S2,求它們的最長公共子串的長度。
將S1的每個子串都hash成一個整數,將S2的每個子串都hash成一個整數
兩堆整數,相同的配對,而且找到所表示的字符串長度最大的便可。
複雜度:O(|S1|2+|S2|2)O(|S1|2+|S2|2)
用後綴數組能夠優化到O(|S|log|S|)O(|S|log|S|)
四、馬拉車
問題:給一個字符串S,求S的最長迴文子串。
先求子串長度位奇數的,再求偶數的。枚舉迴文子串的中心位置,而後二分子串的長度,直到找到一個該位置的最長迴文子串,不斷維護長度最大值便可。
複雜度:O(|S|log|S|)O(|S|log|S|)
用manacher能夠作到O(|S|)O(|S|)的複雜度
五、擴展kmp
問題:給一個字符串S,求S的每一個後綴與S的最長公共前綴
枚舉每個後綴的起始位置,二分長度,求出每一個後綴與S的最長公共前綴。
複雜度:O(|S|log|S|)O(|S|log|S|)
用extend-kmp能夠作到O(|S|)O(|S|)的複雜度
後記
hash真是一種優雅的暴力。
由於字符串特殊的性質,咱們能夠二分得處理它,通常都有單調性。