PAM回文自动机学习小结

更好的阅读体验

PAM


回文自动机

建议先学习AC自动机:AC自动机讲解超详细

回文自动机,顾名思义,用来处理回文串的自动机。

功能:

1.求 S S S串内本质不同的回文串个数

2.求 S S S串内本质不同的回文串出现次数

3.最小回文划分

4. S S S串中以下标 i i i结尾的最长回文串长度

回文树

看看自己感悟一下。感觉特别形象,都不用解释了啊

还是稍微解释一下:

1.回文数上每一个节点代表了原串上出现过的一个本质不同回文子串,原串上的每一个回文子串都在回文树上有对应。回文树上每一个点代表的串都是回文串。

2.回文树分两部分,奇和偶,奇树上的点代表的回文串长度为奇数,偶树上的为偶

3.儿子节点代表串长度为父亲节点代表串长度 + 2 +2 +2

4.和 T r i e Trie Trie相似的其他性质,不说了

Fail指针

学过AC自动机的OIer们应该就很熟悉啦QwQ

F a i l Fail Fail指针含义:这个节点所代表的回文串的最长回文后缀

Trans指针

一般做许多PAM题目常用的东西

T r a n s Trans Trans指针含义:小于等于当前节点长度一半最长回文后缀

构建PAM

我们要维护以下信息

char s[maxn];		//原串
int fail[maxn];		//fail指针
int len[maxn];		//该节点表示的字符串长度
int tree[maxn][26];	//同Trie,指向儿子
int trans[maxn];	//trans指针
int tot,pre;		//tot代表节点数,pre代表上次插入字符后指向的回文树位置

其中 f a i l , l e n , t r e e , t r a n s fail,len,tree,trans fail,len,tree,trans为PAM上的信息

构建PAM的方法为增量,即一个一个加入字符构建PAM

奇树和偶树的根长度 l e n len len分别为 − 1 -1 1 0 0 0

设当前我们插入原串中 i i i位置的字符 u u u

那么以 i i i为结尾的最长回文串应该为(以 i − 1 i-1 i1为结尾的最长回文串 + u +u +u),并且那个回文串要满足前一个字符等于 u u u(不然就不是回文串了啊)

要找到那个点非常简单,不断从 p r e pre pre开始跳 f a i l fail fail,直到找到一个满足 s [ i − l e n [ x ] − 1 ] = = u s[i-len[x]-1]==u s[ilen[x]1]==u 的节点 F a i l Fail Fail ,那么从 F a i l Fail Fail建一个 u u u儿子即可以表示新的回文串。

新点的 f a i l fail fail怎么求呢。

明显为从 p r e pre pre开始跳 f a i l fail fail,找到**{** [第二个(满足 s [ i − l e n [ x ] − 1 ] = = u s[i-len[x]-1]==u s[ilen[x]1]==u) 的节点 x x x **]**的 u u u儿子 }

也就是从 F a i l Fail Fail开始跳 f a i l fail fail,找到**{** [第一个(满足 s [ i − l e n [ x ] − 1 ] = = u s[i-len[x]-1]==u s[ilen[x]1]==u) 的节点 x x x **]**的 u u u儿子 }

跳到根记得判断

特别提醒:节点 1 1 1为奇根,节点 0 0 0为偶根, f a i l [ 0 ] = 1 fail[0]=1 fail[0]=1 , l e n [ 1 ] = − 1 len[1]=-1 len[1]=1

时间复杂度证明参考OIwiki:OIwiki-PAM

放代码理解:

int getfail(int x,int i){		//从x开始跳fail,满足字符s[i]的节点
	while(i-len[x]-1<0||s[i-len[x]-1]!=s[i])x=fail[x];
	return x;
}
void insert(int u,int i){
	int Fail=getfail(pre,i);		//找到符合要求的点
	if(!tree[Fail][u]){		//没建过就新建节点
		len[++tot]=len[Fail]+2;	//长度自然是父亲长度+2
		fail[tot]=tree[getfail(fail[Fail],i)][u];	//fail为满足条件的次短回文串+u
		tree[Fail][u]=tot;		//认儿子
	}
	pre=tree[Fail][u];		//更新pre
}

