Manacher算法
Manacher算法通常用于计算一个字符串的最长回文子串,注意是子串而不是子序列,子序列有另一种算法。
对于最长回文子串的朴素方法是通过枚举左右两端的位置进而枚举所有的可能子串,然后在左右比较判断是否为回文,对一个子串进行判断的时间复杂度是 O ( n ) O(n) O(n),枚举的子串一共有 n 2 n^2 n2个,所以总复杂度是 O ( n 3 ) O(n^3) O(n3)。
对此我有一步简单的优化,我们不再通过两端来枚举子串而是通过中心位置与半径长度来枚举,这样我们可以节省一些无意义的运算。例如
我们在以4为中心判断
不是回文后我们就不用进一步扩充半径判断了。因为他们也一定不再是回文。
通过这一步优化我们可以将时间复杂度下降至
O
(
n
2
)
O(n^2)
O(n2)
而Manacher算法可以将时间复杂度进一步下降至
O
(
n
)
O(n)
O(n)。
算法其实仅添加了一条回文的性质,进行了优化。即回文的对称性,例如
1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|
a | b | a | d | a | b | a |
我们此前求得以位置2上的b为中心的回文子串的最长半径是2,我们有求得以d为中心的回文子串的最长半径是4,在我们求位置6时发现位置6在d的半径范围内,也就是满足以d为中心的回文串的对称性,那么我们就可以确定以位置6的b的半径至少是以位置2的b的半径。(当然可能更长)这样我们就避免了一些运算。
你可能会觉得这种情况太少见的,只出现在了有很多很长的回文子串的情况下,但换一种情况假设回文子串很少很短,那么上一种算法的时间复杂度本身就会接近于 O ( n ) O(n) O(n)。换句话说,利用这个性质我们解决了上一个算法难以处理的情况。
我们会发现对于回文半径最右边界之前的点我们都可以用 O ( 1 ) O(1) O(1)的时间复杂度得出半径,而对于计算边界外的点也一定会扩展边界,这样我们不是直接得出答案就是在扩展边界,边界从左到右,算法只用进行n次扩充,所以时间复杂度是 O ( n ) O(n) O(n)
另一些改进
(1)奇偶优化我们通过上面的论述得到了快速求解一个奇数长度回文子串的方法,对于偶数回文子串呢?
我们可以在字符串中间加入相同的无意义字符,来转换问题,例如
这样对于以两个3中心得偶回文子串的长度数值上等于以两个3之间的#为中心得奇回文子串的半径减一。
(2)边界优化我们在拓展半径时都需要进行一次判断,判断半径拓展后是否超出字符串的长度,即取到的字符串之外的字符,这是一次运算,我们可以将其去除。
我们可以人为的在#变换之后字符串的最前面加上一个原字符串不会出现的字符。例如
因为@不与原字符串的任何一个字符相同所以,在判断@时一定会停下不在拓展边界。
理论上在字符串的最后也应该加入一个字符,但我提前开辟的数组足够大,使得字符串之后都是空值,空值‘\0’本身就是一种原字符不可能出现的字符。
P3805 【模板】manacher算法
#include<iostream>
#include<string>
#include<algorithm>
using namespace std;
const int MAXN = 1.1e7 + 10;
string s;
char c[MAXN<<1];
int Len[MAXN<<1];
int Manacher() {
int ans = 0;
int r = 0, po = 0, i = 1;
for (i = 1; c[i]; ++i) {
if (r > i)
Len[i] = min(r - i, Len[2 * po - i]);
else
Len[i] = 1;
while (c[i - Len[i]] == c[i + Len[i]])
Len[i]++;
if (po + Len[i] > r) {
r = po + Len[i];
po = i;
}
ans = max(ans, Len[i]);
}
return ans - 1;
}
int main() {
cin >> s;
c[0] = '$';
c[1] = '#';
int num = 1;
for (char i : s) {
c[++num] = i;
c[++num] = '#';
}
cout << Manacher() << endl;
system("pause");
return 0;
}
最长双回文子串
P4555 [国家集训队]最长双回文串
双回文子串是两个回文子串拼接,且不能有重复的子串。
朴素方法:我们先使用Manacher算法打出表,在枚举断点,在断点左右搜索最长的回文子串,当然这样的时间复杂度是
O
(
n
2
)
O(n^2)
O(n2)。
动态规划优化:我们建立两个数组,用来储存以i为开头的最长回文子串的长度,与以i结尾的最长回文子串的长度。那么所有#中两个数组之和最大的就是解。
我们怎么高效的维护这两个数组呢?我们首先在每次计算左右边界时,都更新一下这两个数组,在计算过以后再使用
O
(
n
)
O(n)
O(n)的时间复杂度来递推出所有‘#’为断点的两个数组
计算出所有边界值在递推相比每次求出边界值后都递推边界相比节约了一些计算(懒惰算法)。
#include<iostream>
#include<string>
#include<algorithm>
using namespace std;
const int MAXN = 1e5 + 10;
string s;
char c[MAXN << 1];
int Len[MAXN << 1];
int ll[MAXN << 1], rr[MAXN << 1];
int Manacher() {
int ans = 0;
int r = 0, po = 0, i = 1;
for (i = 1; c[i]; ++i) {
if (r > i)
Len[i] = min(r - i, Len[2 * po - i]);
else
Len[i] = 1;
while (c[i - Len[i]] == c[i + Len[i]])
Len[i]++;
if (po + Len[i] > r) {
r = po + Len[i];
po = i;
}
ll[i + Len[i] - 1] = max(ll[i + Len[i] - 1], Len[i] - 1);
rr[i - Len[i] + 1] = max(rr[i - Len[i] + 1], Len[i] - 1);
}
for (i = 1; c[i]; i += 2) rr[i] = max(rr[i], rr[i - 2] - 2);
for (; i >= 1; i -= 2)ll[i] = max(ll[i], ll[i + 2] - 2);
for (int i = 1; c[i]; i += 2)if(rr[i]&&ll[i])ans = max(ans, ll[i] + rr[i]);
return ans;
}
int main() {
cin >> s;
c[0] = '$';
c[1] = '#';
int num = 1;
for (int i = 0; i < s.size();++i) {
c[++num] = s[i];
c[++num] = '#';
}
cout << Manacher() << endl;
system("pause");
return 0;
}