一、用途:
给一个字符串,求它的最长回文子串;比如:
s = "abbacbca"
,最长回文子串为 "acbca"
,长度为
5
5
;
如果用暴力的算法,枚举对称轴,向两边延伸;复杂度高达
O(n2)
O
(
n
2
)
!
有个叫 Manacher 的人发明了一种算法,可以
O(n)
O
(
n
)
的求出最长回文子串,就叫 Manacher 算法(俗称 马拉车算法);
二、算法详情:
2.1 预处理:
回文串分为 奇回文串(如 "acbca"
) 和 偶回文串(如 "abba"
),处理的时候判奇偶是一件很麻烦的事,所以用一个小技巧对原串进行预处理:
∙
∙
在头尾以及两两字符中间都插入一个无关字符 (没有出现在这个串中的字符);
举个例子:"abcd"
填充之后 变为 "#a#b#c#d#"
;只要是无关字符都可以用来填充;
偶回文串 "abba"
处理后变为 "#a#b#b#a#"
,奇回文串 "acbca"
处理后变为 "#a#c#b#c#a#"
,长度都变成了奇数;
2.2 p[] 的定义:
首先定义一个
p
p
数组:
p[i]
p
[
i
]
表示以
i
i
为中心的回文串的最大回文半径。
比如:
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
New_s | @ | # | a | # | b | # | b | # | a | # | c | # | b | # | c | # | a | # |
p[i] | 1 | 2 | 1 | 2 | 5 | 2 | 1 | 2 | 1 | 2 | 1 | 6 | 1 | 2 | 1 | 2 | 1 |
可以看出, max(p[i]−1) m a x ( p [ i ] − 1 ) 就是原串的最长回文子串的长度;
2.3 求解p[]
假设我们正在求
p[i]
p
[
i
]
,即
p[1,⋯,i−1]
p
[
1
,
⋯
,
i
−
1
]
都已求出;
新增两个变量 Mr 和 Mid,定义如下:
Mr : 中心在
i
i
之前的所有回文子串,所能延伸至的最右端的位置;
Mid : 右端延伸至 Mr 处的回文子串的中心位置;
即 Mid + p[Mid] = Mr;
假设变量的相对位置如图:
(1) if (i < Mr)
即以 Mid 为中心的回文串为黑色的那段覆盖了红色的两段,根据回文串的性质,有 以 j 为中心 的回文串 和 以 i 为中心 的回文串相等,即图中红色两段相等;
既然这样,就不用以
i
i
为中心向两边延伸了,直接 p[i] = p[j]
,加速查找;
(2) else
即
i
i
<script type="math/tex" id="MathJax-Element-5811">i</script> 在 Mr 右边,这种情况,只能老老实实向两边延伸了;
2.4 参考代码:
/**********************************************
*Author* :XzzF
*Created Time* : 2018/4/12 16:42:30
*Ended Time* : 2018/4/12 16:57:48
*********************************************/
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <algorithm>
using namespace std;
typedef long long LL;
const int inf = 1 << 30;
const LL INF = 1LL << 60;
const int MaxN = 110005;
char s[MaxN + 5];
char New_s[2 * MaxN + 5]; //填充字符之后的串
int p[2 * MaxN + 5]; //p[i]表示以i为中心的最长回文串半径
int Get_New() {
int len = strlen(s);
New_s[0] = '@'; New_s[1] = '#';
int L = 1;
for(int i = 0; i < len; i++) {
New_s[++L] = s[i];
New_s[++L] = '#';
}
New_s[L + 1] = '\0'; //不要忘了
return L;
}
int Manacher() {
int len = Get_New();
int Max_len = 0;
int Mr = 0; //遍历过的所有回文串中,所能到达的最右端的位置
int Mid = 0; //能到达最右端位置的回文串的中心位置
for(int i = 1; i <= len; i++) {
if(i < Mr)
p[i] = min(p[2 * Mid - i], Mr - i);
else p[i] = 1;
//不需边界判断,因为左有'@',右有'\0'
while(New_s[i - p[i]] == New_s[i + p[i]])
p[i]++;
//更新Mr
if(Mr < i + p[i]) {
Mid = i;
Mr = i + p[i];
}
Max_len = max(Max_len, p[i] - 1);
}
return Max_len;
}
int main()
{
while(scanf("%s", s) != EOF)
{
printf("%d\n", Manacher());
}
return 0;
}
算法复杂度分析: