扩展 KMP(Z 函数)& Manacher 学习笔记

扩展 KMP(Z 函数)& Manacher 学习笔记

以下字符串从 \(1\) 开始标号。

扩展 KMP(Z 函数)

例题。求字符串 \(a\)\(a\) 的每一个后缀 \(i\) 的 LCP \(z_i\),求字符串 \(a\)\(b\) 的每一个后缀 \(i\) 的 LCP \(p_i\)

先看第一问,考虑枚举 \(i\)\(z_{1\dots{i-1}}\) 都已处理好了,接下来要求 \(z_i\)。我们维护此前找到的 \(r\) 最大的匹配串 \(a_{l\dots r}\)

如果 \(i\le r\),我们将 \(z_i=\min(z_{i-l+1},r-i+1)\)。这是因为 \(a_{i-l+1\dots r-l+1}\)\(a_{i\dots r}\) 是相同的。

否则如果 \(i>r\),那么 \(z_i=0\)

接下来我们暴力往后尝试扩展 \(z_i\),直接比较下一个位置是否匹配。最后尝试更新 \(l,r\)

那么由于每个位置只会被扩展一次,所以复杂度是 \(O(n)\)。实现如下,注意这里要特判掉 \(z_1=m\)

z[1]=m;
for(int l=0,r=0,i=2;i<=m;++i) {
    if(i<=r) z[i]=min(z[i-l+1],r-i+1);
    while(i+z[i]<=m&&b[i+z[i]]==b[z[i]+1]) ++z[i];
    if(i+z[i]-1>r) l=i,r=i+z[i]-1;
}

第二问是类似的,不过需要先求出 \(a\)\(z\) 数组,然后每次将 \(p_i=\min(z_{i-l+1},r-i+1)\)。实现如下:

for(int l=0,r=0,i=1;i<=n;++i) {
    if(i<=r) p[i]=min(z[i-l+1],r-i+1);
    while(i+p[i]<=n&&a[i+p[i]]==b[p[i]+1]) ++p[i];
    if(i+p[i]-1>r) l=i,r=i+p[i]-1;
}

Manacher

例题。对于字符串 \(a\) 的每个 \(a_i\) 我们求出以它为中心的最长回文串的长度 \(p_i\)

我们先在原字符串的头尾以及每相邻两个字符之间插入一个特殊字符,这样所有回文串就都是奇回文串,且合法回文串头尾都是特殊字符

然后枚举 \(i\),我们维护此前找到的 \(r\) 最大的以 \(mid\) 为中心的回文串 \(a_{l\dots r}\)\(l\) 不用记录)。

如果 \(i\le r\) 则因为 \(a_{l\dots mid}\)\(a_{mid\dots r}\) 是相同的,所以 \(p_i=\min(p_{2\times mid-i},r-i+1)\),其中 \(2\times mid-i\)\(i\) 关于 \(mid\) 的对称点。

否则如果 \(i>r\),则初始 \(p_i=0\)

接下来我们尝试暴力扩展 \(p_i\),然后尝试更新 \(mid,r\)

由于每个字符只会被扩展一次,所以复杂度是 \(O(n)\) 的。答案即 \(\max\{p_i-1\}\)。实现如下:

const int N=2.2e7+5;
char a[N];
int n,p[N],ans=0;
signed main(){
	read(a+1);
	n=strlen(a+1);
	fd(i,n,1) a[2*i]=a[i],a[2*i-1]='$';
	n=n*2+1;
	a[n]='$';
	for(int i=1,mid=0,r=0;i<=n;++i) {
		if(i<=r) p[i]=min(p[2*mid-i],r-i+1);
		while(p[i]<i&&i+p[i]<=n&&a[i-p[i]]==a[i+p[i]]) ++p[i];
		if(i+p[i]-1>r) r=i+p[i]-1,mid=i;
		ans=max(ans,p[i]-1);
	}
	write(ans);
	return 0;
}

Manacher 的性质

由 Manacher 的算法可知,本质不同的回文子串个数是 \(O(n)\) 的,因为只有当 \(p_i\) 进行扩展时有可能生成新的回文子串。

由此可以统计有关回文子串的信息,例题:APIO2014回文串

posted @ 2025-04-19 10:15  dengchengyu  阅读(17)  评论(0)    收藏  举报