Loading

回文自动机(PAM)

PAM

前言

改天把论文弄过来看看...

定义与构造

其实对于一个学过SAM的人来说,PAM就是换了个维护的东西,然后把相应的东西改改就完事了

首先,与SAM和AC自动机类似的,PAM每个点有 \(\text{len}\)\(\text{fail}\) 指针且意义不变,依然是代表的最长长度与失配指针

具体的,每个点的 \(\text{len}\) 代表以这个点结尾的最长回文子串的长度,跳一个点的 \(\text{fail}\) 指针代表跳到这个点所代表的串的除自己外的最长回文后缀所代表的点

其次,PAM本质上是回文树,并且回文树分两部分:奇与偶

从根节点到一个点的依次走过的路径所代表的字母连起来是一个回文串的一半

也就是说我们每走一条转移边,就相当于在一个串的首尾同时添加一个相同的字母

奇树上的点所代表的回文串的长度为奇数,根节点编号即为 \(1\),偶树类似,根节点编号为 \(0\)

奇树的根节点,也就是编号为 \(1\) 的点,\(\text{len}\) 需要初始化为 \(-1\)

个人感觉这样最大的好处是方便处理回文长度为 \(1\) 的情形或是其扩展情形


\(s_i\) 表示一个字符串第 \(i\) 位上所代表的字符,\(s_{i,j}\) 表示第 \(i\) 个字符到第 \(j\) 个字符所组成的子串,\(i.len\)\(i.fail\) 与上文定义相同,分别代表第 \(i\) 个点的最大长度与失配指针,\(las\) 表示最后加入的那个点

我们假设已经构造好了 \(s_{1,i-1}\) 的PAM,接下来我们要加入 \(s_i\)

根据定义很容易想到判断 \(s_{i-las.len+1}\) 的位置上的字符是否与 \(s_{i}\) 相同,如果相同,说明加入 \(s_i\) 依然构成回文串

否则不断跳 \(fail\) 指针,直到找到了一个满足条件的位置为止

然后我们新建结点,\(len\) 的值是其父结点 \(len\) 值加 \(2\)

接下来构造新结点的 \(fail\) 指针

不难想到只需要仿照上文的过程,再不断跳 \(fail\) 即可

最后更新 \(las\),一个字符就插入成功了,不断插入,即可得到PAM

P5496 【模板】回文自动机(PAM)

题目描述

给定一个串,求以每个位置结尾的回文子串个数,强制在线

题目分析

这种套路在SAM上已经出烂了

不难想到一个位置上的回文子串个数,就是其 \(fail\) 指针指向的点的答案加 \(1\)

对于一个点,其 \(fail\) 指针指向的点肯定先于其被加入PAM

于是我们加点的时候不断维护即可

#define R register
#define fx(l,n) inline l n
struct Trie{
	int fail,len,c[26],cnt;
}t[N];
char s[N];
int lans,node=1,las,nf,len;
fx(void,insert)(int p,int val){
	while(s[p-t[las].len-1]!=s[p]) las=t[las].fail;
	if(!t[las].c[val]){
		t[++node].len=t[las].len+2;
		nf=t[las].fail;
		while(s[p-t[nf].len-1]!=s[p]) nf=t[nf].fail;
		t[node].fail=t[nf].c[val];
		t[las].c[val]=node;
		t[node].cnt=t[t[node].fail].cnt+1;
	}
	las=t[las].c[val];
}
signed main(){
	scanf("%s",s+1);len=strlen(s+1);
	t[0].fail=1;t[1].len=-1;
	for(R int i=1;i<=len;i++){
		if(i>1) s[i]=(s[i]-97+lans)%26+97;
		insert(i,s[i]-'a');
		printf("%d ",lans=t[las].cnt);
	}
}

P4287 [SHOI2011]双倍回文

题目描述

一个字符串 \(s\) 的翻转为 \(s^R\),如果一个子串能写成 \(ss^Rss^R\) 的形式,那么它就是一个双倍回文子串

求最长的双倍回文子串的长度

题目分析

[SHOI2011]双倍经验

首先对于 \(ss^R\),它一定是一个偶回文串

于是我们要求的即是两个相同的偶回文串复合的子串的最长长度

这很简单,只要我们判定小于等于当前结点长度的一半的最长回文后缀的长度,是否为当前结点长度的一半即可

于是我们引入广为人知的 \(trans\) 指针,它维护的即是小于等于当前结点长度的一半的最长回文后缀

对于一个长度小于等于 \(2\) 的回文串,其 \(trans\) 指针与 \(fail\) 指针指向的是同一结点

否则我们不断跳 \(fail\),直到找到一个满足条件的最长回文后缀

求出了 \(trans\) 指针,这题也就做完了

#define R register
#define fx(l,n) inline l n
struct Trie{
	int fail,len,c[26],trs;
}t[N];
char s[N];
int ans,node=1,las,nf,len;
fx(void,insert)(int p,int val){
	while(s[p-t[las].len-1]!=s[p]) las=t[las].fail;
	if(!t[las].c[val]){
		t[++node].len=t[las].len+2;
		nf=t[las].fail;
		while(s[p-t[nf].len-1]!=s[p]) nf=t[nf].fail;
		t[node].fail=t[nf].c[val];
		t[las].c[val]=node;
		if(t[node].len<=2) t[node].trs=t[node].fail;
		else {
			nf=t[las].trs;
			while(t[nf].len+2>t[node].len>>1||s[p-t[nf].len-1]!=s[p]) nf=t[nf].fail;
			t[node].trs=t[nf].c[val];
		}
	}
	las=t[las].c[val];
}
signed main(){
	len=gi();scanf("%s",s+1);
	s[0]='$';t[0].fail=1;t[1].len=-1;
	for(R int i=1;i<=len;i++){
		insert(i,s[i]-'a');
		if((t[t[node].trs].len<<1==t[node].len)&&(t[t[node].trs].len%2==0)) ans=max(ans,t[node].len);
	}
	printf("%d",ans);
}

