M a n a c h e r A l g o r i t h m \mathrm{Manacher\ Algorithm} Manacher Algorithm
Manacher 算法主要是解决怎样的问题呢,其实是求解最长的回文串,但是只能找到长度为奇数的回文串,不过可以通过转化使得能够求解任意长度的回文串。
例题:P3805 【模板】manacher
A l g o r i t h m F l o w \mathrm{Algorithm\ Flow} Algorithm Flow
Manacher 的算法是在 O ( n ) O(n) O(n) 的时间复杂度内,查找对于每一个 i i i,最大的回文半径 p i p_i pi 是多少?
从左往右递推做,在递推的过程中,每一次维护右端点最靠右的回文串的中心位置 m i d mid mid,以及该回文串最靠右的位置 m r mr mr(通常不包含该回文串最靠右的位置,即为该位置 + 1 +1 +1)。(如图)
那么,对于求解 i i i 的 p i p_i pi,可以分情况讨论:
- i i i 在 m r mr mr 的左边,即在 m i d mid mid 所在回文串的内部。那么,充分利用已经算过的信息,考虑 i i i 在该回文串内的对应点 j j j,其中 j = m i d × 2 − i j=mid\times 2 - i j=mid×2−i。
- 若 j j j 所在的最大回文串仍然在 m i d mid mid 所在回文串的范围内,则 p i ≥ p j p_i\ge p_j pi≥pj。(何时取 ≥ \ge ≥,当且仅当 j j j 的左端点恰好为 m i d mid mid 所在回文串的左端点时)
- 若 j j j 所在的最大回文串超出了 m i d mid mid 所在回文串的范围,则 p i = m r − i p_i=mr-i pi=mr−i
综上所述, p i ≥ min ( p j , m r − i ) p_i\ge \min(p_j,mr-i) pi≥min(pj,mr−i)
- i i i 在 m r mr mr 的右边,即在 m i d mid mid 所在回文串的外部。那么,此时 p i ≥ 1 p_i\ge 1 pi≥1
最后,暴力的向外扩展即可:
while (S[i - p[i]] == S[i + p[i]]) p[i] ++;
判断当前的 p i + i p_i+i pi+i 是否超出 m r mr mr,若超出,则更新 m r mr mr 和 m i d mid mid 即可。
A n a l y s i s o f t h e T i m e C o m p l e x i t y \mathrm{Analysis\ of\ the\ Time\ Complexity} Analysis of the Time Complexity
其实,时间复杂度就是 while
循环的执行次数(即暴力扩展的次数)。
只要执行 while
那么一定会更新
m
r
mr
mr,且 while
执行的次数与
m
r
mr
mr 向右扩展的数量相同。
所以 m r mr mr 总是线性向右递推的,所以总的时间复杂度为 O ( n ) O(n) O(n)
T r a n s f o r m \mathrm{Transform} Transform
刚才的算法只是解决一个字符串中奇数长度的回文串,偶数的是找不出来的。
不过,通过加一些字符可以转化为能够求出任意长度的最长回文串,具体如下:
在任意
2
2
2 个字符之间添加
1
1
1 个 #
,首尾也要加入 #
,再在首尾分别加入 $
,^
以防出界,因为你会发现 while
循环是没有边界的。
具体形式: $ # a 1 # a 2 # a 3 # … a n # ∧ \$ \# a_1\# a_2\# a_3\#\dots a_n\#\wedge $#a1#a2#a3#…an#∧
这样对于新串的最长回文半径 − 1 -1 −1 即为旧串最长回文串长度。
A c c e p t e d C o d e \mathrm{Accepted\ Code} Accepted Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
typedef pair<int, int> PII;
typedef long long LL;
const int SIZE = 4e7 + 10;
int N;
string A, S;
int P[SIZE];
void Manacher()
{
int mid, mr = 1;
for (int i = 1; i <= N; i ++)
{
if (i < mr) P[i] = min(P[mid * 2 - i], mr - i);
else P[i] = 1;
while (S[i - P[i]] == S[i + P[i]]) P[i] ++;
if (i + P[i] > mr)
{
mr = i + P[i];
mid = i;
}
}
}
signed main()
{
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(0);
cin >> A;
N = A.size(), A = ' ' + A;
S += "$#";
for (int i = 1; i <= N; i ++)
S += A[i], S += '#';
S += '^';
N = S.size(), S = ' ' + S;
Manacher();
int Result = 0;
for (int i = 1; i <= N; i ++)
Result = max(Result, P[i] - 1);
cout << Result << endl;
return 0;
}
练习题:P5446 【THUPC2018】绿绿和串串
D e s c r i p t i o n \mathrm{Description} Description
定义翻转的操作:把一个串以最后一个字符作对称轴进行翻转复制。形式化地描述就是,如果他翻转的串为 R R R,那么他会将前 R − 1 R-1 R−1 个字符倒序排列后,插入到串的最后
现给出字符串 S S S,询问有哪些串长度不超过 ∣ S ∣ |S| ∣S∣ 的串 R R R 经过若干次翻转操作后得到的串作为 T T T 的前缀。
数据范围: ∑ ∣ S ∣ ≤ 5 × 1 0 6 \sum|S|\le 5\times 10^6 ∑∣S∣≤5×106
S o l u t i o n \mathrm{Solution} Solution
考虑怎样的串翻转若干次之后前缀包含 S S S,其一定是 S S S 的一个前缀,这样翻转后 S S S 才会是它的前缀。
那么,对于 S S S 中每一个位置 i i i 分情况讨论:
- 若 1 ∼ i 1\sim i 1∼i 的串翻转 1 1 1 次之后长度超过 ∣ S ∣ |S| ∣S∣,则要求 i + 1 ∼ ∣ S ∣ i+1\sim |S| i+1∼∣S∣ 的串是 1 ∼ i − 1 1\sim i-1 1∼i−1 的串取反后的前缀,即以 i i i 为中心的回文串最大右端点为 ∣ S ∣ |S| ∣S∣。标记 i i i 位置为可行位置。
- 若 1 ∼ i 1\sim i 1∼i 的串翻转 1 1 1 次之后长度小于 ∣ S ∣ |S| ∣S∣,则要求 1 ∼ 2 i − 1 1\sim 2i-1 1∼2i−1 是回文串,且右端点为可行位置,也就是能够经过多次翻转使 S S S 为其的前缀。
A c c e p t e d C o d e \mathrm{Accepted\ Code} Accepted Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
typedef pair<int, int> PII;
typedef long long LL;
const int SIZE = 2e6 + 10;
int N;
string S;
int P[SIZE], Vis[SIZE];
void Manacher()
{
int mr = 1, mid;
for (int i = 1; i <= N; i ++)
{
if (i < mr) P[i] = min(P[mid * 2 - i], mr - i);
else P[i] = 1;
while (S[i - P[i]] == S[i + P[i]]) P[i] ++;
if (P[i] + i > mr)
mr = P[i] + i, mid = i;
}
}
void solve()
{
cin >> S;
N = S.size(), S = ' ' + S, S += '^';
Manacher();
for (int i = N; i >= 1; i --)
if (i + P[i] - 1 >= N) Vis[i] = 1;
else if (Vis[i + P[i] - 1] && i - P[i] + 1 == 1) Vis[i] = 1;
for (int i = 1; i <= N; i ++)
if (Vis[i])
cout << i << " ", Vis[i] = 0;
cout << endl;
}
signed main()
{
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(0);
int Data;
cin >> Data;
while (Data --)
solve();
return 0;
}