字符串哈希
字符串哈希从入门到精通
前言
字符串哈希是处理字符串匹配、子串查询等问题的利器。它的核心思想是将字符串映射为一个整数,从而将字符串比较转化为整数比较,将 O(n) 的比较降为 O(1)。
本文通过三道模板题,带你从零掌握字符串哈希。
一、哈希原理
1.1 基本概念
把一个字符串看作一个 B 进制数:
hash(s) = s[0] * B^(n-1) + s[1] * B^(n-2) + ... + s[n-1]
其中 B 是一个大于字符集大小的数(通常取 131 或 13331)。
1.2 为什么用 unsigned long long
利用无符号整数自然溢出取模(相当于对 2^64 取模),省去取模运算,速度更快。
1.3 预处理
const int N=1e6+10;
unsigned long long hs[N], pk[N];
int k = 131;
// 预处理
pk[0] = 1;
for(int i=1;i<=n;i++){
hs[i] = hs[i-1] * k + s[i]; // 前缀哈希
pk[i] = pk[i-1] * k; // k 的幂
}
// 子串哈希
ull gethash(int l,int r){
return hs[r] - hs[l-1] * pk[r-l+1];
}
二、模板题:P3370 字符串哈希
题目
给定 n 个字符串,求不同字符串的个数。
思路
对每个字符串求哈希值,排序后去重。
代码
#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const ull N=1e6+10;
ull k=131;
ull hs[N];
ull as[N];
int main(){
ios_base::sync_with_stdio(0);
cin.tie(0);
int n;
cin>>n;
string s;
for(int op=1;op<=n;op++){
cin>>s;
hs[0]=0;
int len=s.size();
s=" "+s;
for(int i=1;i<=len;i++){
hs[i]=hs[i-1]*k+s[i];
}
as[op]=hs[len];
}
sort(as+1,as+1+n);
int cnt=1;
for(int i=1;i<n;i++){
if(as[i]!=as[i+1]) cnt++;
}
cout<<cnt;
return 0;
}
关键点
- 每个字符串独立计算哈希,存放在
as[]数组中 - 排序后统计不同值
三、兔子与兔子:P10468
题目
给定一个字符串,多次询问两个子串是否相同。
思路
预处理前缀哈希和 k 的幂,O(1) 查询子串哈希。
代码
#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const ull N=1e6+10;
ull k=131;
ull hs[N], pk[N];
ull gethash(ull l,ull r){
return hs[r]-hs[l-1]*pk[r-l+1];
}
int main(){
ios_base::sync_with_stdio(0);
cin.tie(0);
string s;
cin>>s;
ull len=s.size();
s=" "+s;
pk[0]=1;
for(ull i=1;i<=len;i++){
hs[i]=hs[i-1]*k+s[i];
pk[i]=pk[i-1]*k;
}
ull m;
cin>>m;
while(m--){
ull l1,r1,l2,r2;
cin>>l1>>r1>>l2>>r2;
if(gethash(l1,r1)==gethash(l2,r2)){
cout<<"Yes\n";
}else{
cout<<"No\n";
}
}
return 0;
}
关键点
pk[i]预处理k^i- 子串哈希公式:
hs[r] - hs[l-1] * pk[r-l+1]
四、匹配统计:P10479
题目
给定文本串 a 和模式串 b,对 a 的每个后缀,求其与 b 的最长公共前缀(LCP)长度。最后回答若干次询问,输出 LCP 长度等于 x 的后缀个数。
思路
- 预处理
a和b的哈希 - 二分 LCP 长度(单调性:若前 mid 个字符匹配,则更短的也匹配)
- 统计每个 LCP 长度的出现次数
代码
#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const int N=2e5+10;
ull k=131;
int n,m,q;
string a,b;
ull hs1[N],hs2[N],pk[N];
ull gethash1(int l,int r){
return hs1[r]-hs1[l-1]*pk[r-l+1];
}
ull gethash2(int l,int r){
return hs2[r]-hs2[l-1]*pk[r-l+1];
}
int lcp(int i){
int l=1,r=min(n-i+1,m),res=0;
while(l<=r){
int mid=(l+r)>>1;
if(gethash1(i,i+mid-1)==gethash2(1,mid)){
res=mid;
l=mid+1;
}else{
r=mid-1;
}
}
return res;
}
int cnt[N];
int main(){
ios_base::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m>>q;
cin>>a>>b;
a=" "+a;
b=" "+b;
pk[0]=1;
for(int i=1;i<=n;i++){
hs1[i]=hs1[i-1]*k+a[i];
pk[i]=pk[i-1]*k;
}
for(int i=1;i<=m;i++){
hs2[i]=hs2[i-1]*k+b[i];
}
for(int i=1;i<=n;i++){
int len=lcp(i);
cnt[len]++;
}
while(q--){
int x;
cin>>x;
if(x>m) cout<<"0\n";
else cout<<cnt[x]<<"\n";
}
return 0;
}
关键点
- 二分 LCP 时,
mid是长度,用哈希判断a[i..i+mid-1]是否等于b[1..mid] cnt[x]统计 LCP 长度恰好为x的后缀个数
五、常见问题与避坑指南
5.1 哈希冲突
自然溢出(unsigned long long)在绝大多数情况下足够安全。若想更保险,可以使用双哈希(两个不同的进制或模数)。
5.2 进制选择
- 常用
131、13331 - 进制应大于字符集大小(字母数字 128 足够)
5.3 下标处理
字符串建议从 1 开始,避免 0 的边界问题:
s = " " + s;
5.4 幂数组
pk[i] 需要预处理到字符串长度,否则子串哈希公式会出错。
总结
| 题目 | 核心技巧 | 复杂度 |
|---|---|---|
| P3370 | 字符串哈希 + 排序去重 | O(n L log n) |
| P10468 | 前缀哈希 O(1) 子串查询 | O(n + m) |
| P10479 | 哈希 + 二分 LCP | O(n log m + q) |
掌握这三道题,字符串哈希的基本应用就过关了。核心在于理解 hash 的进制表示和 子串哈希 的计算公式。

浙公网安备 33010602011771号