請勿將字符串Hash和哈希表搞混。雖然二者都是哈希算法,但實現原理和應用上有很大區別。php
如下默認字符串下標從1開始,用\(s[l,r]\)表示字符串\(s\)的第\(l\)到第\(r\)個字符組成的子串。node
字符串Hash經常使用於各類字符串題目的部分分中。字符串Hash能夠在\(O(1)\)時間內完成判斷兩個字符串的子串是否相同。一般能夠用這個性質來優化暴力以達到騙分的目的。ios
字符串Hash其實本質上是一個公式:
\[Hash(s)=(\sum_{i=1}^{len}{s[i]\cdot b^{len-i}})mod\ m\]
其中\(b,m\)是常量。算法
是否是感受很像一個生成函數數據結構
那爲何要採用這樣一個長得像生成函數的式子呢?函數
參照哈希表,咱們知道若是兩個字符串的Hash值相同,那麼這兩個串大機率是相同的。優化
但事實上咱們經常須要截取一個字符串的子串。能夠發現,對於\(s[l,r]\)這個子串的Hash值
\[Hash(s[l,r])=(\sum_{i=l}^{r}{s[i]\cdot b^{r-i}})\mod m\]
考慮原串\(s\)的前綴和
\[Hash(s[1,r])=(\sum_{i=1}^{r}{s[i]\cdot b^{r-i}})\mod m\]
\[Hash(s[1,l-1])=(\sum_{i=1}^{l-1}{s[i]\cdot b^{l-i-1}})\mod m\]
因而能夠推出:\(Hash(s[l,r])=Hash(s[1,r])-Hash(s[1,l-1])\cdot b^{r-l+1}\)spa
固然這都是在模\(m\)意義下。code
因而對於原串記錄Hash前綴和,就能夠\(O(1)\)截取子串Hash值blog
字符串Hash是一種十分暴力的算法。但因爲它能\(O(1)\)判斷字符串是否相同,因此能夠騙取很多分甚至過掉一些字符串題。
接下來先介紹字符串Hash與其餘字符串算法的對比。
這個不用說了,枚舉起始點掃一遍\(O(n)\)解決,時間複雜度和KMP相同。
考慮以同一個字符爲中心的迴文串的子串必定是迴文串,因此知足可二分性。
將字符串正着和倒着Hash一遍,若是一個串正着和倒着的Hash值相等則這個串是迴文串。枚舉每一個節點爲迴文中心,二分便可。
時間複雜度相比較manacher較劣,爲\(O(n\log n)\)。發現過不了模板題。
關鍵代碼
ull num[22000000],num2[22000010]; ull find_hash(int l,int r) { if(l<=r) return num[r]-num[l-1]*_base[r-l+1]; return num2[r]-num2[l+1]*_base[l-r+1]; } int l=0,r=min(i-1,len-i); int len=0; while(l<=r) { int mid=(l+r)>>1; if(find_hash(i,i+mid)==find_hash(i,i-mid)) l=mid+1,len=mid; else r=mid-1; }
LCP也具備可二分性。對於\(s_1,s_2\),和兩個前綴長度\(j,i\),\(j<i\),其中\(i\)是\(s1,s2\)的前綴,則\(j\)也是\(s1,s2\)的前綴。
因此能夠在\(O(\log n)\)時間求出兩個串的前綴。
仿照上述求lcp的方式,由於決定兩個字符串的大小的是他們lcp的後一個字符,因此用快排加二分求lcp便可作到\(O(n\log^2n)\)的時間複雜度。比SA多了一個\(\log\)。
#include<cstdio> #include<cstdlib> #include<iostream> #include<algorithm> #include<cstring> #define base 233 #define ull unsigned long long using namespace std; ull bases[1000010],hashs[1000010]; char str[1000010]; int n; inline ull get(int l,int r){return hashs[r]-hashs[l-1]*bases[r-l+1];} bool cmp(int l1,int l2) { int l=-1,r=min(n-l1,n-l2); while(l<r) { int mid=(l+r+1)>>1; if(get(l1,l1+mid)==get(l2,l2+mid)) l=mid; else r=mid-1; } if(l>min(n-l1,n-l2)) return l1>l2; else return str[l1+l+1]<str[l2+l+1]; } int a[1000010]; int main() { scanf("%s",str+1); n=strlen(str+1); bases[0]=1; for(int i=1;i<=n;i++) { bases[i]=bases[i-1]*base; hashs[i]=hashs[i-1]*base+str[i]; a[i]=i; } stable_sort(a+1,a+n+1,cmp); for(int i=1;i<=n;i++) printf("%d ",a[i]); return 0; }
難道字符串Hash只能去水其餘算法的題嗎?不!暴力有時不僅是騙分的!
題意:求一個字符串的子串的最短循環節。
能夠發現對於串\(s[l,r]\),若是\(x\)是子串的一個循環節,必有\((r-l+1)\mod x=0\) 且 \(s[l,r-x]=s[l+x,r]\)
若是存在長度\(y\)是\(s\)的循環節(\(y\)是\(x\)的因數)且\(x\)是串長的約數,則\(x\)必然是\(s\)的循環節。
考慮篩出每一個數的最大質因數,而後\(O(\log n)\)分解質因數,而後從大到小試除,看餘下的長度是不是循環節,若是是則更新答案。
#include<iostream> #include<cstdio> #include<cstdlib> #define N 500010 #define base 233 #define ull unsigned long long using namespace std; char str[N]; int len; ull hashs[N],bases[N]; void make_hash(void) { bases[0]=1; for(int i=1;i<=len;i++) { hashs[i]=hashs[i-1]*base+str[i]-'a'+1; bases[i]=bases[i-1]*base; } } ull get_hash(int l,int r){return hashs[r]-hashs[l-1]*bases[r-l+1];} int prime[N],nxt[N],cnt; int num[N],tot; int main() { scanf("%d",&len); scanf("%s",str+1); make_hash(); for(int i=2;i<=len;i++) { if(!nxt[i]) nxt[i]=prime[++cnt]=i; for(int j=1;j<=cnt && i*prime[j]<=len;j++) { nxt[i*prime[j]]=prime[j]; if(i%prime[j]==0) break; } } int m; scanf("%d",&m); for(int i=1;i<=m;i++) { int l,r; scanf("%d%d",&l,&r); int lens=r-l+1; int ans=0; tot=0; while(lens>1) { num[++tot]=nxt[lens]; lens/=nxt[lens]; } lens=r-l+1; for(int j=1;j<=tot;j++) { int len1=lens/num[j]; if(get_hash(l,r-len1)==get_hash(l+len1,r)) lens=len1; } printf("%d\n",lens); } return 0; }
如今的大多數字符串算法(好比KMP,AC自動機,SAM,SA,PAM)大多都是靜態的查詢或者只容許在最後插入。但若是要求在字符串中間插入或修改,這些算法就無能爲力了。
而字符串Hash的式子實際上是能夠合併的,只要知道左區間的Hash值,右區間的Hash值,和右區間的大小,就能夠知道總區間的Hash值。這就使得字符串Hash能夠套上不少數據結構來維護。
題意:求兩個後綴的lcp,動態插入字符和改字符。
用平衡樹維護區間的Hash值,仿照上述求lcp的方法,時間複雜度\(O(n\log^2n)\)。
因爲長度過長,只放主程序和關鍵代碼。
inline void update(int u) { siz[u]=siz[ch[u][0]]+siz[ch[u][1]]+1; sum[u]=sum[ch[u][0]]*bases[siz[ch[u][1]]+1]+val[u]*bases[siz[ch[u][1]]]+sum[ch[u][1]]; } int main() { srand(19260817); scanf("%s",str+1); int m; scanf("%d",&m); n=strlen(str+1); bases[0]=1; for(int i=1;i<=100000;i++) bases[i]=bases[i-1]*base; for(int i=1;i<=n;i++) root=t.merge(root,t.new_node(str[i]-'a'+1)); for(int i=1;i<=m;i++) { int x,y; scanf("%s%d",opt,&x); if(opt[0]=='Q') { scanf("%d",&y); printf("%d\n",lcp(x,y)); } else if(opt[0]=='R') { scanf("%s",opt); t.erase(root,x); t.insert(root,x-1,opt[0]-'a'+1); } else if(opt[0]=='I') { scanf("%s",opt); t.insert(root,x,opt[0]-'a'+1); n++; } } return 0; }
題意:維護一個字符串的子串的集合,一開始字符串和集合均爲空。
要求完成:在集合中插入一個子串,在字符串末尾加一個字符,求集合中與當前詢問的子串lcp最大值。
好比字符串爲\(abbabba\)
集合中的子串爲\(s[1,4],s[3,6],s[5,7]\)。
此時查詢與子串\(s[2,5]\),答案爲2(\(s[2,5]\)和\(s[5,7]\)的lcp爲2)。
\(m\leq 10^5\),強制在線(爲了防止SA過而特地加的)。
(假如存在SAM的作法能夠在下方評論)
首先,考慮一些暴力的作法:
顯然上述的方法都不可行。
考慮使用SA的想法,與一個串lcp最大的串必定是字典序最靠近它的串,也就是比它字典序大中最小的,和比它小中最大的。
仿照這個思路,使用上述比較兩個串字典序大小的方法,考慮使用平衡樹來維護子串集合中字典序的順序,查詢時只需查詢前驅後繼中的lcp最大值便可。
時間複雜度\(O(m\log^2m)\)
雖然字符串Hash在暴力方面有極大的優點,但從它的名字中也能夠看出它存在的缺點:Hash衝突。
題意大概就是卡單Hash的代碼,且\(mod=10^9+7\)。
根據生日悖論,對於\(n\)個不一樣的字符串,假如你的\(mod\)很小(好比\(10^9+7\)),極有可能把其中兩個判成相同。對於\(mod=N\)時錯誤的機率\(P\),有(詳見百度百科)
能夠發現這個機率遠大於\(n/N\)。
因此假如用天然溢出可能會被卡的狀況下,建議寫雙Hash。可是須要注意一點,雙Hash的常數十分之大。