Manacher 算法可以在 \(O(n)\) 时间内求得一个字符串的最长回文子串。
比如 baka 的最长回文子串为 aka。
板:P3805 【模板】Manacher
由于奇、偶数长度的回文串形态不同,为了避免分类讨论,我们在字符之间添加特殊字符如 #,这样就只需要考虑奇数长度了。
另外,我们需要在字符串开头添加另一个特殊字符如 $,原因见后面的代码。
令 \(d_i\) 为 \(i\) 为中心扩展出的最长回文半径。再记录 \(r\) 为目前最长回文子串的右端点,\(mid\) 为它的回文中心。
分两种情况讨论:
- 
\(mid<i<r\)。 令 \(j\) 为 \(i\) 关于 \(mid\) 的对称点,则有 \(S[j-d_j+1,j+d_j-1]=S[i-d_i+1,i+d_i-1]\)。 然而在 \(i+d_i-1>r\) 时,我们不能保证上式成立。所以应当让 \(d_i=\min(d_j,r-i+1)\),再暴力向右扩展。  
- 
\(i\ge r\)。 没办法利用以前的信息,暴力向右扩展即可。 
考虑分析这样的时间复杂度:
- \(min<i<r\) 时,设置 \(d_i\) 的初值是 \(O(1)\) 的,每次扩展会让 \(r\) 增加 \(1\)。
- \(i\ge r\) 时,每次扩展会让 \(r\) 增加 \(1\)。
而 \(r\) 的增加是 \(O(n)\) 的,所以总时间也是 \(O(n)\) 的。
const int N=1.1e7+10;
int n,d[N<<1];
string tmp,s;
void get_d(){d[1]=1;for(int i=2,l,r=1;i<=n;i++){if(i<=r) d[i]=min(d[r-i+l],r-i+1);while(s[i-d[i]]==s[i+d[i]]) d[i]++;//开头添加 $ 是为了防止这里越界if(i+d[i]-1>r) l=i-d[i]+1,r=i+d[i]-1;}
}
signed main(){cin>>tmp;s="$#";for(char i:tmp) s+=i,s+='#';n=s.size()-1;get_d();cout<<*max_element(d+1,d+1+n)-1;return 0;
}
例题
P5446 [THUPC 2018] 绿绿和串串
令 \(f_i\) 为 \(i\) 能否作为一个合法的前缀,\(d_i\) 为以 \(i\) 为中心的最长回文半径。
分讨一下:
- 
\(i+d_i-1=n\)。 这意味着一次翻转即可覆盖 \(S\),自然 \(i\) 可以作为合法的前缀,\(f_i=1\)。 
  
- 
\(i-d_i+1=1\) 这意味着需要多次旋转才可能覆盖 \(S\),此时 \(f_i=f_{i+d_i-1}\)。 
  
时间复杂度 \(O(n)\)。
实现细节上:
- 注意多测清空,比如 \(f[1]\) 可能被自己转移到,所以上一轮的 \(f[1]\) 必须提前清空。
- 本题不需要求偶数长度的回文串,所以不需要在字符间补充 #;但是需要在开头和结尾补充不同的特殊字符。开头加是防止越界,结尾加是防止上一轮的字符串影响。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int t,n,d[N];
bitset<N> f;
string s;
inline void get_d(){d[1]=1;for(int i=2,l=0,r=1;i<=n;i++){if(i<=r) d[i]=min(d[r-i+l],r-i+1);else d[i]=1;//多测清空 while(s[i-d[i]]==s[i+d[i]]) d[i]++;if(i+d[i]-1>r) l=i-d[i]+1,r=i+d[i]-1;}
}
signed main(){cin>>t;while(t--){cin>>s;n=s.size();s='#'+s+'$';get_d();f[1]=0;//多测清空 for(int i=n;i;i--){if(i+d[i]-1==n) f[i]=1;else f[i]=(f[i+d[i]-1]&&i==d[i]);}for(int i=1;i<=n;i++) if(f[i]) cout<<i<<" ";cout<<"\n";}return 0;
}
此题也有字符串哈希写法,原理类似。
\(\text{-\quad to be continued\quad -}\)