回文自动机(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)\) 的算法:

\[dp_{i}=\sum dp_j[s[j+1:i]\text{ is palindromic}] \]

只需要在 \(\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)\)

\(\color{blue}{\text{code}}\)

#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)\)

\(\color{blue}{\text{code}}\)

#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)\)

\(\color{blue}{\text{code}}\)

#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;
}
posted @ 2022-06-13 19:34  Samsara-soul  阅读(92)  评论(0编辑  收藏  举报