Manacher 算法
该算法由 Glenn K. Manacher 在 1975 年提出,首先注意到回文串的对称中心特性可能有所不同(中心可能为一个字符或者是在两个字符之间),那么我们将字母之间插入隔板,这两个回文串的对称中心就都在一个字符上了,such as "|A|B|B|A|"、"|A|B|C|B|A|"
过程
对于一个回文串,有且仅有一个对称中心,我们称之为回文对称中心。
在一个回文串内,任选一段区间 \(X\),一定存在关于“回文对称中心”对称的一个区间 \(Y\),我们把区间 \(Y\) 叫关于区间 \(X\) 的对称区间。
可得:
- 区间和对称区间一定全等。
- 若一个区间的对称区间是回文串,这个区间必定是一个回文串。在大的回文串内(即不考虑其他字符)它们回文半径相等
- 我们通过确定关系预先得到的回文半径的数值必定小于等于这个位置真实的回文半径。
因此,我们若记录以每个位置为中心的的回文串半径,我们可以通过另一个已确认的回文串中心把未确认的中心点对应过去提前赋值,这样可以帮助我们减少判断次数,优化时间复杂度。
假设目前未确认点为 \(i\),已确认的且覆盖点 \(i\) 的回文串中心、半径分别为 \(mid\)、\(r\),那么点 \(i\) 的对称点则为 \(j=2\times mid-i\),那么 \(j\leq mid \leq i\),点 \(j\) 作为回文中心的最大回文半径 \(r'\) 已经求得,我们可以预先确定一部分回文串,将点 \(i\) 初值赋为 \(\min(r', mid+r-i)\),至于为什么取 \(\min\),因为大回文串右端点为 \(mid+r-1\),点 \(i\) 初赋值不能判断到大区间外面的点,那么我们每个点肯定会从右端点最靠右的回文串赋值过来,每次我们存一下右端点最靠右的回文串的中心、半径即可,如果点 \(i\) 赋值为 \(mid+r-i\) 了,我们就可以扩展半径判断是否可行(可以感性理解一下)
按照这样做,每个字符只会被循环访问到一次,所以时间复杂度为 \(O(n)\),代码如下:
#include <bits/stdc++.h>
using namespace std;
int n, len[22000005], mid, mr, ans;
char s[11000005], t[22000005];
int main() {
scanf("%s", s+1);
n=strlen(s+1);
for(int i = 1; i <= n; ++i) t[i*2]=s[i], t[i*2-1]='#';/*中间填上字符*/
t[2*n+1]=t[0]='#', len[0]=len[1]=1, mid=1, mr=2;
for(int i = 2; i <= 2*n+1; ++i) {
if(i >= mr) len[i]=1;//比最右边还右边就直接赋值1
else len[i]=min(len[mid*2-i], mr-i);//从之前那里继承一点从而优化时间复杂度
while(t[i+len[i]] == t[i-len[i]]) ++len[i];//暴力扩展
if(i+len[i] > mr) {//更新
mr=i+len[i];
mid=i;
}
}
for(int i = 1; i <= 2*n+1; ++i)
ans=max(ans, (len[i]*2-1)/2);
printf("%d\n", ans);
return 0;
}
扩展 KMP/exKMP(Z 函数)
Z 函数,又称扩展 KMP (exkmp),可以 \(O(n)\) 求出一个字符串的所有后缀与这个字符串的 LCP 长度。
怎么叫做扩展 KMP 但是前置知识没有 KMP,Z 函数的做法与 Manacher 有着异曲同工之妙,即存下了目前已扩展到的右端点最靠右端的后缀 \(i\) 与原串的 LCP:\([i,i+Z[i]-1]\),可以发现,当 \(i<j\leq i+Z[i]-1\) 时,字符串 \([j,i+Z[i]-1]\) 与 \([j-i+1,z[i]]\)(相当于将 \([1,Z[i]]\)、\([i,i+Z[i]-1]\) 按 \(j\) 对应的位置切开取了右半边) 一定是相同的,那么 \(Z[j]\) 就可以直接初赋值上 \(\min\{Z[j-i+1],i+Z[i]-j\}\),这样就能保证每个字符只会被扩展到 \(1\) 次,因此总时间复杂度是 \(O(n)\) 的(纯意识流是因为懒得画图嘿嘿)。
于是我们可以在 \(O(|a|+|b|)\) 的时间中求模式串 \(b\) 与另一个字符串 \(a\) 所有后缀的 LCP 长度,具体的,可以先求出 \(b\) 的 Z 函数后参照求 Z 函数的思路求 \(a\) 的所有后缀与 \(b\) 的 LCP,也可以将 \(b\)、\(a\) 拼在一起后求一次 Z 函数,模板代码如下:(此处只展示第一种先求 \(b\) 的 Z 函数的做法 QwQ,毕竟会求 Z 函数就会第二种求法了)
#include <bits/stdc++.h>
using namespace std;
const int N = 2e7+5;
int n, m, z[N], p[N];
char a[N], b[N];
template <typename T> void read(T& x) {
x = 0; int f = 0; char c = getchar();
while(c < '0' || c > '9') f |= (c == '-'), c=getchar();
while(c >= '0' && c <= '9') x=(x<<1)+(x<<3)+(c^48), c=getchar();
x=(f ? -x : x);
}
int lne; char put[105];
template <typename T> void write(T x, char ch) {
lne = 0; if(x < 0) putchar('-'), x=-x;
do { put[++lne]=x%10, x/=10; } while(x);
while(lne) putchar(put[lne--]^48);
putchar(ch);
}
signed main() {
scanf("%s%s", a+1, b+1);
n=strlen(a+1), m=strlen(b+1);
//求Z函数:
z[1]=m;//初赋值
for(int i = 2, l = 0, r = 0; i <= m; ++i) {
if(i <= r) z[i]=min(z[i-l+1], r-i+1);//与Manacher异曲同工之处
while(i+z[i] <= m && b[i+z[i]] == b[z[i]+1]) ++z[i];//向右扩展没有被扩展过的位置
if(i+z[i]-1 > r) r=i+z[i]-1, l=i;//更新
}
//求a的后缀与b的LCP长度:
for(int i = 1/*从1开始,求Z函数时则不能从1开始*/, l = 0, r = 0; i <= n; ++i) {
if(i <= r) p[i]=min(z[i-l+1], r-i+1);//是对b求得的Z函数取min!一定要注意
while(i+p[i] <= n && a[i+p[i]] == b[p[i]+1]) ++p[i];//a与b比较
if(i+p[i]-1 > r) r=i+p[i]-1, l=i;//更新
}
long long ans1 = 0, ans2 = 0;
for(int i = 1; i <= m; ++i)
ans1^=1LL*i*(z[i]+1);
for(int i = 1; i <= n; ++i)
ans2^=1LL*i*(p[i]+1);
write(ans1, '\n');
write(ans2, '\n');
return 0;
}
看着似乎用处不大可以被二分加哈希代替但是在有时不能二分哈希或者卡常的时候还是很有用的