字符串哈希学习笔记

First Post:

Last Update:

Word Count:
3.2k

Read Time:
14 min

字符串学到现在只会 SA,绷不住了。

还是得学学哈希,不然真考字符串就寄了。

什么是字符串哈希

通过一个函数把一个字符串映射为一个整数,这个函数就是哈希函数。

如何计算

我们通常采用多项式哈希。

为底数, 为模数,则

底数通常选择一个质数,模数通常选择一个大质数。

推荐:底数 131,模数 1926081719491001071

upd:131也被卡了,可以换成19260817。

如果要用的话记得开 int128,不然你一乘就炸了。

比较好记,就是江泽民主席的诞辰和建国时间拼起来再拼上071。

如果没想起来用 1e9+7 也行,不过容易被卡。

自然溢出还是算了吧,真怕被卡。

如何计算子串哈希值

首先处理出来前缀哈希值。

设第 位哈希值为 ,则

的哈希值,为

如何判断是否是回文串

正着算一遍哈希值,反着算一遍哈希值,如果一样说明是回文串。

如何得到两个串接起来得到的新串的哈希值

设两个字符串分别为 ,若把 接到 后面,则新串哈希值为

如果题目涉及到以上问题(即需要相乘)且你使用了大模数,请先想想会不会炸,需不需要 int128。

应用

大概就字符串匹配吧?还有别的用途吗?

字符串哈希厉害之处就在于可以在预处理后 匹配。

哈希还有一个常用搭配:二分。

因为字符串的匹配是有单调性的,长的匹配说明短的也匹配,所以可以二分长度然后用哈希判断。

例题

【模板】字符串哈希

第一道题肯定要放模板啦。

求出每个字符串的哈希值,那么问题就变成了序列中有几个不同的数。

非常简单吧。

[CTSC2014] 企鹅 QQ

因为要求恰好一位不同,所以考虑枚举哪一位不同。

因为要把枚举到的那一位删掉,所以得处理所有串的前缀哈希,然后删这一位就是把前后的拼起来。

求子串哈希和怎么拼起来都在上文提到过。

然后就变成模板了。

复杂度

[POI2010] KOR-Beads

首先枚举 ,然后枚举每一段。

听上去很暴力对不对?但复杂度其实是有保证的。

总时间复杂度应该是

提出来,剩下的是个调和级数。

所以时间复杂度

用哈希匹配是 的,所以总复杂度

但有个需要注意的:这道题的串正着和反着视为相同,所以既要处理前缀哈希还要处理后缀哈希,只有都不相同时才能记录到答案。

[POI2012] OKR-A Horrible Poem

首先,设 为答案,则 为答案的充要条件为

其次,答案一定是区间长度的约数。

所以我们从小到大枚举约数,然后哈希 判断。

复杂度

虽然约数个数是一个比 小一点的数,但依旧会 T。

考虑优化一下这个东西。

如果一个循环节出现了 2 次,另一个出现了 3 次,那么一定有一个会出现 6 次。

所以我们对每个质因数分别做,最后把它们合起来。

质因数个数是 级别的,可以通过。

[CQOI2014] 通配符匹配

因为通配符不超过 10,所以考虑把原串分成若干段,然后分段匹配,每段第一位为通配符。

表示分出来的第 个串能否匹配上查询串的 第 位。

转移为

这里的 代表按位或。

能否转移用哈希判断一下即可。

设通配符个数为 ,复杂度为

[POI2006] PAL-Palindromes

这不就是把上面那两个东西拼一块了吗。

设两个字符串分别为 ,那么这对字符串合法的条件为

移项,得到

对每个字符串求出这个东西,然后随便统计一下即可。

[TJOI2017] DNA

允许 次失配的字符串匹配。

考虑二分+哈希。

首先枚举所有可能匹配的子串,然后二分找到第一个失配的位置,把这个位置及之前的东西删掉,然后再进行这个过程。

