字符串进阶

字符串进阶

字符串 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\) 的问题。

步骤

  1. 先对字符串 \(P\) 自我匹配,即求前缀函数;
  2. 再利用 \(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 算法也有“盒子”。

类似的,我们维护右端点最靠右的回文子串,我们利用这个盒子,借助之前的状态,加速计算。

同样,盒内快速转移,盒外暴力枚举即可。

posted @ 2023-12-10 17:19  WhileTureRP++  阅读(14)  评论(0)    收藏  举报