zoukankan      html  css  js  c++  java
  • PAM学习小结

    PAM

    回文自动机

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

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

    功能:

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

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

    3.最小回文划分

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

    回文树

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

    还是稍微解释一下:

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

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

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

    4.和(Trie)相似的其他性质,不说了

    Fail指针

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

    (Fail)指针含义:这个节点所代表的回文串的最长回文后缀

    Trans指针

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

    (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代表上次插入字符后指向的回文树位置
    

    其中(fail,len,tree,trans)为PAM上的信息

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

    奇树和偶树的根长度(len)分别为(-1)(0)

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

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

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

    新点的(fail)怎么求呢。

    明显为从(pre)开始跳(fail),找到{ [第二个(满足(s[i-len[x]-1]==u)) 的节点(x) ](u)儿子 }

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

    跳到根记得判断

    特别提醒:节点(1)为奇根,节点(0)为偶根,(fail[0]=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
    }
    

    至于(trans)维护也和(fail)差不多

    根据(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) (这超好理解的吧

    那就在多维护一个信息(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]双倍回文

    学好(trans)指针,秒切此题

    明显:当存在(i)满足(len[trans[i]]*2==len[i])并且满足题意中(len[trans[i]]%2==0)即为符合题意的串,取最长即可。

    代码:真·模板

    #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
    ",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])记录当前位置(i)结尾最长回文串长度

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

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

    (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
    ",ans);
    	return 0;
    }
    

    P4762[CERC2014]Virus synthesis

    PAM好题,请好好思考

    题意:

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

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

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

    求最小化操作数, (∣ S∣≤10^5)

    题解:

    PAM上dp

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

    一眼可得PAM

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

    当我们处理完了(sum),对于长度(len)为奇数的节点的信息(sum)计入数组(a[i]).

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

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

    重点来了

    讲一下怎么处理(sum)

    我们可以发现当一个节点(u)(sum+1),那么(fail[u])(sum)也要(+1)

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

    PAM的时候打个标记,最后统一一个拓扑排序向(fail)去更新(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自动机是建好(Trie)后再进行(getFail)的,(fail)的节点编号是会大于自身节点编号

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

    所以就可以不用拓扑排序了,直接一个(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
    ",ans%mod);
    	else
    	printf("-1
    ");
    	return 0;
    }
    

    CF17E Palisection

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

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

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

    先讲思路

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

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

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

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

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

    现在讲一下邻接链表PAM

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

    我们记边结构体(line)

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

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

    当我们要寻找(u)(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
    ",((1ll*b[0]*(b[0]-1)/2ll)%mod-res+mod)%mod);
    	return 0;
    }
    

    To be continue……

  • 相关阅读:
    shell-3
    shell-2
    shell-1
    zabbix监控top
    django指导网址
    文件下载漏洞
    Build a Raspberry Pi powered live train station sign for your desk
    Use a Raspberry Pi to communicate with Amazon AWS IoT
    Describing and Listing Your Stacks
    固有功能参考 Intrinsic Function Reference
  • 原文地址:https://www.cnblogs.com/hyfhaha/p/13654308.html
Copyright © 2011-2022 走看看