如果失配次数 ,说明合法。

设原串长度为 ,模式串长度为 ,复杂度

「TAOI-2」Ciallo~(∠・ω< )⌒★

Ciallo~(∠・ω< )⌒★

洛谷题解传送门

一道很好玩的题。

直接做可能没什么想法,那我们先对 讨论一下。

这里的 都是以原串为下标的。

1. 无交

那说明删除没任何影响。我们先找出所有匹配位置,然后随便算一下方案数就行了。

2. 有交。

不难发现,合法的 一定形如下图

图中的 分别代表

长为 长为

红色部分为重复部分,长为

不难发现,对于这种情况,合法的方案数为

然后观察这种情况需要满足什么条件,不难发现,需要满足

不能等于 是因为这部分贡献已经在前面算过了,不能重复计算。

然后这东西就是个二维数点,数就完了。

数点时记得减去贡献。

树状数组 add 时可能会用到 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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include"bits/stdc++.h"
#define re register
#define int long long
#define lb(x) (x&(-x))
using namespace std;
const int maxn=4e5+10,base=131,mod=1e9+7;
int n,m,ans;
char s1[maxn],s2[maxn];
int pw[maxn];
int h1[maxn],h2[maxn];
int x[maxn],y[maxn];
struct BIT{
int tr[maxn];
inline void add(int x,int val){++x;while(x<maxn) tr[x]+=val,x+=lb(x);}
inline int query(int x){++x;int res=0;while(x>0) res+=tr[x],x-=lb(x);return res;}
}a,b;
inline int calc1(int l,int r){return ((h1[r]-h1[l-1]*pw[r-l+1]%mod)%mod+mod)%mod;}
inline int calc2(int l,int r){return ((h2[r]-h2[l-1]*pw[r-l+1]%mod)%mod+mod)%mod;}
inline bool check1(int pos,int len){
if(pos+len-1>n) return 0;
return calc1(pos,pos+len-1)==calc2(1,len);
}
inline bool check2(int pos,int len){
if(pos-len+1<1) return 0;
return calc1(pos-len+1,pos)==calc2(m-len+1,m);
}
inline void init(){
pw[0]=1;
for(re int i=1;i<maxn;++i) pw[i]=pw[i-1]*base%mod;
for(re int i=1;i<=n;++i) h1[i]=(h1[i-1]*base+s1[i])%mod;
for(re int i=1;i<=m;++i) h2[i]=(h2[i-1]*base+s2[i])%mod;
for(re int i=1,l,r,mid,res;i<=n;++i){
l=0,r=m,res=0;
while(l<=r){
mid=(l+r)>>1;
if(check1(i,mid)) res=mid,l=mid+1;
else r=mid-1;
}
x[i]=res;
l=0,r=m,res=0;
while(l<=r){
mid=(l+r)>>1;
if(check2(i,mid)) res=mid,l=mid+1;
else r=mid-1;
}
y[i]=res;
}
}
signed main(){
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>(s1+1)>>(s2+1);n=strlen(s1+1),m=strlen(s2+1);
init();
for(re int i=1;i<=n;++i) if(y[i]==m) ans+=((i-m)*(i-m+1)/2+(n-i)*(n-i+1)/2);
for(re int i=m+1;i<=n;++i){
a.add(m-x[i-m],1),b.add(m-x[i-m],x[i-m]);
ans+=b.query(y[i])+(y[i]-m+1)*a.query(y[i]);
if(y[i]==m) ans-=a.query(y[i]);
ans-=a.query(0);
}
cout<<ans;
return 0;
}

[蓝桥杯 2016 国 AC] 碱基

数据范围开这么小随便做吧。

直接爆搜选哪些串,然后把长度为 的子串的哈希值存起来。

这样对于每一个哈希值可以求出它在每个串中各出现了多少次,乘起来即可。

复杂度

【模板】manacher

马拉车过于困难,考虑用哈希搞搞部分分。

