字符串进阶
字符串进阶
字符串 Hash
多项式 Hash
\(f(s)=\sum^l_{r=1}s_i\times b^{i-1}\pmod M\)。
矩阵 Hash
主要应用于字符串集合的 Hash。
若使用多项式 Hash,字符串集合 \(\{ab,bd\}\) 和 \(\{ad,bd\}\) 的 Hash 值相当。
将每一个字符抽象为一个 \(2\times 2\) 的矩阵,对于同一个字符串之间矩阵相乘,不同字符串之间矩阵相加,因为矩阵乘法通常无交换律,所以 Hash 值一般不同。
例题
P3370
多项式 Hash 模板。
经典应用
给定串 \(S,q\) 次询问,每次询问两个位置 \(l_1,l_2\) 的最长公共前缀,\(|S|,q\leq3\times10^5\)。
正解: \(Hash\) 和二分。
CF 1017E
UOJ552
相当于给定两个字符串可重集合,求长度最小,字典序最小的字符串。
显然符合矩阵 Hash 的性质。
每个点记录以它为起点的 Hash 值。
KMP 算法
一种非常简单的字符串匹配算法。
前缀函数
定义
设 \(nxt_i\) 为最长的、前缀与后缀相等的、前缀的长度。
例如,字符串 abcabcd 的 \(nxt\) 数组为 \(\{0,0,0,1,2,3,0\}\)。
实现
for(int i=1,j=0;i<s2.length();i++){
while(j && s2[i] != s2[j]){//匹配不到或者走到了开头,就回调到上一个可以匹配上的位置。
j = nxt[j];
}
if(s2[i] == s2[j]){//匹配上了就往后走一个。
j ++;
}
nxt[i+1] = j;
}
KMP 算法
应用场景
解决在字符串 \(T\) 中匹配字符串 \(P\) 的问题。
步骤
- 先对字符串 \(P\) 自我匹配,即求前缀函数;
- 再利用 \(nxt\) 数组在 \(T\) 数组中匹配。
实现
for(int i=1;j=0;i<=s2.length();i++){
while(j && s2[i] != s2[j]){
j = nxt[j];
}
if(s1[i] == s2[j]){
j ++;
}
nxt[i+1] = j;
}
for(int i=0,j=0;i<s1.length();i++){
while(j && s1[i] != s2[j]){
j = nxt[j];
}
if(s1[i] == s2[j]){
j ++;
}
if(j == s2.lenght()){
ans[++cnt] = i-s2.lengyh()+2;
}
}
例题
NOI2014 动物园 (P2375)
扩展 KMP 算法(Z 函数)
应用场景
从字符串 \(S\) 的某一位出发,最多能匹配 \(P\) 中的多少字符。
Z 函数
定义
对于一个长度为 \(N\) 的字符串 \(S\),\(z_i\) 表示字符串 \(S\) 与字符串 \(S_{i:N}\) 的最长公共前缀的长度。
例如,字符串 \(S=aaabaaabc\),所对应的 \(z=\{9,2,1,0,4,2,1,0\}\)。
朴素算法
暴力求解
对于每一个位置,直接扫描即可。时间复杂度 \(O(N^2)\)。
Hash + 二分
判断是否相同时,使用 \(Hash\) 优化即可。时间复杂度 \(O(n\log m+m)\)。
Z-box
定义
\(Z-box\) 是字符串 \(S\) 的一个区间 \([l,r]\) ,满足这段区间是 \(S\) 的前缀。我们希望 \(r\) 尽可能大。
例如字符串 \(S=aaabaaabc\) 时,\([4,7]\) 区间显然是 \(S\) 的前缀。
所以我们可以直接由 \([1,3]\) 区间的 \(z_i\) 推导出 \([5,7]\) 区间的 \(z_i\)。
实现
for(int i=2,l=0,r=0;i<=n;i++){
if(i <= r){// 在 Z-box 中;
z[i] = min(z[i-l+1],r-i+1);
//z[i] 可以直接由 z[i-l+1] 得到;
//但是右端点不会大于 Z-box 的右端点;
//所以长度最大为 r-i+1;
}
while(s[1+z[i]] == s[i+z[i]]){
// 预判下一位能不能选
z[i] ++;
}
if(i+z[i]-1 > r){
// 如果右端点比当前的 Z-box 更靠右,即可以更新更多的点;
// 当前 Z-box 就没用了;
// 所以开新的 Z-box;
l = i;
r = i+z[i]-1;
}
}
右端点 \(r\) 的移动不会超过 \(n\) 所以,时间复杂度是 \(O(N)\) 的。
例题
P5410
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e7+5;
char a[N],b[N];
int n,m;
int z[N],p[N];
void get_z(char *b){
z[1] = n;
for(int i=2,l=0,r=0;i<=n;i++){
if(i <= r){// 在 Z-box 中;
z[i] = min(z[i-l+1],r-i+1);
//z[i] 可以直接由 z[i-l+1] 得到;
//但是右端点不会大于 Z-box 的右端点;
//所以长度最大为 r-i+1;
}
while(b[1+z[i]] == b[i+z[i]]){
// 预判下一位能不能选
z[i] ++;
}
if(i+z[i]-1 > r){// 右端点比当前的 Z-box 更靠右,即可以更新更多的点;
// 开新的 Z-box;
l = i;
r = i+z[i]-1;
}
}
ll ans = 0;
for(int i=1;i<=n;i++){
// cout<<z[i]<<' ';
ans ^= (ll)i*((ll)z[i]+1ll);
}
printf("%lld\n",ans);
}
void exkmp(char *a,int m,char *b,int n){
for(int i=1,l=0,r=0;i<=m;i++){
if(i <= r){
p[i] = min(z[i-l+1],r-i+1);
}
while(i+p[i] <= m && a[i+p[i]] == b[p[i]+1]){
p[i] ++;
}
if(i+p[i]-1 > r){
l = i;
r = i+p[i]-1;
}
}
ll ans = 0;
for(int i=1;i<=m;i++){
ans ^= (ll)i*((ll)p[i]+1ll);
}
printf("%lld",ans);
}
int main(){
scanf("%s%s",a+1,b+1);
m = strlen(a+1);
n = strlen(b+1);
get_z(b);
exkmp(a,m,b,n);
}
其中第二问,相当于将字符串 \(a\) 与字符串 \(b\) 相连后求解后 \(strlen(a)\) 个 \(z_i\)。
Manacher
定义
- 回文串:正向读和反向读的结果相同的字符串,即 \(\forall s_i=s_{N-i}\)。
- 回文子串:既是回文串又是子串的字符串。
- 奇字符串:长度为奇数的字符串。
- 偶字符串:长度为偶数的字符串。
Manacher 通常适用于求给定字符串 \(S\) 的最长回文子串长度。
朴素算法
暴力
我们可以枚举起点和终点,并判断该字符串是否为回文,时间复杂度为 \(O(N^3)\)。
考虑优化,我们只需要枚举字符串的中间位置向两边扩展即可,时间复杂度为 \(O(N^2)\)。
Hash + 二分
枚举每个位置,二分字符串长度,\(Hash\) 判断是否为回文,时间复杂度 \(O(n\log n)\)。
实现步骤
设 \(S=aaba\)。
改造字符串
将字符串每两个字符中间添加任意在字符串中为出现过的字符,如 #。
此时 \(S=\#a\#a\#b\#a\#\),此时所有的回文子串一定是奇回文串,方便接下来的处理。
为了便于实现,通常在字符串开头插入另一个字符,例如 $ 等。
此时 \(S\) 为 $#a#a#b#a#。
加速盒子
与扩展 KMP 类似,Manacher 算法也有“盒子”。
类似的,我们维护右端点最靠右的回文子串,我们利用这个盒子,借助之前的状态,加速计算。
同样,盒内快速转移,盒外暴力枚举即可。

浙公网安备 33010602011771号