回文自动机(Palindrome Automaton)小结
\(\text{CF932G. Palindrome Partition}\)
给定一个字符串 \(s\),求有多少种将 \(s\) 划分成偶数个字符串 \((t_1,t_2,\ldots,t_k)\ (2\mid k)\) 的方法,满足 \(\forall i\in [1,k],t_i=t_{k-i+1}\),答案对 \(10^9+7\) 取模。
\(2\le |s|\le 10^6\)。
记 \(n=|s|\),显然 \(n\) 为奇数无解。
考虑构造 \(s'=s_1s_{n}s_2s_{n-1}\ldots s_{\frac{n}{2}}s_{\frac{n}{2}+1}\)。那么原来的划分方案与对 \(s'\) 进行偶数长度的回文串划分一一对应。
现在问题变成了求一个字符串偶数长度的回文串划分的方案数。
显然有一个 \(O(n^2)\) 的算法:
只需要在 \(\text{Fail}\) 链上跳求出所有的 \(j\) 即可,\(j=i-len(u)\),\(u\) 是 \(\text{Fail}\) 链上的节点。
有一个结论:
若 \(s\) 是回文串,则 \(s\) 的回文后缀长度可以被划分为 \(O(\log |s|)\) 个等差数列。
\(\\\)
具体证明见 \(\text{OI-wiki}\)。
怎么使用这个结论呢?考虑从节点 \(u\) 开始在后缀树上跳时会依次经过 \(O(\log n)\) 个等差数列。记录每个等差数列的公差(节点与 \(\text{fail}\) 节点的长度差),\(\text{diff(}x\text{)=len(}x\text{)}-\text{len(fail(}x\text{))}\),转折点 \(\text{slink}(x)\)(\(\text{Fail}\) 链上满足 \(\text{diff}(x)\neq \text{diff}(y)\) 的第一个 \(y\))。
根据结论,跳 \(\text{slink}\) 到根节点的次数为 \(O(\log n)\) 次,只需在这 \(O(\log n)\) 次内更新值就可以。考虑到 \(f\) 的值为若干等差数列上的节点的和,不妨记录每条链的 \(\text{dp}\) 值之和,记 \(g_{x}\) 表示 \(x\rightarrow \text{slink}(x)\) 路径上不包括 \(\text{slink}(x)\) 的节点的 \(\text{dp}\) 值和。
考虑新增加一个字符对回文树的影响,相当于 \(\text{fail}\) 节点平移 \(\text{diff}(x)\) 个单位,那么加上新增加的 \(f_{i-\text{len(slink}(x))-\text{diff}(x)}\),在 \(2\mid i\) 的时候更新即可。
总时间复杂度 \(O(n\log n)\)。
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+5,mod=1e9+7;
int n,f[N],g[N];char s[N],S[N];
struct Node{int son[26],fail,len,diff,slink;};
struct Palindrome_Automaton{
Node node[N];int cnt=1,last=0;
Palindrome_Automaton(){node[0].fail=node[1].fail=1,node[1].len=-1;}
inline int getfail(int x,int i){
while(i-node[x].len-1<0||s[i-node[x].len-1]!=s[i])x=node[x].fail;
return x;
}
inline void insert(char c,int i){
int x=getfail(last,i),ch=c-'a';
if(!node[x].son[ch]){
node[++cnt].len=node[x].len+2;
int u=getfail(node[x].fail,i);
node[cnt].fail=node[u].son[ch];
node[cnt].diff=node[cnt].len-node[node[cnt].fail].len;
node[cnt].slink=node[cnt].diff==node[node[cnt].fail].diff?node[node[cnt].fail].slink:node[cnt].fail;
node[x].son[ch]=cnt;
}
last=node[x].son[ch];
}
}PAM;
int main(){
scanf("%s",S+1),n=strlen(S+1);
if(n&1)return puts("-1"),0;
for(int i=1;i<=n/2;++i)s[i*2-1]=S[i];
for(int i=1;i<=n/2;++i)s[i*2]=S[n-i+1];
f[0]=1;
for(int i=1;i<=n;++i){
PAM.insert(s[i],i);
for(int j=PAM.last;j>1;j=PAM.node[j].slink){
g[j]=f[i-PAM.node[PAM.node[j].slink].len-PAM.node[j].diff];
if(PAM.node[j].fail!=PAM.node[j].slink)
(g[j]+=g[PAM.node[j].fail])%=mod;
if(!(i&1))(f[i]+=g[j])%=mod;
}
}
return printf("%d\n",f[n]),0;
}
\(\text{CF 906E. Reverses}\)
给定两个长度为 \(n\) 的字符串 \(s\) 和 \(t\)。你可以翻转 \(t\) 的若干个不相交的区间,要求最终 \(s\) 和 \(t\) 相同。问你最少翻转几个区间,并输出方案。
\(1\le n\le 5\cdot 10^5\)。
构造 \(s'=s_1t_1s_2t_2\ldots s_nt_n\),那么翻转的区间对应到 \(s'\) 中是一个偶数长度的回文串,所以求出最小偶数长度的回文划分即可。
套路和上面一题一样,记录一下一条链中最小 \(\text{dp}\) 值的位置即可。注意长度为 \(2\) 的回文串在原来的字符串中表示 \(s_k=t_k\),花费为 \(0\)。
总时间复杂度 \(O(n\log n)\)。
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5,mod=1e9+7;
int n,inf,f[N],dp[N],pre[N];char s[N],S[N],T[N];
struct Node{int son[26],fail,len,diff,slink;};
struct Palindrome_Automaton{
Node node[N];int cnt=1,last=0;
Palindrome_Automaton(){node[0].fail=node[1].fail=1,node[1].len=-1;}
inline int getfail(int x,int i){
while(i-node[x].len-1<0||s[i-node[x].len-1]!=s[i])x=node[x].fail;
return x;
}
inline void insert(char c,int i){
int x=getfail(last,i),ch=c-'a';
if(!node[x].son[ch]){
node[++cnt].len=node[x].len+2;
int u=getfail(node[x].fail,i);
node[cnt].fail=node[u].son[ch];
node[cnt].diff=node[cnt].len-node[node[cnt].fail].len;
node[cnt].slink=node[cnt].diff==node[node[cnt].fail].diff?node[node[cnt].fail].slink:node[cnt].fail;
node[x].son[ch]=cnt;
}
last=node[x].son[ch];
}
}PAM;
int main(){
memset(dp,63,sizeof dp),inf=dp[0],dp[0]=0;
scanf("%s%s",S+1,T+1),n=strlen(S+1);
for(int i=1;i<=n;++i)s[i*2-1]=S[i],s[i*2]=T[i];n*=2;
for(int i=1;i<=n;++i){
PAM.insert(s[i],i);
if(!(i&1)&&s[i]==s[i-1]&&dp[i-2]<dp[i])dp[i]=dp[i-2],pre[i]=i-2;
for(int j=PAM.last;j>1;j=PAM.node[j].slink){
f[j]=i-PAM.node[PAM.node[j].slink].len-PAM.node[j].diff;
if(PAM.node[j].fail!=PAM.node[j].slink&&dp[f[PAM.node[j].fail]]<dp[f[j]])
f[j]=f[PAM.node[j].fail];
if(!(i&1)&&dp[f[j]]+1<dp[i])dp[i]=dp[f[j]]+1,pre[i]=f[j];
}
}
if(dp[n]==inf)return puts("-1"),0;
printf("%d\n",dp[n]);
for(int i=n;i>1;i=pre[i])if(i!=pre[i]+2)printf("%d %d\n",pre[i]/2+1,i/2);
return 0;
}
\(\text{CF835D Palindromic characteristics}\) 加强版
给定一个长度为 \(n\) 的字符串,对于 \(k\in [1,n]\),求出 \(k\) 阶回文子串有多少个。
\(1\) 阶子串的定义是:子串是回文串。
\(k\ (k>1)\) 阶子串的定义是:子串本身是回文串,而且它的左半部分和右半部分是 \(k-1\) 阶回文串。显然 \(k\) 阶子串也是 \(k-1\) 阶子串。
\(1\le n\le 10^6\)。
建 \(\text{PAM}\) 时求出每个回文串的长度不超过一半的回文后缀,然后根据该回文后缀求出阶数即可。
总时间复杂度 \(O(n)\)。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e6+5;
int n,inf,f[N],cn[N];ll ans[N];char s[N];
struct Node{int son[26],fail,len,fa;};
struct Palindrome_Automaton{
Node node[N];int cnt=1,last=0;
Palindrome_Automaton(){node[0].fail=node[1].fail=1,node[1].len=-1;}
inline int getfail(int x,int i){
while(i-node[x].len-1<0||s[i-node[x].len-1]!=s[i])x=node[x].fail;
return x;
}
inline void insert(char c,int i){
int x=getfail(last,i),ch=c-'a';
if(!node[x].son[ch]){
node[++cnt].len=node[x].len+2;
int u=getfail(node[x].fail,i);
node[cnt].fail=node[u].son[ch];
if(node[cnt].len<=2)node[cnt].fa=node[cnt].fail;
else{
int p=node[x].fa;
while(i-node[p].len-1<0||s[i-node[p].len-1]!=s[i]||(node[p].len+2)*2>node[cnt].len)p=node[p].fail;
node[cnt].fa=node[p].son[ch];
}
node[x].son[ch]=cnt;
}
last=node[x].son[ch],++cn[last];
}
}PAM;
int main(){
scanf("%s",s+1),n=strlen(s+1);
for(int i=1;i<=n;++i)PAM.insert(s[i],i);
for(int i=PAM.cnt;i>1;--i)cn[PAM.node[i].fail]+=cn[i];
for(int i=2;i<=PAM.cnt;++i){
if(PAM.node[i].len/2==PAM.node[PAM.node[i].fa].len)
f[i]=f[PAM.node[i].fa]+1;
else f[i]=1;
ans[f[i]]+=cn[i];
}
for(int i=n;i;--i)ans[i]+=ans[i+1];
for(int i=1;i<=n;++i)printf("%lld\n",ans[i]);
return 0;
}
\(\text{CERC2014 Virus synthesis}\)
初始有一个空串,利用下面的操作构造给定串 \(S\):
\(1.\) 串开头或末尾加一个字符;
\(2.\) 串开头或末尾加一个该串的逆串。
求最小的操作数。
\(1\le |S| \leq 10^5\),字符集为 \(\{A,T,C,G\}\)。
操作时一定时最大化第 \(2\) 种操作的次数,最后的操作一定类似于第 \(2\) 个操作后加上若干第 \(1\) 个操作。
考虑对于一个回文串 \(s\),一定是由 \(s\) 的一个回文子串 \(t\) 经过一系列第 \(1\) 个操作再加一个第 \(2\) 个操作达成。
首先对于 \(\text{PAM}\) 上的一条边 \(u\rightarrow v\),显然可以用 \(dp_{u}+1\) 更新 \(dp_{v}\),也就是在最后一次翻转前加上 \(u\rightarrow v\) 上的字符 \(c\)。那么只需考虑不超过长度一半的最长回文后缀对答案的影响。
设当前节点为 \(u\),\(p\) 是 \(u\) 的不超过长度一半的最长回文后缀的节点,那么用 \(dp_{p}+1+\frac{\text{len}(u)}{2}-\text{len}(p)\) 更新 \(dp_{u}\) 即可。
注意到 \(p\) 的深度一定小于 \(u\),所以在 \(\text{bfs}\) 时求出每个节点的 \(\text{dp}\) 值即可。
总时间复杂度 \(O(|S|)\)。
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int T,n,cnt,ans,last,dp[N];char s[N];
struct Node{int son[4],fail,len,fa;}node[N];
inline int calc(char c){
if(c=='A')return 0;
if(c=='C')return 1;
if(c=='T')return 2;
return 3;
}
inline void init(){
for(int i=0;i<=cnt;++i){
for(int j=0;j<4;++j)node[i].son[j]=0;
node[i].fail=node[i].len=node[i].fa=0;
}
cnt=1,last=0,node[0].fail=node[1].fail=1,node[1].len=-1,ans=n;
}
inline int getfail(int x,int i){
while(i-node[x].len-1<0||s[i-node[x].len-1]!=s[i])x=node[x].fail;
return x;
}
inline void insert(char c,int i){
int x=getfail(last,i),ch=calc(c);
if(!node[x].son[ch]){
node[++cnt].len=node[x].len+2;
int u=getfail(node[x].fail,i);
node[cnt].fail=node[u].son[ch];
if(node[cnt].len<=2)node[cnt].fa=node[cnt].fail;
else{
int p=node[x].fa;
while(i-node[p].len-1<0||s[i-node[p].len-1]!=s[i]||(node[p].len+2)*2>node[cnt].len)p=node[p].fail;
node[cnt].fa=node[p].son[ch];
}
node[x].son[ch]=cnt;
}
last=node[x].son[ch];
}
queue<int>q;
inline void bfs(){
for(int i=2;i<=cnt;++i)dp[i]=node[i].len;
for(int i=0;i<4;++i)if(node[0].son[i])q.push(node[0].son[i]);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=0;i<4;++i)if(node[u].son[i]){
int v=node[u].son[i],F=node[v].fa;
dp[v]=min(dp[u]+1,dp[F]+1+node[v].len/2-node[F].len);
ans=min(ans,dp[v]-node[v].len+n);
q.push(v);
}
}
}
int main(){
for(scanf("%d",&T);T--;){
scanf("%s",s+1),n=strlen(s+1);
init();
for(int i=1;i<=n;++i)insert(s[i],i);
bfs();
printf("%d\n",ans);
}
return 0;
}