• PAM(回文自动机)学习笔记


    PAM学习笔记

    一、概述

    感觉(PAM)(SAM)什么的好理解多了。。。

    顾名思义,回文自动机(PAM)就是一个能够高效处理一个字符串所有的回文子串的自动机,也就是一个字符串所有回文子串的信息的高度压缩得到的结果。因此,(PAM)能够方便地解决一系列关于回文串的问题。

    二、结构

    回文自动机维护了原串上的所有本质不同的回文串。

    回文自动机的结构可以看成是两棵树,一棵的根是奇根(odd),代表着一个长度为(-1)的实际上不存在的回文串,存储所有长度为奇数的回文串,一棵的根是偶根(even),代表着长度为(0)的回文串,存储所有长度为偶数的回文串。

    自动机的每一个转移对应树上的一条边,同时带有一个字符(c),如果这条边连接(u ightarrow v),那么(s_v)就是(s_u)在前后分别加上字符(c)组成的,(s_u)(u)节点维护的回文串。显然,这样做可以表示出所有的回文子串。

    (AC)自动机类似,(PAM)也带有(fail)边,在这里每个点的(fail)连向它的最长回文后缀对应的点,同时我们特判(fail[even]=fail[odd]=odd),同样的(fail)指针也能构成一棵树

    翁文涛论文《回文树及其应用》中的示例图:

    三、对线性状态数与转移数的证明

    • 定理:对于一个字符串(s),它的本质不同的回文子串至多有(|s|)

    • 证明:考虑数学归纳法

      • (|s|=1)时定理显然成立,当(|s|>1)时,设(s=tc),假设结论对(t)成立,那么增加(c)后设新增的回文串的左端点从小到大排序后依次为(l_1,l_2,dots l_k),于是有(s[l_1dots |s|])是回文串

      • 那么我们就有(s[l_idots |s|]=s[l_1,|s|-l_1+l_i]),当(i ot= 1)(|s|-l_1+l_ile|t|),也就是说(s[l_idots|s|])这个回文串已经在(t)中出现过了。因此,每增加一个字符,本质不同的回文子串数量最多增加(1)

      • 由此可知该定理成立

    • 因为(PAM)中的节点个数(=)本质不同的回文子串的个数(+2)(去掉(odd)(even)),于是(PAM)的状态数是线性的,每个状态由唯一的转移边转移而来,因此转移数也是线性的。

    四、构造

    对于每一个节点,我们需要维护它对应回文串的长度(len)以及(fail),并用类似(Trie)树的方法维护它的儿子,我们用(0)表示(even)(1)表示(odd),初始化(len[0]=0,len[1]=-1)

    采用增量法构造(PAM),因为上面的定理,我们每次插入最多只会增加一个节点,即新字符串的最长回文后缀。记(last)为上次插入后字符串的最长回文后缀对应的点,初始为(0),设我们插入的是第(i)个字符(u),那么插入后新字符串的最长回文后缀,应当是由原来的一个点(x)通过(u)转移而来,并且必须满足(s[i-len[x]-1]=u),这样就能满足它是回文串了。

    找到(x)的方法则是从(last)开始一路跳(fail)直到满足条件为止。设新点为(tot),那么(len[tot]=len[x]+2)

    新点的(fail)怎么求呢?如果(x)是奇根,那么我们直接将(fail)指向(0),否则我们同样从(fail[x])开始一路跳(fail)直到找到一个满足(s[i-len[x]-1]=u)(x),那么(x)(u)儿子就是新点的(fail)

    对于复杂度证明,(PAM)主要看起来不显然是线性的操作就是跳(fail),注意到新找到的点的深度最多(+1),于是构造的复杂度也是(mathcal O(|s|))的。

    五、例题

    • 例题:洛谷模板题

      • 即求本质不同的回文子串个数,就等于回文自动机节点数(-2)。(去掉奇根与偶根)

      • 代码:

    • #include<bits/stdc++.h>
      using namespace std;
      const int N=5e5+10;
      char s[N];
      int n;
      namespace PAM{
      	int fail[N],len[N],dep[N],last,tot;//dep为fail树的深度
      	int ch[N][26]; 
      	inline void init(){
      		len[0]=0;len[1]=-1;//1为odd,0为even 
      		fail[0]=1;fail[1]=1;
      		last=0;tot=1;
      	}
      	inline void newnode(int x,int p,int v){
      		ch[p][v]=++tot;len[tot]=len[p]+2;
      		if(p==1) fail[tot]=0;
      		else{
      			p=fail[p];
      			while(s[x-len[p]-1]!=s[x]) p=fail[p];
      			fail[tot]=ch[p][v]; 
      		}
      		dep[tot]=dep[fail[tot]]+1;
      	}
      	inline int insert(int x){
      		int p=last,v=s[x]-'a';
      		while(s[x-len[p]-1]!=s[x]) p=fail[p];//跳fail直至找到能增加转移v的点 
      		if(!ch[p][v]) newnode(x,p,v);
      		last=ch[p][v];
      		return dep[last];
      	} 
      }
      
      int main(){
      	scanf("%s",s+1);
      	n=strlen(s+1);
      	PAM::init();
      	int lastans=0;
      	for(int i=1;i<=n;++i){
      		s[i]=(s[i]-97+lastans)%26+97;
      		printf("%d ",lastans=PAM::insert(i));
      	}
      	return 0;
      } 
      

      例题:[APIO2014]回文串

      • 即求一个回文子串在原串中的出现次数

      • 考虑每新增一个字符时它的所有回文后缀出现次数都会(+1),于是给它的最大回文后缀打一个(tag),然后按拓扑序从大到小枚举,将当前点的(tag)添加到(fail)上即可。在(PAM)中,节点本就是按拓扑序加入的,因此直接倒着扫一遍即可。

      • 代码:

        #include<bits/stdc++.h>
        using namespace std;
        const int N=3e5+10;
        char s[N];
        int n;
        namespace PAM{
        	int fail[N],len[N],ch[N][26],siz[N];
        	int last,tot;
        	inline void init(){
        		len[0]=0;len[1]=-1;
        		fail[0]=fail[1]=1;
        		last=0;tot=1;
        	}
        	inline void newnode(int x,int p,int v){
        		ch[p][v]=++tot;len[tot]=len[p]+2;
        		if(p==1) fail[tot]=0;
        		else{
        			p=fail[p];
        			while(s[x-len[p]-1]!=s[x]) p=fail[p];
        			fail[tot]=ch[p][v];
        		}
        	}
        	inline void insert(int x){
        		int p=last,v=s[x]-'a';
        		while(s[x-len[p]-1]!=s[x]) p=fail[p];
        		if(!ch[p][v]) newnode(x,p,v);
        		last=ch[p][v];
        		siz[last]++;
        	}
        	inline void getsiz(){
        		long long ans=0;
        		for(int i=tot;i>=2;--i){
        			siz[fail[i]]+=siz[i];
        			ans=max(ans,1ll*siz[i]*len[i]);
        		}
        		printf("%lld
        ",ans);
        	}
        }
        int main(){
        	scanf("%s",s+1);
        	n=strlen(s+1);
        	PAM::init();
        	for(int i=1;i<=n;++i) PAM::insert(i);
        	PAM::getsiz();
        	return 0;
        }
        
    • 例题:[SHOI2011]双倍回文

      • 对于这道题,我们引入一个(PAM)非常常用的一个东西:(trans)指针,该指针指向不超过回文串长度一半的最长回文后缀,有了它,那么这道题中我们只需要看每一个回文串的长度是否是(4)的倍数,且它的(trans)的长度恰好是它的一半即可

      • 根据定义不难确定(trans)的维护方式:新增一个节点时,若节点长度(le 2)则让(trans=fail).,否则,设它的父亲为(u),先跳到(trans[u]),再从(trans[u])一路跳(fail)直到跳到一个长度不超过当前回文串一半的回文后缀即可

      • 代码:

        #include<bits/stdc++.h>
        using namespace std;
        const int N=5e5+10;
        int n,ans;
        char s[N];
        namespace PAM{
        	int last,tot;
        	int ch[N][26],fail[N],len[N];
        	int trans[N];//trans指向不超过串长一半的最长回文后缀 
        	inline void init(){
        		len[0]=0;len[1]=-1;
        		fail[0]=fail[1]=1;
        		last=0;tot=1;
        	}
        	inline void newnode(int x,int p,int v){
        		ch[p][v]=++tot;len[tot]=len[p]+2;
        		if(p==1) fail[tot]=trans[tot]=0;
        		else{
        			int rec=p;
        			p=fail[p];
        			while(s[x-len[p]-1]!=s[x]) p=fail[p];
        			fail[tot]=ch[p][v];
        			if(len[tot]==2) trans[tot]=fail[tot];
        			else{
        				p=trans[rec];
        				while(((len[p]+2)<<1)>len[tot]||s[x-len[p]-1]!=s[x]) p=fail[p];
        				trans[tot]=ch[p][v];
        			}
        		}
        	}
        	inline int insert(int x){
        		int p=last,v=s[x]-'a';
        		while(s[x-len[p]-1]!=s[x]) p=fail[p];
        		if(!ch[p][v]) newnode(x,p,v);
        		last=ch[p][v];
        		if(len[trans[last]]==len[last]/2&&len[last]%4==0) return len[last];
        		else return 0;
        	} 
        }
        int main(){
        	scanf("%d",&n);
        	scanf("%s",s+1);
        	PAM::init();
        	for(int i=1;i<=n;++i){
        		int x=PAM::insert(i);
        		ans=max(ans,x);	
        	}
        	printf("%d
        ",ans);
        	return 0;
        }
        
    • 例题:loj#141 回文子串

      这道题要求我们同时支持在字符串前后添加,于是我们初始将字符串的左右端点设为(4e5+1)(4e5),然后对前后插入时分别维护一个(llast)(rlast),对每个节点维护最长回文前缀与最长回文后缀,后者就是(fail),而因为是回文串,所有最长回文前缀就等于最长回文后缀所以直接使用(fail)即可,注意当插入字符后整个字符串成为回文串时,要将(llast)(rlast)合并

      #include<bits/stdc++.h>
      using namespace std;
      typedef long long ll;
      const int N=4e5+10;
      namespace PAM{
      	char s[N<<1];
      	ll ans;
      	int L,R,llast,rlast,tot,ch[N][26],len[N],fail[N],dep[N];
      	inline void init(){
      		len[0]=0;len[1]=-1;
      		fail[0]=1;fail[1]=1;
      		llast=rlast=0;tot=1;
      		L=N,R=N-1;
      	}
      	inline void push_back(char c){
      		int v=c-'a';
      		s[++R]=c;
      		int p=rlast;
      		while(s[R-len[p]-1]!=c) p=fail[p];
      		if(!ch[p][v]){
      			ch[p][v]=++tot;len[tot]=len[p]+2;
      			if(p==1) fail[tot]=0;
      			else{
      				int now=fail[p];
      				while(s[R-len[now]-1]!=c) now=fail[now];
      				fail[tot]=ch[now][v];
      			}
      			dep[tot]=dep[fail[tot]]+1;
      		}
      		rlast=ch[p][v];
      		ans+=dep[rlast];
      		if(R-L+1==len[rlast]) llast=rlast;
      	}
      	inline void push_front(char c){
      		int v=c-'a';
      		s[--L]=c;
      		int p=llast;
      		while(s[L+len[p]+1]!=c) p=fail[p];
      		if(!ch[p][v]){
      			ch[p][v]=++tot;len[tot]=len[p]+2;
      			if(p==1) fail[tot]=0;
      			else{
      				int now=fail[p];
      				while(s[L+len[now]+1]!=c) now=fail[now];
      				fail[tot]=ch[now][v];
      			}
      			dep[tot]=dep[fail[tot]]+1;
      		}
      		llast=ch[p][v];
      		ans+=dep[llast];
      		if(len[llast]==R-L+1) rlast=llast;
      	}
      }
      using PAM::ans;
      int q;
      char ch[N];
      int main(){
      //	freopen("in.in","r",stdin);
      //	freopen("ans.ans","w",stdout);
      	scanf("%s",ch+1);
      	int len=strlen(ch+1);
      	PAM::init();
      	for(int i=1;i<=len;++i) PAM::push_back(ch[i]);
      	scanf("%d",&q);
      	while(q--){
      		int op;
      		scanf("%d",&op);
      		if(op<=2){
      			scanf("%s",ch+1);
      			len=strlen(ch+1);
      			for(int i=1;i<=len;++i){
      				if(op==1) PAM::push_back(ch[i]);
      				else PAM::push_front(ch[i]);
      			}
      		}
      		else printf("%lld
      ",ans);
      	}
      	return 0;
      }
      
    • 例题:洛谷P5555 秩序魔咒

      sol

  • 相关阅读:
    XML案例
    4.6Java数组的遍历
    4.3Java多态(polymorphism)
    XML文档的标准
    4.6Java数组的定义
    4.6Java数组初始化的方式
    XML介绍
    4.6Java对象转型(casting)
    HelloWorld之MyBatis
    Hibernate查询方法比较
  • 原文地址:https://www.cnblogs.com/tqxboomzero/p/14312742.html
Copyright © 2020-2023  润新知