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\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]\)记录当前位置\(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\n",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\n",ans%mod);
else
printf("-1\n");
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\n",((1ll*b[0]*(b[0]-1)/2ll)%mod-res+mod)%mod);
return 0;
}

To be continue……

PAM学习小结的更多相关文章

  1. flex学习小结

    接触到flex一个多月了,今天做一个学习小结.如果有知识错误或者意见不同的地方.欢迎交流指教. 画外音:先说一下,我是怎么接触到flex布局的.对于正在学习的童鞋们,我建议大家没事可以逛逛网站,看看人 ...

  2. Python 学习小结

    python 学习小结 python 简明教程 1.python 文件 #!/etc/bin/python #coding=utf-8 2.main()函数 if __name__ == '__mai ...

  3. react学习小结(生命周期- 实例化时期 - 存在期- 销毁时期)

    react学习小结   本文是我学习react的阶段性小结,如果看官你是react资深玩家,那么还请就此打住移步他处,如果你想给一些建议和指导,那么还请轻拍~ 目前团队内对react的使用非常普遍,之 ...

  4. objective-c基础教程——学习小结

    objective-c基础教程——学习小结   提纲: 简介 与C语言相比要注意的地方 objective-c高级特性 开发工具介绍(cocoa 工具包的功能,框架,源文件组织:XCode使用介绍) ...

  5. pthread多线程编程的学习小结

    pthread多线程编程的学习小结  pthread 同步3种方法: 1 mutex 2 条件变量 3 读写锁:支持多个线程同时读,或者一个线程写     程序员必上的开发者服务平台 —— DevSt ...

  6. ExtJs学习笔记之学习小结LoginDemo

    ExtJs学习小结LoginDemo 1.示例:(登录界面) <!DOCTYPE html> <html> <head> <meta charset=&quo ...

  7. 点滴的积累---J2SE学习小结

    点滴的积累---J2SE学习小结 什么是J2SE J2SE就是Java2的标准版,主要用于桌面应用软件的编程:包括那些构成Java语言核心的类.比方:数据库连接.接口定义.输入/输出.网络编程. 学习 ...

  8. (转) Parameter estimation for text analysis 暨LDA学习小结

    Reading Note : Parameter estimation for text analysis 暨LDA学习小结 原文:http://www.xperseverance.net/blogs ...

  9. dubbo学习小结

    dubbo学习小结 参考: https://blog.csdn.net/paul_wei2008/article/details/19355681 https://blog.csdn.net/liwe ...

随机推荐

  1. root登陆530 Permission denied、530 Login incorrect解决

    感谢大佬:https://blog.51cto.com/3241766/2316986?source=dra 背景:由于云平台上22端口不对外放开,sftp使用不了,故选择ftp服务 操作系统版本: ...

  2. java创建一个子类对象是会调用父类的构造方法会不会创建父类

    1.子类在创建实例后,类初始化方法会调用父类的初始化方法(除了Java.lang.Object类,因为java.lang.Object类没有父类),而这种调用会逐级追述,直到java.lang.Obj ...

  3. JUC并发包学习

    1.什么是JUC java.util工具包.包.分类 业务:普通的线程代码 Thread Runable:没有返回值.效率相对于Callable相对较低. 2.线程和进程 进程:一个程序.如:QQ.e ...

  4. Spring中@Autowired 注解的注入规则

    默认根据类型,匹配不到则根据bean名字 1.声明一个service接口 public interface HelloService { void sayHello(); } 2.service接口的 ...

  5. 字符编码和Python代码操作文件

    字符编码和Python代码操作文件 读写模式之a模式 # a模式 只追加模式 # 路径不存在:自动创建 with open(r'a.txt','a',encoding='utf8') as f: pa ...

  6. PHP面试常考内容之面向对象(3)

    PHP面试专栏正式起更,每周一.三.五更新,提供最好最优质的PHP面试内容.继上一篇"PHP面试常考内容之面向对象(2)"发表后,今天更新面向对象的最后一篇(3).需要(1),(2 ...

  7. 基于TI DSP TMS320C6455、Xilinx V5 FPGA XC5VSX95T的高速数据处理核心板

    一.板卡概述 该DSP+FPGA高速信号采集处理板由我公司自主研发,包含一片TI DSP TMS320C6455和一片Xilinx V5 FPGA XC5VSX95T-1FF1136i.包含1个千兆网 ...

  8. Vue中组件通信的几种方法(Vue3的7种和Vue2的12种组件通信)

    Vue3组件通信方式: props $emit expose / ref $attrs v-model provide / inject Vuex 使用方法: props 用 props 传数据给子组 ...

  9. 通过shell脚本进行linux服务器的CPU和内存压测

    文章目录 内存压测 python的方式 shell的方式 cpu压测 在正常手段下,这个只是压测的方法 在不正常手段下(crontab计划任务),可以提高CPU和内存的使用率 什么?你问我为什么要提高 ...

  10. c++ 汇编代码看内存分配

    汇编代码看内存分配 (1). 程序运行时分为存储区域分为 存储区域 存储内容 extra 代码区 存放代码指令,包括除字符串常量的字面值 静态存储区 存放静态变量和全局变量 执行main之前就分配好了 ...