描述: Manacher 算法用于求解字符串中最长回文子串的问题。
在了解该算法之前需要明白一些基本概念:
解法:直观的暴力做法时间复杂度为 O ( n 3 ) O(n^3) O(n3),即枚举所有的子串,对这些子串判断是否回文。
稍加思考便会发现,对于一个回文串,他一定有一个对称中心点,这个点的左右两边是相同的,如果按照对称中心来依次枚举,那么复杂度为变为 O ( n 2 ) O(n^2) O(n2)
Manacher算法实际上就是对枚举对称中心这一做法的优化。
字符个数的奇偶性不同,对称中心也是不同的,例如对于奇回文串 a b a aba aba,对称中心点为 b b b,对于偶回文串 a b b a abba abba,对称中心点则为 b b bb bb中间,为了统一对称点,一般的做法是,将原字符串 s s s的首尾以及相邻的两个字符中间插入一个不会出现的字符(例如:#)这样会统一成一个奇回文串,新字符串长度 = 原串 * 2 + 1
例如原串: a b b a abba abba
新字符串: ∗ a ∗ b ∗ b ∗ a ∗ *a*b*b*a* ∗a∗b∗b∗a∗
考虑维护一个数组: L L L
其中 L [ i ] L[i] L[i]表示以 i i i为对称中心的最长回文串的最右边的字符到 s [ i ] s[i] s[i]的长度(即回文半径)
对于原串: a a b a b aabab aabab,添加一个字符变为 ∗ a ∗ a ∗ b ∗ a ∗ b ∗ *a*a*b*a*b* ∗a∗a∗b∗a∗b∗,他的 L L L数组如下:
* | a | * | a | * | b | * | a | * | b | * |
---|---|---|---|---|---|---|---|---|---|---|
1 | 2 | 3 | 2 | 1 | 4 | 1 | 4 | 1 | 2 | 1 |
L L L数组的作用: L [ i ] − 1 L[i]-1 L[i]−1可以表示该子串在原串中对应的回文串的长度,例如上面图片中 b b b的 L L L值为4,那么原串 a a b a b aabab aabab中,以 b b b为对称中心的回文串长度为 4 − 1 = 3 4-1=3 4−1=3
因此,只需要找到所有的 L L L数组就可以求出最长回文串了。
假定要求位置 i i i的 L L L数组,( i i i之前的已经求出),我们考虑维护两个值 k k k, r r r。其中 r r r表示, i i i之前所有位置求出来的右端点的最大值(即 L L L数组的最大值), k k k表示 r r r最大时的位置。
那么可以分为以下情况讨论
1、 i < r i < r i<r时
如上图所示,其中红色和橙色部分是相同的,找到 i i i关于 k k k的对称位置,即 j j j(其中 j = 2 ∗ k − i j = 2*k-i j=2∗k−i ),由于 k k k是对称中心位置,那么判断一下 L [ j ] L[j] L[j]与 r − i r-i r−i的大小关系
l [ j ] < r − i l[j]
绿色部分必定相同,因此 l [ i ] = l [ j ] l[i] = l[j] l[i]=l[j]
l [ j ] > r − i l[j] > r-i l[j]>r−i
此时, l [ i ] > = r − i l[i] >= r-i l[i]>=r−i的,超出的部分,需要暴力更新。
2、 i > r i>r i>r时
没有可以使用的信息,直接暴力更新即可。
注意:记得更新 k , r k,r k,r的值。
一些技巧:可以在字符串首尾放置两个"哨兵",它们和其他字符串均不同,这样暴力更新时不用考虑边界问题
//其中s[0]='@',s[n]='%',中间插入的字符为'#'
int ans = 0, k = 0, r = 0;
l[1] = 1;
for (int i = 2; i <= n; i ++) {
if (i < r) l[i] = min (r - i, l[(k << 1) - i]);
else l[i] = 1;
while (s[i - l[i]] == s[i + l[i]]) l[i] ++; // 暴力更新超过r的部分
if (i + l[i] > r) k = i, r = i + l[i]; // 更新k,r
ans = max (ans, l[i] - 1); // 找到最长回文子串
}
cout << ans << endl;
题目描述
https://www.luogu.com.cn/problem/P3805
给出一个只由小写英文字符 a , b , c , … y , z \texttt a,\texttt b,\texttt c,\ldots\texttt y,\texttt z a,b,c,…y,z 组成的字符串 S ,求 S 中最长回文串的长度 。
字符串长度为 n。
输入格式
一行小写英文字符 a , b , c , ⋯ , y , z \texttt a,\texttt b,\texttt c,\cdots,\texttt y,\texttt z a,b,c,⋯,y,z 组成的字符串 S。
输出格式
一个整数表示答案。
输入样例
aaa
输出样例
3
说明/提示
1 ≤ n ≤ 1.1 × 1 0 7 1\le n\le 1.1\times 10^7 1≤n≤1.1×107。
参考程序
#include
#include
using namespace std;
const int N = 2e7 + 2e6;
string t, s;
int n, m;
int l[N];
int main () {
cin >> t;
m = t.length();
n = (m << 1) + 1;
s.resize(n + 1);
s[0] = '@', s[n] = '%', s[1] = '#';
for (int i = 1; i <= m; i ++) {
s[i << 1] = t[i - 1];
s[i << 1 | 1] = '#';
}
int ans = 0, k = 0, r = 0;
l[1] = 1;
for (int i = 2; i <= n; i ++) {
if (i < r) l[i] = min (r - i, l[(k << 1) - i]);
else l[i] = 1;
while (s[i - l[i]] == s[i + l[i]]) l[i] ++;
if (i + l[i] > r) k = i, r = i + l[i];
ans = max (ans, l[i] - 1);
}
cout << ans << endl;
return 0;
}