至于 t r a n s trans trans维护也和 f a i l fail fail差不多

根据 t r a n s trans trans的定义去推一下怎么搞吧

放一下完整代码:

char s[maxn];		//原串
int fail[maxn];		//fail指针
int len[maxn];		//该节点表示的字符串长度
int tree[maxn][26];	//同Trie,指向儿子
int trans[maxn];	//trans指针
int tot,pre;		//tot代表节点数,pre代表上次插入字符后指向的回文树位置
int getfail(int x,int i){		//从x开始跳fail,满足字符s[i]的节点
	while(i-len[x]-1<0||s[i-len[x]-1]!=s[i])x=fail[x];
	return x;
}
int gettrans(int x,int i){
	while(((len[x]+2)<<1)>len[tot]||s[i-len[x]-1]!=s[i])x=fail[x];
	return x;
}
void insert(int u,int i){
	int Fail=getfail(pre,i);		//找到符合要求的点
	if(!tree[Fail][u]){		//没建过就新建节点
		len[++tot]=len[Fail]+2;	//长度自然是父亲长度+2
		fail[tot]=tree[getfail(fail[Fail],i)][u];	//fail为满足条件的次短回文串+u
		tree[Fail][u]=tot;		//指儿子
		if(len[tot]<=2)trans[tot]=fail[tot];	//特殊trans
		else{
			int Trans=gettrans(trans[Fail],i);	//求trans
			trans[tot]=tree[Trans][u];
		}
	}
	pre=tree[Fail][u];		//更新pre
}

应用

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

求第 i 个整数表示原串以第 i 个字符结尾的回文子串个数,强制在线