部分分还是很好做的,我们处理出正着的哈希值和倒着的哈希值,然后枚举对称轴并二分长度,就做完了。

复杂度

事实上哈希也是能做到线性的,详见OI Wiki

[POI2010] ANT-Antisymmetry

二分+哈希。

基本和上题一样。

处理出原串的哈希值和取反后的哈希值,然后枚举对称轴并二分长度。

对于统计答案,可以发现只有总长为偶数才可能合法。

然后没了。

[USACO17OPEN] Bovine Genomics G

简单题。

枚举左端点,然后二分长度,然后哈希判断有没有重复。

二分正确性也比较显然:如果当前长度不合法,那就必须变长,否则可以变短。

复杂度

感觉做不到 ,因为不管用 set,map 还是离散化都得带 ,所以题解复杂度是不是都写错了?

[BalticOI 2014 Day1] Three Friends

枚举哪个是插入的,然后把它删掉。

然后看剩下的东西能不能组成两个相同串即可。

这里还是稍微有点细节的,因为涉及到子串哈希值和把两个串拼接起来。

观察样例一,你会发现把枚举的那个删掉以后,因为要组成两个相同串,所以要保证两段长度相等。

所以要给短的那一段拼上一部分。

大概就是这样吧,其他应该没什么细节。

复杂度

[NOIP2020] 字符串匹配

如果 确定了,那么 也是确定的。

所以考虑枚举 ,这样做的好处是 级别的,所以总复杂度是

然后要解决的问题就是出现次数了。

我们可以维护出现次数为奇数次的个数的前缀和以及后缀和,那么查询用树状数组搞一下即可。

为字符集大小,复杂度

似乎需要卡常。

upd:完全不用卡。

而且没卡自然溢出,跑的飞快。

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
51
52
53
54
55
56
57
58
#include"bits/stdc++.h"
#define re register
#define int long long
#define ull unsigned long long
using namespace std;
const int maxn=2e6+10,maxm=30,V=26,base=131;
int n;
ull pw[maxn],h[maxn];
int buc[maxm];
int pre[maxn],suf[maxn];
char s[maxn];
struct BIT{
#define lb(x) (x&(-x))
int tr[maxm];
inline void add(int x,int val){++x;while(x<=V+1) tr[x]+=val,x+=lb(x);}
inline int query(int x){++x;int res=0;while(x) res+=tr[x],x-=lb(x);return res;}
inline void clear(){memset(tr,0,sizeof tr);}
#undef lb(x)
}a;
inline void init(){pw[0]=1;for(re int i=1;i<maxn;++i) pw[i]=pw[i-1]*base;}
inline int calc(int l,int r){return h[r]-h[l-1]*pw[r-l+1];}
inline void solve(){
cin>>(s+1);n=strlen(s+1);
for(re int i=1;i<=n;++i) h[i]=h[i-1]*base+s[i];
memset(buc,0,sizeof buc);
for(re int i=1;i<=n;++i){
++buc[s[i]-'a'];
if(buc[s[i]-'a']&1) pre[i]=pre[i-1]+1;
else pre[i]=pre[i-1]-1;
}
memset(buc,0,sizeof buc);
suf[n+1]=0;//对于这种多测还要求类似后缀和的东西一定要注意清空!学弟CSP就被这个卡了
for(re int i=n;i>=1;--i){
++buc[s[i]-'a'];
if(buc[s[i]-'a']&1) suf[i]=suf[i+1]+1;
else suf[i]=suf[i+1]-1;
}
int ans=0;a.clear();
for(re int len=1;len<=n;++len){
ull hs=calc(1,len);
for(re int r=len;r<n;r+=len){
int l=r-len+1;
if(calc(l,r)==hs) ans+=a.query(suf[r+1]);
else break;
}
a.add(pre[len],1);
}
cout<<ans<<'\n';
}
signed main(){
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
init();int T;cin>>T;while(T--) solve();
return 0;
}