浅谈字符串哈希 入门
基本介紹
字符串哈希的主要思路是這樣的:首先選定一個進制 \(P\),對于一個長度為 \(N\) 的字符串 \(S\) 的所有 \(i(1\leq i \leq n)\) 的 \(S_1,S_2,...,S_i\) 子串表示成 \(P\) 進制的值預處理記錄下來。這樣判斷 \(S_i,S_{i+1},...,S_{i+m-1}\) 和 \(T_1,T_2,...,T_m\) 是否相等的時候就可以直接以這兩段的哈希值作為判斷依據。顯然如果兩個字符串相等那么對于同一個模數這兩段的哈希值是相等的。
具體如何計算 \(S_l,S_{l+1},...,S{r}\) 的哈希值呢?根據進制的定義,哈希值等于 \(H_r-H_{l-1}\times P^{r-l+1}\),其中 \(H_i\) 表示 \(1\) 到 \(i\) 的哈希值。具體類比帶入一個十進制整數可以幫助更好的理解。
實現方法
自然溢出
顯然如果字符串稍微長一點那么普通的 int 啥的就存不下了。我們需要進行取模。
一個討巧的做法是直接不管他,不取模。因為在 C++ 中如果一個數大于該類型的最大值那么就會溢出從最小值繼續開始算,相當于對該類型的值域取模。
但由于模數的固定性,這個很容易被卡(構造兩個不同的字符串但是在特定模數下哈希值相同,進制不影響大多數構造方法)。
單模哈希
所以我們就選擇一個特定的模數對哈希值進行取模即可。注意減法時值不要變成負數(可以通過加上模數再減再取模解決)。
多模哈希
根據抽屜原理我們可以證明,對于一個 \(10^9\) 級別的模數,一個 \(10^5\) 長度的字符串會有兩個子串哈希值相同。
所以我們可以通過增加模數的方式提高正確率。如兩個 \(10^{9}\) 級別的模數同時哈希,要兩種哈希方式值都一樣才判斷子串相同,那就等同于模數是 \(10^{18}\) 級別的。這時候純隨機要出錯就得串長 \(10^9\) 了。所以一般雙模哈希可以保證正確。
關于模數和進制
模數和進制直接決定著字符串哈希的正確率。給出幾點建議:
- 
盡量選擇質數。尤其不要使用類似 \(2^n\) 的進制數,極其容易沖突。 
- 
重要比賽不要寫自然溢出,容易被卡。 
- 
不要使用人盡皆知的模數,如 \(998244353,10^9+7\)。可能有良心數據人照著卡。進制數無所謂。 
大質數表以供參考。
多維字符串
類似高維前綴和的寫法,每一維單獨相加,模數不同。也可以類似二維前綴和的寫法。
例題
例2.1:字符串匹配
題意
有兩個字符串 \(S\) 和 \(T\),求字符串 \(T\) 在 \(S\) 中出現了幾次。
題解
把 \(S\) 的哈希值算出來,然后依次比較 \(S_i,S_{i+1},...,S_{i+m-1}(1\leq i,i+m-1\leq n)\) 的哈希值是否等于 \(T\) 的哈希值。
單模可以過。因為只有 \(n\) 級別的子串。
代碼
#include<bits/stdc++.h>
#define p 131
#define mod 1000001011
#define int long long
using namespace std;
string s,t;
int n,m,ht,ans,h[1000005],base[1000005];
signed main(){
	cin>>s>>t;
	n=s.length(),m=t.length();
	s='#'+s,t='#'+t;
	base[0]=1;
	for(int i=1;i<=n;i++) h[i]=(h[i-1]*p%mod+s[i])%mod,base[i]=base[i-1]*p%mod;
	for(int i=1;i<=m;i++) ht=(ht*p+t[i])%mod;
	for(int i=1;i+m-1<=n;i++) if((h[i+m-1]+mod-h[i-1]*base[m]%mod)%mod==ht) ans++;
	cout<<ans;
	return 0;
}
例2.2:兩個后綴的最長公共前綴
題意
給定一個長度為 \(N\) 的字符串 \(S\),下標從 \(1\) 開始。有 \(Q\) 個詢問,每次詢問有兩個整數 \(x\) 和 \(y\),求 \(S[x...n]\) 和 \(S[y...n]\) 的最長公共前綴,即從 \(x\) 和 \(y\) 開始有多少個字母是相同的。
題解
算出 \(S\) 的哈希值,然后二分最長的長度,按題意比較就可以了。復雜度 \(O(Q\log N+N)\)。
代碼
#include<bits/stdc++.h>
#pragma GCC optimize(2,3,"inline","-Ofast")
#define p 131
#define mod 1000001011
#define int unsigned long long
using namespace std;
string s;
int n,q,h[100005],base[100005];
int get(int i,int m){
	return (h[i+m-1]+mod-h[i-1]*base[m]%mod)%mod;
}
signed main(){
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	cin>>n>>q>>s;
	s='#'+s;
	base[0]=1;
	for(int i=1;i<=n;i++) h[i]=(h[i-1]*p%mod+s[i])%mod,base[i]=base[i-1]*p%mod;
	while(q--){
		int x,y;
		cin>>x>>y;
		if(x>y) swap(x,y);
		int l=0,r=n-y+1,ans=0;
		while(l<=r){
			int mid=(l+r)>>1;
			if(get(x,mid)==get(y,mid)) l=mid+1,ans=mid;
			else r=mid-1;
		}
		cout<<ans<<'\n';
	}
}
例2.3 最長回文
題意
求一個字符串的最長回文子串。
題解
和上一題相似的,預處理字符串正著和反著的哈希值,對于原字符串的每一位二分這一位作為回文串的最中間可以達到的最大回文長度。也就是判斷從這位往左和往右相同長度的子串反著、正著的哈希值是否相等。注意分討回文串長度為偶數的情況。復雜度 \(O(N\log N)\)。
代碼
#include<bits/stdc++.h>
#pragma GCC optimize(2,3,"inline","-Ofast")
#define p 131
#define mod 1000001011
#define int long long
using namespace std;
string s;
int n,h1[1000005],h2[1000005],base[1000005];
int get1(int i,int m){
	return (h2[i]+mod-h2[i+m]*base[m]%mod)%mod;
}
int get2(int i,int m){
	return (h1[i+m-1]+mod-h1[i-1]*base[m]%mod)%mod;
}
signed main(){
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	cin>>n>>s;
	s='#'+s;
	base[0]=1;
	for(int i=1;i<=n;i++) h1[i]=(h1[i-1]*p%mod+s[i])%mod,base[i]=base[i-1]*p%mod;
	for(int i=n;i>=1;i--) h2[i]=(h2[i+1]*p%mod+s[i])%mod;
	int ans=0;
	for(int i=1;i<=n;i++){
		int l=0,r=min(i,n-i+1);
		while(l<=r){
			int mid=(l+r)>>1;
			if(get1(i-mid+1,mid)==get2(i,mid)) l=mid+1,ans=max(ans,mid*2-1);
			else r=mid-1;
		}
		if(i<n){
			l=0,r=min(i,n-i);
			while(l<=r){
				int mid=(l+r)>>1;
				if(get1(i-mid+1,mid)==get2(i+1,mid)) l=mid+1,ans=max(ans,mid*2);
				else r=mid-1;
			}
		}
	}
	cout<<ans<<'\n';
}
例2.4:二維匹配
題意
給定一個 \(M\) 行 \(N\) 列的 01 矩陣,以及 \(Q\) 個 \(A\) 行 \(B\) 列的01矩陣,你需要求出這 \(Q\) 個矩陣哪些在原矩陣中出現過。
題解
算出所有端點為左上角的子矩陣的哈希值存進 map 里,暴力檢查新的矩陣哈希值是否存在即可。復雜度 \(O(NM\log NM+QAB\log NM)\)。
代碼
#include<bits/stdc++.h>
#define p1 131
#define p2 13331
#define int long long
using namespace std;
int n,m,q,a,b,h1[1005][1005],h2[1005][1005],base1[1005],base2[1005];
string s[1005],t[1005];
map<int,bool> mp;
signed main(){
	cin>>n>>m>>a>>b;
	base1[0]=base2[0]=1;
	for(int i=1;i<=n;i++) cin>>s[i],s[i]='#'+s[i];
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			h1[i][j]=h1[i][j-1]*p1+s[i][j];
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			h1[i][j]+=h1[i-1][j]*p2;
		}
	}
	for(int i=1;i<=n;i++){
		base1[i]=base1[i-1]*p1;
		base2[i]=base2[i-1]*p2;
	}
	for(int i=a;i<=n;i++){
		for(int j=b;j<=m;j++){
			int v1=h1[i][j];
			int v2=h1[i-a][j]*base2[a];
			int v3=h1[i][j-b]*base1[b];
			int v4=h1[i-a][j-b]*base2[a]*base1[b];
			mp[v1-v2-v3+v4]=1;
		}
	}
	cin>>q;
	while(q--){
		for(int i=1;i<=a;i++) cin>>t[i],t[i]='#'+t[i];
		for(int i=1;i<=a;i++){
			for(int j=1;j<=b;j++) h2[i][j]=h2[i][j-1]*p1+t[i][j];
		}
		for(int i=1;i<=a;i++){
			for(int j=1;j<=b;j++) h2[i][j]+=h2[i-1][j]*p2;
		}
		cout<<mp[h2[a][b]]<<'\n';
	}
}
關于哈希
哈希/小trick/雜題總結
總結
以上是生活随笔為你收集整理的浅谈字符串哈希 入门的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: Reflect API:每个 JavaS
- 下一篇: 千疮百孔下一句是什么啊?