P4555 [国家集训队]最长双回文串

题目描述

找出一个最长子串,满足其能分割为两个回文串,输出其长度

题目分析

一眼枚举分割点,然后找出以这个点开始,向前与向后的最长回文串的长度,相加即可

向前很好处理,向后的话只需要反过来再建PAM即可

所有可能的长度取 \(\max\),本题就做完了

struct Trie{
	int fail,len,c[26],trs;
	Trie(){fail=0,len=0,trs=0,set(c,0,c,1);};
};
char s[N];
int ans,len,pres[N];
struct PAM{
	Trie t[N];
	int node,las,nf;
	fx(void,reset)(){set(t,0,t,1),node=1,t[0].fail=1,t[1].len=-1,las=0,nf=0;}
	fx(void,insert)(int p,int val){
		while(s[p-t[las].len-1]!=s[p]) las=t[las].fail;
		if(!t[las].c[val]){
			t[++node].len=t[las].len+2;
			nf=t[las].fail;
			while(s[p-t[nf].len-1]!=s[p]) nf=t[nf].fail;
			t[node].fail=t[nf].c[val];
			t[las].c[val]=node;
		}
		las=t[las].c[val];
	}
}pam;
signed main(){
	scanf("%s",s+1);len=strlen(s+1);
	pam.reset();
	for(R int i=1;i<=len;i++) pam.insert(i,s[i]-'a'),pres[i]=pam.t[pam.las].len;
	pam.reset();reverse(s+1,s+len+1);
	for(R int i=1;i<=len;i++){
		pam.insert(i,s[i]-'a');
		if(i<len) ans=max(ans,pres[len-i]+pam.t[pam.las].len);
	}
	printf("%d",ans);
}

P4762 [CERC2014]Virus synthesis

题目描述

原题题面已经很简洁了,这里就不再放一遍了

题目分析

不难想到一个字符串的形成肯定是先利用 \(2\) 操作生成一个最长的回文串,然后暴力 \(1\) 操作

于是我们的任务就变成了求生成最长的回文串所需要的操作数

很自然的,我们利用一个数组来存储生成这个点所代表的回文串的最少操作数

对于一个新建的点 \(i\),我们可以

  • 暴力生成,操作数 \(i.len\)
  • 在其 \(fail\) 指针指向的点所代表的的串生成前,加一个字符然后使用一次操作 \(2\),操作数 \(dp_{las}+1\)
  • 在其 \(trans\) 指针指向的点所代表的的串生成前,暴力加字符,然后使用一次操作 \(2\),操作数 \(dp_{i.trs}+(i.len/2-i.trs.len)+1\)

对上述操作取 \(\min\) 即可,不断加点,本题就做完了

本题时空有点小卡,需要压缩压缩

struct Trie{
	int fail,len,c[5],trs;
}t[N];
char s[N];
int ans,node=1,las,nf,len,dp[N],n;
fx(void,insert)(int p,int val){
	while(s[p-t[las].len-1]!=s[p]) las=t[las].fail;
	if(!t[las].c[val]){
		t[++node].len=t[las].len+2;
		nf=t[las].fail;
		while(s[p-t[nf].len-1]!=s[p]) nf=t[nf].fail;
		t[node].fail=t[nf].c[val];
		t[las].c[val]=node;
		if(t[node].len<=2) t[node].trs=t[node].fail;
		else {
			nf=t[las].trs;
			while(t[nf].len+2>t[node].len>>1||s[p-t[nf].len-1]!=s[p]) nf=t[nf].fail;
			t[node].trs=t[nf].c[val];
		}
		dp[node]=t[node].len;
		if(t[node].len%2==0){
			dp[node]=min({dp[node],dp[las]+1,dp[t[node].trs]+t[node].len/2-t[t[node].trs].len+1});
			ans=min(ans,len-t[node].len+dp[node]);
		}
	}
	las=t[las].c[val];
}
fx(int,mmap)(char a){
	if(a=='A') return 1;
	else if(a=='C') return 2;
	else if(a=='G') return 3;
	else if(a=='T') return 4;
	return 114514;
}
signed main(){
	n=gi();
	while(n--){
		scanf("%s",s+1);
		len=strlen(s+1);
		s[len+1]='$';
		t[0].fail=1;t[1].len=-1;ans=len;las=0;node=1;dp[0]=1;
		for(R int i=1;i<=len;i++) insert(i,mmap(s[i]));
		printf("%d\n",ans);
		for(R int i=1;i<=node;i++) dp[i]=0;
		for(R int i=0;i<=node;i++) t[i].fail=t[i].len=t[i].trs=t[i].c[0]=t[i].c[1]=t[i].c[2]=t[i].c[3]=t[i].c[4]=0;
	}
}
posted @ 2021-04-18 14:59  zythonc  阅读(137)  评论(0编辑  收藏  举报