PAM学习小结

hyfhaha

2020-09-11 21:10:26

Personal

# PAM [TOC] 回文自动机 建议先学习AC自动机:[AC自动机讲解超详细](https://www.cnblogs.com/hyfhaha/p/10802604.html) 回文自动机,顾名思义,用来处理回文串的自动机。 ## 功能: 1.求$S$串内本质不同的回文串个数 2.求$S$串内本质不同的回文串出现次数 3.最小回文划分 4.$S$串中以下标$i$结尾的最长回文串长度 # 回文树 ![](https://img2020.cnblogs.com/blog/1593693/202009/1593693-20200911214341133-576021393.png) 看看自己感悟一下。感觉特别形象,都不用解释了啊 还是稍微解释一下: 1.回文数上每一个节点代表了原串上出现过的一个本质不同回文子串,原串上的每一个回文子串都在回文树上有对应。回文树上每一个点代表的串都是回文串。 2.回文树分两部分,奇和偶,奇树上的点代表的回文串长度为奇数,偶树上的为偶 3.儿子节点代表串长度为父亲节点代表串长度$+2$ 4.和$Trie$相似的其他性质,不说了 ## Fail指针 学过AC自动机的OIer们应该就很熟悉啦QwQ $Fail$指针含义:这个节点所代表的回文串的**最长回文后缀** ## Trans指针 一般做许多PAM题目常用的东西 $Trans$指针含义:小于等于当前节点长度**一半**的**最长回文后缀** # 构建PAM 我们要维护以下信息 ```cpp 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](https://oi-wiki.org/string/pam/#_2) 放代码理解: ```cpp 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$的定义去推一下怎么搞吧 放一下完整代码: ```cpp 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)](https://www.luogu.com.cn/problem/P5496) 求第 *i* 个整数表示原串以第 *i* 个字符结尾的回文子串个数,强制在线 明显:一个回文串的答案等于其最长回文后缀的答案$+1$ (这超好理解的吧 那就在多维护一个信息$ans$表示答案,新建节点时更新即可 `ans[tot]=ans[fail[tot]]+1;` 答案为`lastans=ans[pre];` 代码: ```cpp #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]双倍回文](https://www.luogu.com.cn/problem/P4287) 学好$trans$指针,秒切此题 明显:当存在$i$满足$len[trans[i]]*2==len[i]$并且满足题意中$len[trans[i]]%2==0$即为符合题意的串,取最长即可。 代码:真·模板 ```cpp #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[国家集训队]最长双回文串](https://www.luogu.com.cn/problem/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 ```cpp #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](https://www.luogu.com.cn/problem/P4762) PAM好题,请好好思考 题意: 初始有一个空串,利用下面的操作构造给定串 $S$ 。 1、串开头或末尾加一个字符 2、串开头或末尾加一个该串的逆串 求最小化操作数, $∣\ S∣≤10^5$ 。 题解: PAM上dp ## [P1659[国家集训队]拉拉队排练](https://www.luogu.com.cn/problem/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$即可 ```cpp 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]); } } ``` 好像没什么问题,多一个拓扑排序就行了 但真的如此吗? 我们观察**PAM**和**AC自动机**的区别 **AC自动机**是建好$Trie$后再进行$getFail$的,$fail$的节点编号是会大于自身节点编号 而**PAM**不会出现这种情况,**PAM**$fail$定义不同于**AC自动机**,构建使用增量法,保证了$fail$的节点编号一定小于自身节点编号。 所以就可以不用拓扑排序了,直接一个$for$从后到前更新即可 ```cpp for(int i=tot;i>=0;i--)sum[fail[i]]+=sum[i]; ``` 总代码: ```cpp #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](https://www.luogu.com.cn/problem/CF17E) 卡空间PAM,2010没有PAM,所以都是马拉车 众所周知,PAM拥有十分优秀的时间复杂度,但空间复杂度lj得不行 但这题卡空间,所以得用到**邻接链表PAM** 先讲思路 题目要求相交的回文子串对,这很难做 于是我们求补集,求不相交的回文子串对,再用总数减即可 求法和上文的**最长双回文子串** 类似 正反建一次PAM,存该位置结尾的回文子串个数,然后加法改乘法 自己领悟一下,挺简单的。 现在讲一下**邻接链表PAM** #### 注意:**邻接链表PAM**不是使空间变小了,而是用时间换空间 我们记边结构体$line$ 存$3$个信息:$nx,to,w$ 分别表示上一条边,这条边通向的节点编号,这条边是代表哪个字符 数组$fir[i]$表示$i$伸出的最后一条边的编号(头插式 当我们要寻找$u$的$v$儿子 我们就像邻接链表一样找,直到有一条边的$w==v$为止 找不到记得指根 ```cpp 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; } ``` 建点的时候把边建上 ```cpp 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); } ``` 然鹅事实上你仍然过不了,你还要继续压空间,省掉一堆数组就可以过啦! 总代码: ```cpp #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……