【学习笔记】字符串回文算法
Manacher
非线性的求回文算法
-
\(O(n^2)\):枚举每个位置作为中心,不断向两侧扩展。
-
\(O(n\log n)\):二分+哈希。
线性的求回文算法
维护一个回文串 \([l,r]\) 为当前右端点最靠右的回文串,设当前枚举位置 \(i\),\(i\) 与 \([l,r]\) 回文中心对称的位置 \(j=l+r-i\)。
由于 \(j\) 的最长回文半径已经处理过,因此当 \(i\le r\) 时,分别以 \(i\) 和 \(j\) 为中心的回文串中的部分(在 \([l,r]\) 中的部分)同样对称;剩余情况则从 \(1\) 开始。
按照暴力的方法扩展,更新 \([l,r]\),这里由于 \(r\) 只增不减,所以总扩展次数是 \(O(n)\) 的。
回文分奇数偶数两种,统一处理一般插入分隔符。
然而事实上求回文串的的线性算法不会优化算法瓶颈。(对数复杂度中二分与数据结构中的修改是并列的)
点击查看代码
int n;
char s[maxn<<1];
int d[maxn<<1];
int ans;
inline void Manacher(){
for(int i=1,l=0,r=-1;i<=2*n+1;++i){
int j=l+r-i,k;
if(i>r) k=1;
else k=min(d[j],r-i+1);
while(i-k>=1&&i+k<=2*n+1&&s[i-k]==s[i+k]) ++k;
d[i]=k;
ans=max(ans,(d[i]*2-1)/2);
if(i+k-1>r) l=i-k+1,r=i+k-1;
}
}
int main(){
scanf("%s",s+1);
n=strlen(s+1);
for(int i=n;i>=1;--i) s[i*2]=s[i];
for(int i=0;i<=n;++i) s[i*2+1]='#';
Manacher();
printf("%d\n",ans);
return 0;
}
回文自动机 Palindrome Automaton
构建
与回文有关且同样存在后缀指针的一种自动机。
由于回文串分奇数偶数,自动机存在两个根:奇根 \(1\) 和偶跟 \(0\),需要维护回文串长 \(\mathrm{len}(u)\) 和最长回文真后缀指针 \(\mathrm{fail}(u)\)。
构建的实际过程比较好理解,在上一个节点的基础上增量,一直跳 \(\mathrm{fail}\) 指针直到可以前后各扩展一个位置(为了方便计算奇根的长度设为 \(-1\)),如果已经有对应的转移,无需再修改;如果需要新建节点,就要再处理一下两个指针,按照同样方法就可以。设刚刚找到可以扩展的位置 \(u\),那么再寻找到的 \(\mathrm{fail}\) 指针位置关于 \(u\) 的最长回文串回文中心对称之后,显然是已经出现过的,也就是不需要再新建节点。
将上述过程扩展,也就是在一个字符串结尾增加一个字符,之后最长的回文后缀可能是新增加的,换言之,一个字符串的本质不同回文子串只有不超过 \(|s|\) 个。
点击查看代码
char s[maxn];
struct PalindromeAutomaton{
int tot,last;
int ch[maxn][26];
int len[maxn],fail[maxn];
PalindromeAutomaton(){
tot=1,last=1;
len[0]=0,fail[0]=1;
len[1]=-1,fail[1]=1;
}
int get_fail(int u,int pos){
while(s[pos-len[u]-1]!=s[pos]) u=fail[u];
return u;
}
void extend(int pos){
int c=s[pos]-'a';
int u=get_fail(last,pos);
if(!ch[u][c]){
int f=get_fail(fail[u],pos);
++tot;
len[tot]=len[u]+2,fail[tot]=ch[f][c];
ch[u][c]=tot;
}
last=ch[u][c];
}
}PAM;
参考资料
Manacher
回文自动机 Palindrome Automaton
- OI Wiki