明显:一个回文串的答案等于其最长回文后缀的答案 + 1 +1 +1 (这超好理解的吧

那就在多维护一个信息 a n s ans ans表示答案,新建节点时更新即可

ans[tot]=ans[fail[tot]]+1;

答案为lastans=ans[pre];

代码:

#include<bits/stdc++.h>
#define maxn 510001
using namespace std;
char s[maxn];
int fail[maxn],len[maxn],ans[maxn],trie[maxn][26];
int pre,slen,lastans,tot;
int getfail(int x,int i){
	while(i-len[x]-1<0||s[i-len[x]-1]!=s[i])x=fail[x];
	return x;
}
int main(){
	scanf("%s",s);slen=strlen(s);
	fail[0]=1;len[1]=-1;tot=1;
	for(int i=0;i<slen;i++){
		if(i>=1)s[i]=(s[i]-97+lastans)%26+97;
		int u=s[i]-'a';
		int Fail=getfail(pre,i);
		if(!trie[Fail][u]){
			fail[++tot]=trie[getfail(fail[Fail],i)][u];
			trie[Fail][u]=tot;
			len[tot]=len[Fail]+2;
			ans[tot]=ans[fail[tot]]+1;
		}
		pre=trie[Fail][u];
		lastans=ans[pre];
		printf("%d ",lastans);
	}
	return 0;
}

P4287[SHOI2011]双倍回文

学好 t r a n s trans trans指针,秒切此题

明显:当存在 i i i满足 l e n [ t r a n s [ i ] ] ∗ 2 = = l e n [ i ] len[trans[i]]*2==len[i] len[trans[i]]2==len[i]并且满足题意中 l e n [ t r a n s [ i ] ] len[trans[i]]%2==0 len[trans[i]]即为符合题意的串,取最长即可。

代码:真·模板

#include<bits/stdc++.h>
#define maxn 510001
using namespace std;
char s[maxn];		//原串
int fail[maxn];		//fail指针
int len[maxn];		//该节点表示的字符串长度
int tree[maxn][26];	//同Trie,指向儿子
int trans[maxn];	//trans指针
int tot,pre;		//tot代表节点数,pre代表上次插入字符后指向的回文树位置
int getfail(int x,int i){		//从x开始跳fail,满足字符s[i]的节点
	while(i-len[x]-1<0||s[i-len[x]-1]!=s[i])x=fail[x];
	return x;
}
int gettrans(int x,int i){
	while(((len[x]+2)<<1)>len[tot]||s[i-len[x]-1]!=s[i])x=fail[x];
	return x;
}
void insert(int u,int i){
	int Fail=getfail(pre,i);		//找到符合要求的点
	if(!tree[Fail][u]){		//没建过就新建节点
		len[++tot]=len[Fail]+2;	//长度自然是父亲长度+2
		fail[tot]=tree[getfail(fail[Fail],i)][u];	//fail为满足条件的次短回文串+u
		tree[Fail][u]=tot;		//指儿子
		if(len[tot]<=2)trans[tot]=fail[tot];	//特殊trans
		else{
			int Trans=gettrans(trans[Fail],i);	//求trans
			trans[tot]=tree[Trans][u];
		}
	}
	pre=tree[Fail][u];		//更新pre
}
int slen,ans;
int main(){
	scanf("%d",&slen);
	scanf("%s",s);
	fail[0]=1;len[1]=-1;tot=1;
	for(int i=0;i<slen;i++)insert(s[i]-'a',i);
	for(int i=2;i<=tot;i++){
		if(len[trans[i]]*2==len[i]&&len[trans[i]]%2==0)
		ans=max(ans,len[i]);
	}
	printf("%d\n",ans);
	return 0;
}

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

题目描述:

顺序和逆序读起来完全一样的串叫做回文串。比如`acbca`是回文串,而`abc`不是(`abc`的顺序为`abc`,逆序为`cba`,不相同)。

输入长度为n的串S,求S的最长双回文子串T,即可将T分为两部分X,Y,(|X|,|Y|≥1)且X和Y都是回文串。

简单PAM题

题解:

正着建一棵PAM, a [ i ] a[i] a[i]记录当前位置 i i i结尾最长回文串长度

反着建一棵PAM, b [ i ] b[i] b[i]记录当前位置 i i i结尾最长回文串长度

a [ i ] + b [ i + 1 ] a[i]+b[i+1] a[i]+b[i+1]即为以 i i i为分界的双回文串

a [ i ] + b [ i + 1 ] a[i]+b[i+1] a[i]+b[i+1]最大值即为答案

代码:封装版PAM

#include<bits/stdc++.h>
#define maxn 510001
using namespace std;
char s[maxn];
int slen,a[maxn],b[maxn],ans;
struct PAM{
	int fail[maxn],len[maxn],trie[maxn][26];
	int tot,pre;
	void init(){fail[0]=1;len[1]=-1;tot=1;pre=0;}
	int getfail(int x,int i){
		while(i-len[x]-1<0||s[i-len[x]-1]!=s[i])x=fail[x];
		return x;
	}
	void insert(int u,int i){
		int Fail=getfail(pre,i);
		if(!trie[Fail][u]){
			fail[++tot]=trie[getfail(fail[Fail],i)][u];
			trie[Fail][u]=tot;
			len[tot]=len[Fail]+2;
		}
		pre=trie[Fail][u];
	}
}A,B;
int main(){
	scanf("%s",s);slen=strlen(s);A.init();B.init();
	for(int i=0;i<slen;i++)A.insert(s[i]-'a',i),a[i]=A.len[A.pre];
	reverse(s,s+slen);		//翻转
	for(int i=0;i<slen;i++)B.insert(s[i]-'a',i),b[slen-i-1]=B.len[B.pre];
	for(int i=0;i<slen-1;i++)ans=max(ans,a[i]+b[i+1]);
	printf("%d\n",ans);
	return 0;
}

P4762[CERC2014]Virus synthesis

PAM好题,请好好思考

题意:

初始有一个空串,利用下面的操作构造给定串 S S S

1、串开头或末尾加一个字符

2、串开头或末尾加一个该串的逆串

求最小化操作数, ∣   S ∣ ≤ 1 0 5 ∣\ S∣≤10^5  S105

题解:

PAM上dp

P1659[国家集训队]拉拉队排练

一眼可得PAM

我们令PAM上多记录一个信息 s u m sum sum,表示该节点表示串在原串上出现了多少次。

当我们处理完了 s u m sum sum,对于长度 l e n len len为奇数的节点的信息 s u m sum sum计入数组 a [ i ] a[i] a[i].

a [ i ] a[i] a[i]为长度为 i i i的回文子串出现次数。

a [ i ] a[i] a[i]降序排序后累加答案快速幂处理一下即可,不需太多点拨

重点来了

讲一下怎么处理 s u m sum sum

我们可以发现当一个节点 u u u s u m + 1 sum+1 sum+1,那么 f a i l [ u ] fail[u] fail[u] s u m sum sum也要 + 1 +1 +1

熟悉AC自动机的OIer可以敏锐的察觉到可以用拓扑排序了(例如我

PAM的时候打个标记,最后统一一个拓扑排序向 f a i l fail fail去更新 s u m sum sum即可

queue<int >q;		//in数组为fail入边数量
void tuopu(){
	for(int i=0;i<=tot;i++)if(in[i]==0)q.push(i);
	while(!q.empty()){
		int u=q.front();q.pop();
		sum[fail[u]]+=sum[u];in[fail[u]]--;
		if(in[fail[u]]==0)q.push(fail[u]);
	}
}

好像没什么问题,多一个拓扑排序就行了

但真的如此吗?

我们观察PAMAC自动机的区别

AC自动机是建好 T r i e Trie Trie后再进行 g e t F a i l getFail getFail的, f a i l fail fail的节点编号是会大于自身节点编号

PAM不会出现这种情况,PAM f a i l fail fail定义不同于AC自动机,构建使用增量法,保证了 f a i l fail fail的节点编号一定小于自身节点编号。

所以就可以不用拓扑排序了,直接一个 f o r for for从后到前更新即可

for(int i=tot;i>=0;i--)sum[fail[i]]+=sum[i];

总代码:

#include<bits/stdc++.h>
#define maxn 1010001
#define ll long long
#define mod 19930726
using namespace std;
char s[maxn];
int fail[maxn],len[maxn],trie[maxn][26],trans[maxn];
long long sum[maxn];
int per,slen,tot;
long long a[maxn],K,ans=1;
int getfail(int x,int i){
	while(i-len[x]-1<0||s[i-len[x]-1]!=s[i])x=fail[x];
	return x;
}
int gettrans(int x,int i){
	while(((len[x]+2)<<1)>len[tot]||s[i-len[x]-1]!=s[i])x=fail[x];
	return x;
}
void insert(int u,int i){
	int Fail=getfail(per,i);
	if(!trie[Fail][u]){
		len[++tot]=len[Fail]+2;
		fail[tot]=trie[getfail(fail[Fail],i)][u];
		trie[Fail][u]=tot;
		if(len[tot]<=2)trans[tot]=fail[tot];
		else{
			int Trans=gettrans(trans[Fail],i);
			trans[tot]=trie[Trans][u];
		}
	}
	per=trie[Fail][u];
	sum[per]++;		//记录sum
}
ll qpow(ll n,ll m){
	ll ans=1ll;
	while(m){
		if(m&1){ans=ans*n;ans%=mod;}
		n=n*n;n%=mod;m>>=1;
	}return ans%mod;
}
int main(){
	scanf("%d%lld",&slen,&K);
	scanf("%s",s);
	fail[0]=1;len[1]=-1;tot=1;
	for(int i=0;i<slen;i++)insert(s[i]-'a',i);
	for(int i=tot;i>=1;i--)sum[fail[i]]+=sum[i];		//更新sum
	for(int i=2;i<=tot;i++)a[len[i]]+=sum[i],a[len[i]]%=mod;	//长度处理
	for(int i=slen;i>=1;i--){			//答案处理
		if(i%2==1){
			if(K>=a[i]){
				ans*=qpow(i,a[i]);ans%=mod;
				K-=a[i];
			}else{
				ans*=qpow(i,K);ans%=mod;
				K-=K;
				break;
			}
		}
	}
	if(K==0)			//判-1
	printf("%lld\n",ans%mod);
	else
	printf("-1\n");
	return 0;
}

CF17E Palisection

卡空间PAM,2010没有PAM,所以都是马拉车

众所周知,PAM拥有十分优秀的时间复杂度,但空间复杂度lj得不行

但这题卡空间,所以得用到邻接链表PAM

先讲思路

题目要求相交的回文子串对,这很难做

于是我们求补集,求不相交的回文子串对,再用总数减即可

求法和上文的最长双回文子串 类似

正反建一次PAM,存该位置结尾的回文子串个数,然后加法改乘法

自己领悟一下,挺简单的。

现在讲一下邻接链表PAM

注意:邻接链表PAM不是使空间变小了,而是用时间换空间

我们记边结构体 l i n e line line

3 3 3个信息: n x , t o , w nx,to,w nx,to,w 分别表示上一条边,这条边通向的节点编号,这条边是代表哪个字符

数组 f i r [ i ] fir[i] fir[i]表示 i i i伸出的最后一条边的编号(头插式

当我们要寻找 u u u v v v儿子

我们就像邻接链表一样找,直到有一条边的 w = = v w==v w==v为止

找不到记得指根

int getson(int u,int v){
	for(int i=u;i!=-1;i=l[i].nx)
		if(l[i].w==v)return l[i].to;
	return -1;
}

建点的时候把边建上

void insert(int u,int i){
	int Fail=getfail(pre,i),ls=getfail(fail[Fail],i);
	if(getson(fir[Fail],u)==-1){
		if(getson(fir[ls],u)==-1)fail[++tot]=0;		//找不到指根
		else fail[++tot]=getson(fir[ls],u);	//找到了
		l[++cnt]=(line){fir[Fail],tot,u};fir[Fail]=cnt;		//加边
		len[tot]=len[Fail]+2;
		ans[tot]=ans[fail[tot]]+1;		//结尾回文子串个数
		pre=tot;
	}else 
	pre=getson(fir[Fail],u);
}

然鹅事实上你仍然过不了,你还要继续压空间,省掉一堆数组就可以过啦!

总代码:

#include<bits/stdc++.h>
#define maxn 2000005
#define mod 51123987
using namespace std;
char s[maxn];
int slen,b[maxn];
long long res;
int fail[maxn],len[maxn],ans[maxn],fir[maxn];
struct line{int nx,to,w;}l[maxn];
int tot,pre,cnt;
void init(){
	memset(fir,-1,sizeof(fir));cnt=0;
	fail[0]=1;len[1]=-1;tot=1;pre=0;
}
int getfail(int x,int i){
	while(i-len[x]-1<0||s[i-len[x]-1]!=s[i])x=fail[x];
	return x;
}
int getson(int u,int v){
	for(int i=u;i!=-1;i=l[i].nx)
		if(l[i].w==v)return l[i].to;
	return -1;
}
void insert(int u,int i){
	int Fail=getfail(pre,i),ls=getfail(fail[Fail],i);
	if(getson(fir[Fail],u)==-1){
		if(getson(fir[ls],u)==-1)fail[++tot]=0;
		else fail[++tot]=getson(fir[ls],u);
		l[++cnt]=(line){fir[Fail],tot,u};fir[Fail]=cnt;
		len[tot]=len[Fail]+2;
		ans[tot]=ans[fail[tot]]+1;
		pre=tot;
	}else 
	pre=getson(fir[Fail],u);
}
int main(){
	int n;
	scanf("%d",&n);
	scanf("%s",s);slen=strlen(s);init();
	reverse(s,s+slen);
	for(int i=0;i<slen;i++)insert(s[i]-'a',i),b[slen-i-1]=ans[pre];
	for(int i=slen-1;i>=0;i--)b[i]+=b[i+1],b[i]%=mod;
	reverse(s,s+slen);init();
	for(int i=0;i<slen-1;i++){
		insert(s[i]-'a',i);int x=ans[pre];
		res+=(1ll*x*b[i+1])%mod,res%=mod;
	}
	printf("%lld\n",((1ll*b[0]*(b[0]-1)/2ll)%mod-res+mod)%mod);
	return 0;
}

To be continue……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值