题意: 给定一个长度为$n$的串,求出这个串中长度最长回文子串的长度。
数据范围:$1\leq n\leq 10^7$
题解:
考虑最直接的暴力,即从最长子串长度开始枚举每个子串,然后判断其中每个子串是否符合回文串,时间复杂度:$O(n^3)$
考虑枚举每个点作为回文串的中心,需要分为奇回文和偶回文两种情况,时间复杂度:$O(n^2)$
看到该数据范围可以应该意识到这个问题需要$O(n)$的时间复杂度才能求解。
引入$Manacher$算法,用于$O(n)$时间求出最长回文子串。
这里借用了第二种暴力:以每个点作为回文中心进行回文串扩展的思想。
第二种暴力进行扩展时,会有一些额外的信息,我们此时可以利用该信息作为求解之后的点作为回文中心时进行扩展得到回文串的直接信息,从而优化。
具体思想和做法:
- 首先我们加入一些字符使得整个串只需要去处理奇回文串,$n$个字符共有$n+1$个空,包括最左和最右,我们在这$n+1$个空中添加字符’#‘,此时可以保证原串中所有的奇回文和偶回文都可以转换为奇回文,不过是奇回文转换后仍以原串中的字符作为回文中心,偶回文转换后以#‘作为回文中心,为了减少边界问题的单独处理,再在串首和串尾添加两个与已有的所有字符都不相同的字符,通常使用:’$’和’@’。
-
这里引入两个点:$mid$和$maxr$,$maxr$表示以$mid$作为回文中心的回文串的右边界,该右边界也是枚举到$i$这个点前的所有回文串的右边界可以到达的最远右边界。设$p[i]$为以$i$为回文中心的最大回文半径的长度。接下来我们考虑三个点:
- 当前枚举的回文中心$i < maxr$,$i$关于$mid$的对称点$j=mid\times 2-i$,$maxr$关于$mid$的对称点$maxl=mid\times 2-maxr$。
- 如果$j-p[j]>maxl$,$p[i]=p[j]$
- 如果$j-p[j]=maxl$,$p[i]$至少为$p[j]$,之后再向两边扩展即可
- 如果$i\geq maxr$,直接向两边扩展即可
- 扩展结束后,更新下$mid$和$maxr$
- 当前枚举的回文中心$i < maxr$,$i$关于$mid$的对称点$j=mid\times 2-i$,$maxr$关于$mid$的对称点$maxl=mid\times 2-maxr$。
-
时间复杂度证明:当$i < maxr$且$j-p[j] > maxl$,此时不向两边扩展,此次操作时间复杂度为:$O(1)$;剩下两种情况:扩展成功时,$maxr$必然变大,所以$maxr$最多扩展$O(n)$次,即时间复杂度为:$O(n)$
代码:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 2e7 + 10;
char a[N], b[N];
int p[N];
int n;
void init() {
int k = 0;
b[k++] = '$', b[k++] = '#';
for(int i = 0; i < n; ++i) b[k++] = a[i], b[k++] = '#';
b[k++] = '@';
n = k;
}
void manacher() {
int maxr = 0, mid;
for(int i = 1; i < n; ++i) {
if(i < maxr) p[i] = min(p[mid * 2 - i], maxr - i);
else p[i] = 1;
while(b[i + p[i]] == b[i - p[i]]) ++p[i];
if(i + p[i] > maxr) {
maxr = i + p[i];
mid = i;
}
}
}
int main()
{
scanf("%s", a);
n = strlen(a);
init();
manacher();
int res = 0;
for(int i = 0; i < n; ++i) res = max(res, p[i]);
printf("%d\n", res - 1);
return 0;
}