【10.24】字符串哈希
定义
我们定义一个把字符串映射到整数的函数 \(f\) ,这个 \(f\) 称为是 Hash 函数。
我们希望这个函数 \(f\) 可以方便地帮我们判断两个字符串是否相等。
\(f\) 值域
在字符串哈希中,值域需要小到能够快速比较(\(10^9\)、\(10^{18}\) 都是可以快速比较的)。
同时,为了降低哈希冲突率,值域也不能太小。
性质
哈希值不一样,则字符串一定不一样。
哈希值一样,字符串不一定一样。
常用 Hash 方法
对于一个字符串 \(s\) ,有以下两种常用多项式 Hash 法:
-
\(hash(s)=s[1]*b^{n-1}+...+s[i]*b^{n-i}+...+s[n]\)
-
\(hash(s)=s[1]+...+s[i]*b^{i-1}+...+s[n]*b^{n-1}\)
相较于第二种,第一种方法更常用,且可以类比为一个 \(b\) 进制数。
取模采用 unsigned long long
自然溢出,可以防止出题人卡大模数 Hash 。至于卡自然溢出 Hash ,可以采用双模数 Hash 。
双模数 Hash
顾名思议,双模数 Hash 就是采用两个不同的 Hash 值,只有当两个 Hash 值都相同时才判断字符串相同。
在实际使用时,常常通过变更 \(b\) 的值来实现双 Hash ,注意两个不同的 \(b\) 最好奇怪一点。
快速计算字串 Hash
有的时候题目会要求重复多次计算字符串字串的 Hash 值,这时候如果每次重新计算则会显得很劣,考虑利用前缀和的思想计算字串 Hash ,这里因 Hash 方法而异。
以第一种方法 \(hash(s)=s[1]*b^{n-1}+...+s[i]*b^{n-i}+...+s[n]\) 为例:
定义 \(f_i(s)\) 为字符串 \(s\) 前 \(i\) 位的 Hash ,有:
\(f_i(s)=s[1]*b^{i-1}+...+s[i]\)
由此可得:
\(hash(s[l..r])=f_r(s)-f_{l-1}(s)*b^{r-l+1}\)
因此我们只需预处理 \(f_i(s)\) 和 \(b^k\) ,然后即可 \(O(1)\) 求得字串 Hash 。
示例代码
单模数 Hash
#define MAXN 10004
#define MAXM 1503 //字符串长度
#define ull unsigned long long
int n;
ull Hash[MAXN]; //Hash值
const int b=127;
ull B[MAXM];
void init(){
B[0]=1;
for (int i=1;i<=MAXM-2;i++){
B[i]=B[i-1]*b;
}
}
int main(){
init();
cin>>n;
for (int i=1;i<=n;i++){
string s; cin>>s;
int len=s.length();
for (int j=0;j<=len-1;j++){
Hash[i]+=(ull)(s[j]*B[len-j-1]);
}
}
return 0;
}
双模数 Hash
#define MAXN 10003
#define MAXM 1503
#define ull unsigned long long
int n;
pair <ull,ull> Hash[MAXN]; //pair存双Hash值
const int b=127,b2=175;
unsigned long long B[MAXM],B2[MAXM];
void init(){
B[0]=B2[0]=1;
for (int i=1;i<=MAXM-2;i++){
B[i]=B[i-1]*b;
B2[i]=B2[i-1]*b2;
}
return;
}
int main(){
init();
cin>>n;
for (int i=1;i<=n;i++){
string a;
cin>>a;
int len=a.length();
for (int j=0;j<=len-1;j++){
Hash[i].first+=(ull)(a[j]*B[len-j-1]);
Hash[i].second+=(ull)(a[j]*B2[len-j-1]);
}
}
return 0;
}
快速计算字串 Hash
#define MAXN 10000003
#define ull unsigned long long
int n,m;
char a[MAXN];
const int base=157;
ull B[MAXN];
ull Hash[MAXN]; //Hash前缀
void init(){
B[0]=1;
for (int i=1;i<=n;i++){
B[i]=B[i-1]*base;
Hash[i]=Hash[i-1]*base+a[i];
}
return;
}
int main(){
cin>>a+1;
n=strlen(a+1);
init();
cin>>m;
while (m--){
int l,r,l2,r2; cin>>l>>r>>l2>>r2;
if (Hash[r]-Hash[l-1]*B[r-l+1]==Hash[r2]-Hash[l2-1]*B[r2-l2+1])
cout<<"Yes\n";
else
cout<<"No\n";
}
return 0;
}
例题
P3370【模板】字符串哈希
Code
#include <bits/stdc++.h>
#define MAXN 10003
#define MAXM 1503
#define ull unsigned long long
using namespace std;
int n;
pair <ull,ull> Hash[MAXN];
const int b=127,b2=175;
unsigned long long B[MAXM],B2[MAXM];
void init(){
B[0]=B2[0]=1;
for (int i=1;i<=MAXM-2;i++){
B[i]=B[i-1]*b;
B2[i]=B2[i-1]*b2;
}
return;
}
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
init();
cin>>n;
for (int i=1;i<=n;i++){
string a;
cin>>a;
int len=a.length();
for (int j=0;j<=len-1;j++){
Hash[i].first+=(unsigned long long)(a[j]*B[len-j-1]);
Hash[i].second+=(unsigned long long)(a[j]*B2[len-j-1]);
}
}
sort(Hash+1,Hash+1+n);
int ans=0;
for (int i=1;i<=n;i++){
if (Hash[i]!=Hash[i-1]) ans++;
}
cout<<ans<<endl;
return 0;
}
P3805【模板】manacher
给出一个只由小写英文字符 \(\text{a,b,c,..,y,z}\) 组成的字符串 \(S\) ,求 \(S\) 中最长回文串的长度 。
字符串长度为 \(n\) 。
\(1 \leq n \leq 1.1 \times 10^7\) 。
Solution
manacher
板题也可以用 Hash 来做(bushi 。
设 \(F_i\) 为右端点为 \(i\) 的最长回文串的长度,显然有 \(F_i \leq F_{i-1}+2\) 。
我们可以枚举右端点 \(i\) ,每次从 \(F_{i-1}+2\) 开始往下枚举答案,然后采用快速计算字串 Hash 的方法判断是否为回文串,总复杂度为 \(O(n)\) 级别,足以通过此题。
复杂度证明:
可以发现从 \(F_0=0\) 开始 ,\(F_n\) 最大为 \(2n\) ,所以在枚举过程中,至多进行 \(2n\) 次 check 。故复杂度为 \(O(n)\) 级别。
Code
Tip:由于空间限制,未使用双模数 Hash 。
#include <bits/stdc++.h>
#define MAXN 11000003
#define ull unsigned long long
using namespace std;
int n;
char a[MAXN],b[MAXN];
const int base=157;
ull B[MAXN];
ull Hasha[MAXN],Hashb[MAXN];
void init(){
B[0]=1;
for (int i=1;i<=n;i++){
B[i]=B[i-1]*base;
Hasha[i]=Hasha[i-1]*base+a[i];
}
for (int i=n;i>=1;i--){
Hashb[i]=Hashb[i+1]*base+a[i];
}
return;
}
int ans[MAXN];
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>a+1;
n=strlen(a+1);
init();
int ANS=0;
for (int i=1;i<=n;i++){
int j;
for (j=ans[i-1]+2;;j--){
int o=i-j+1;
if (o<=0) continue;
int k=i-j/2;
if (j&1){
if (Hasha[i]-Hasha[k]*B[i-k]==Hashb[o]-Hashb[k]*B[i-k]) break;
} else {
if (Hasha[i]-Hasha[k]*B[i-k]==Hashb[o]-Hashb[k+1]*B[i-k]) break;
}
}
ANS=max(ANS,j);
ans[i]=j;
}
cout<<ANS<<endl;
return 0;
}
P5546 [POI2000] 公共串
给出几个由小写字母构成的单词,求它们最长的公共子串的长度。
\(1 \leq n \leq 5\) , \(1 \leq m \leq 2000\)
Solution
二分公共字串的长度 \(x\) ,把所有长度为 \(x\) 的字串塞到每个串的哈希表里,相同哈希值累加到 \(n\) 即有长度为 \(x\) 的公共字串。
Code
#include <bits/stdc++.h>
#define MAXN 8
#define MAXM 2003
#define ull unsigned long long
using namespace std;
int n;
string s[MAXN];
int len[MAXN];
vector <ull> Hash[MAXN];
const int base=127;
ull B[MAXM];
void init(){
B[0]=1;
for (int i=1;i<=MAXM-2;i++){
B[i]=B[i-1]*base;
}
for (int i=1;i<=n;i++){
Hash[i].push_back(0);
for (int j=1;j<=len[i];j++){
Hash[i].push_back(Hash[i][j-1]*base+s[i][j]);
}
}
return;
}
map <ull,bool> mp[MAXN];
map <ull,int> sum;
bool check(int x){
for (int i=1;i<=n;i++) mp[i].clear();
sum.clear();
for (int i=1;i<=n;i++){
for (int j=1;j<=len[i]-x+1;j++){
int k=j+x-1;
ull now=Hash[i][k]-Hash[i][j-1]*B[k-j+1];
if (!mp[i][now]){
sum[now]++;
mp[i][now]=1;
}
}
}
map <ull,int>::iterator it;
for (it=sum.begin();it!=sum.end();it++){
if (it->second==n) return true;
}
return false;
}
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n;
for (int i=1;i<=n;i++){
cin>>s[i];
len[i]=s[i].length();
s[i]=" "+s[i];
}
init();
int l=0,r=2003,ans=0;
while (l<=r){
int mid=(l+r)>>1;
if (check(mid)) l=mid+1,ans=mid;
else r=mid-1;
}
cout<<ans<<endl;
return 0;
}
参考资料
- 字符串哈希 ——oi-wiki
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)