Manacher算法
给定一个字符串,要求求出字符串中所有的回文字串。Manacher算法给出了一个线性时间算法。
Manacher算法分为三个步骤:
- 寻找回文半径下限
- 扩展半径,记录回文子串信息
- 更新区间 [ l , r ] [l,r] [l,r]
寻找回文半径下限
回文串分为奇回文串和偶回文串,而奇回文串操作比较优雅。故先介绍奇回文串的Manacher算法。
源字符串为 t [ i ] t[i] t[i],定义数组 p [ i ] p[i] p[i],代表以字符 t [ i ] t[i] t[i]为中心,回文子串的最大半径,例如:
Manacher算法其实是一种动态规划算法,核心在于如何利用已经计算出的 p p p值去推导没有计算出的 p p p值。
接下来,我们要维护一段区间,变量 l , r l,r l,r记录了区间的左右边界。其区间的含义为,当前计算出的所有回文子串 [ l , r ] [l,r] [l,r]中最大的 r r r值所代表的区间。
当我们计算新的 p p p值的时候,我们有两种情况:
- 当前下标 i i i在区间 [ l , r ] [l,r] [l,r]中。
- 当前下标 i i i不在区间 [ l , r ] [l,r] [l,r]中。
当前下标 i i i在区间 [ l , r ] [l,r] [l,r]中
如果当前下标 i i i在区间 [ l , r ] [l,r] [l,r]中,那么我们可以利用回文子串的对称性,去利用 p [ r − i + l ] p[r-i+l] p[r−i+l]的值,也就是 i i i的对称位置的值。
p [ r − i + l ] p[r-i+l] p[r−i+l]的值也有两种情况,第一,半径没有超过区间:
由图可知,此时 p [ i ] = p [ r − i + l ] p[i]=p[r-i+l] p[i]=p[r−i+l]。
第二种情况,对称位置的回文半径超过了区间:
由对称性可知, p [ i ] p[i] p[i]的值此时不等于 p [ r − i + l ] p[r-i+l] p[r−i+l]。 p [ i ] p[i] p[i]的值应该是 r − i + 1 r - i + 1 r−i+1。
第三种情况,对称位置的回文半径正好等于区间左端点:
总结一下,当 i ≤ r i \leq r i≤r的时候, p [ i ] p[i] p[i]的一个下界为 p [ i ] = min ( r − i + 1 , p [ r − i + l ] ) p[i]=\min(r-i+1,p[r-i+l]) p[i]=min(r−i+1,p[r−i+l])。
当前下标不在 i i i在区间 [ l , r ] [l,r] [l,r]中
如果当前下标不在 i i i在区间 [ l , r ] [l,r] [l,r]中,对称性是未知的,此时 p [ i ] p[i] p[i]的下限为 1 1 1,我们只能使用朴素回文算法,慢慢扩展回文半径。
扩展半径,记录回文子串信息
此时我们找到回文半径的下限之后,我们就需要扩展半径,扩展半径按照朴素回文算法去扩展半径,直到不能再扩展为止,此时的 p [ i ] p[i] p[i]的值就是正确的。
算出 p [ i ] p[i] p[i]之后,我们就可以记录回文子串的信息了。
更新区间 [ l , r ] [l,r] [l,r]
别忘了,如果当前 p [ i ] + i − 1 > r p[i] + i - 1 \gt r p[i]+i−1>r,那么就需要更新区间 l = p [ i ] − i + 1 , r = p [ i ] + i − 1 l = p[i] - i + 1,r = p[i] + i - 1 l=p[i]−i+1,r=p[i]+i−1。保证循环不变式的正确。
偶数情况
为了处理偶数回文的情况,我们有一下两种做法:
插入无关字符
在文本串的开头和末尾,以及每个字符的间隙中插入相同的无关字符,可以让偶回文变成奇回文。此时在按照上述过程运行Manacher算法即可,例如:
T = a b b a , T ′ = # a # b # b # a # T=abba,T'=\#a\#b\#b\#a\# T=abba,T′=#a#b#b#a#
此时去处理 T ′ T' T′即可。
硬处理偶回文
我们模仿奇回文的Manacher算法,写出偶回文的Manacher算法。
为了计算奇回文半径数组 d 1 [ i ] d_{1}[i] d1[i],我们有:
vector<int> d1(n);
for (int i = 0, l = 0, r = -1; i < n; i++) {
int k = (i > r) ? 1 : min(d1[l + r - i], r - i + 1);
while (0 <= i - k && i + k < n && s[i - k] == s[i + k]) {
k++;
}
d1[i] = k;
if (i + k > r) {
l = i - k + 1;
r = i + k - 1;
}
}
注意,如果使用此代码去处理插入无关字符的方法,那么原来的回文长度为 d 1 [ i ] − 1 d_{1}[i] - 1 d1[i]−1。
为了计算偶回文半径数组 d 2 [ i ] d_{2}[i] d2[i],我们改写一下代码即可:
注意,在这种情况下,每个回文子串都存在最小回文中心 p [ i ] p[i] p[i]和 p [ i − 1 ] p[i-1] p[i−1]。
vector<int> d2(n);
for (int i = 0, l = 0, r = -1; i < n; i++) {
int k = (i > r) ? 0 : min(d2[l + r - i], r - i + 1);
while (0 <= i - k - 1 && i + k < n && s[i - k - 1] == s[i + k]) {
k++;
}
d2[i] = k;
if (i + k > r) {
l = i - k;
r = i + k - 1;
}
}
模板
#include <bits/stdc++.h>
using namespace std;
#define FR freopen("in.txt", "r", stdin)
#define FW freopen("out.txt", "w", stdout)
typedef long long ll;
int dp[22000005];
int main()
{
string s;
cin >> s;
string text;
text.push_back('#');
for (int i = 0; i < s.size(); i++)
{
text.push_back(s[i]);
text.push_back('#');
}
int l = 0;
int r = -1;
int ans = 1;
for (int i = 0; i < text.size(); i++)
{
int f = 1;
if (i <= r)
f = min(dp[r - i + l], r - i + 1);
while (i + f < text.size() && i - f >= 0 && text[i + f] == text[i - f])
f++;
if (i + f - 1 > r)
{
l = i - f + 1;
r = i + f - 1;
}
dp[i] = f;
ans = max(ans, f - 1);
}
cout << ans;
return 0;
}
##例题
不重叠,两个回文,枚举分割点:
class Solution
{
public:
long long maxProduct(string s)
{
vector<int> pr(s.size());
vector<int> psum(s.size());
vector<int> ssum(s.size());
int l = -1, r = -1;
for (int i = 0; i < s.size(); i++)
{
int k = i <= r ? min(r - i + 1, pr[l + r - i]) : 1;
while (i + k < s.size() && i - k >= 0 && s[i + k] == s[i - k])
k++;
pr[i] = k;
if (i + k - 1 > r)
{
r = i + k - 1;
l = i - k + 1;
}
}
int c = 0;
for (int i = 0; i < s.size(); i++)
{
while (c + pr[c] - 1 < i)
c++;
psum[i] = max(2 * (i - c) + 1, i == 0 ? 0 : psum[i - 1]);
}
c = s.size() - 1;
for (int i = s.size() - 1; i >= 0; i--)
{
while (c - pr[c] + 1 > i)
c--;
ssum[i] = max(2 * (c - i) + 1, i == s.size() - 1 ? 0 : ssum[i + 1]);
}
long long ans = 0;
for (int i = 0; i < s.size() - 1; i++)
{
ans = max(ans, ((long long)psum[i]) * ssum[i + 1]);
}
return ans;
}
};
#include <bits/stdc++.h>
#define FR freopen("in.txt", "r", stdin)
#define FW freopen("out.txt", "w", stdout)
using namespace std;
typedef long long ll;
int pr[500005];
int dp[500005];
int main()
{
string prestr;
cin >> prestr;
string str;
for (int i = 0; i < prestr.size(); i++)
{
str.push_back('#');
str.push_back(prestr[i]);
}
str.push_back('#');
// manarcher
int l = 0, r = -1;
for (int i = 0; i < str.size(); i++)
{
int k = i > r ? 1 : min(r - i + 1, pr[l + r - i]);
while (i + k < str.size() && i - k >= 0 && str[i + k] == str[i - k])
k++;
pr[i] = k;
if (i + k - 1 > r)
{
r = i + k - 1;
l = i - k + 1;
}
}
int c = 0;
for (int i = 0; i < str.size(); i++)
{
while (i > c + pr[c] - 1)
{
c++;
}
dp[i] = (i - c) * 2 + 1;
}
int ans = 0;
for (int i = 0; i < str.size() - 1; i++)
{
// ans = max(ans, (pr[i] - 1) * 2 + 1 + (i - pr[i] - 1 >= 0 ? dp[i - pr[i] - 1] : 0));
ans = max(ans, (pr[i] - 1) + (i - pr[i] >= 0 ? (dp[i - pr[i]] + 1) / 2 : INT_MIN));
}
cout << ans;
return 0;
}