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

时间:2023-03-10 02:48:42
回文树/回文自动机(PAM)学习笔记

回文树(也就是回文自动机)实际上是奇偶两棵树,每一个节点代表一个本质不同的回文子串(一棵树上的串长度全部是奇数,另一棵全部是偶数),原串中每一个本质不同的回文子串都在树上出现一次且仅一次。

一个节点的fail指针指向它的最长回文后缀(不包括自身,所有空fail均连向1)。归纳容易证明,当在原串末尾新增一个字符时,回文树上至多会新增一个节点,这也证明了一个串本质不同的回文子串个数不会超过n。

建树时采用增量构造法,当考虑新字符s[i]时,先找到以s[i-1]为结尾的节点p,并不断跳fail。若代表新增回文子串的节点已存在则直接结束,否则通过fail[p]不断跳fail找到新节点的fail。

0,1号节点均不代表串,常数大于manacher。初始化fail[0]=fail[1]=1,len[1]=-1,tot=1,last=0。

[BZOJ2160]拉拉队排练

建立后缀树后树上DP求出每种回文子串的出现次数即可。

 #include<cstdio>
#include<algorithm>
#define rep(i,l,r) for (int i=(l); i<=(r); i++)
typedef long long ll;
using namespace std; const int N=,mod=;
char s[N];
ll K,sm;
int n,ans=,lst,nd=,len[N],fail[N],son[N][],sz[N];
struct P{ int l,c; }c[N];
bool operator <(const P &a,const P &b){ return a.l>b.l; } int ksm(int a,int b){
int s=;
for (; b; a=1ll*a*a%mod,b>>=)
if (b & ) s=1ll*s*a%mod;
return s;
} void ext(int c,int n,char s[]){
int p=lst;
while (s[n-len[p]-]!=s[n]) p=fail[p];
if (!son[p][c]){
int np=++nd,q=fail[p];
while (s[n-len[q]-]!=s[n]) q=fail[q];
len[np]=len[p]+; fail[np]=son[q][c]; son[p][c]=np;
}
lst=son[p][c]; sz[lst]++;
} int main(){
freopen("bzoj2160.in","r",stdin);
freopen("bzoj2160.out","w",stdout);
scanf("%d%lld%s",&n,&K,s+);
len[]=-; fail[]=fail[]=;
rep(i,,n) ext(s[i]-'a',i,s);
for (int i=nd; i; i--) sz[fail[i]]+=sz[i];
rep(i,,nd) c[i-]=(P){len[i],sz[i]};
sort(c+,c+nd);
rep(i,,nd-){
if (!(c[i].l&)) continue;
ll t=min(K,(ll)c[i].c); ans=1ll*ans*ksm(c[i].l,t)%mod; K-=t;
if (!K) break;
}
printf("%d\n",K?-:ans);
return ;
}

BZOJ2160

[BZOJ3676][APIO2014]回文串

显然建出回文树后求出每个点的出现次数与长度即可。

 #include<cstdio>
#include<cstring>
#include<algorithm>
#define rep(i,l,r) for (int i=(l); i<=(r); i++)
typedef long long ll;
using namespace std; const int N=;
char s[N];
ll ans;
int n,lst,nd=,len[N],fail[N],sz[N],son[N][]; void ext(int c,int n,char s[]){
int p=lst;
while (s[n-len[p]-]!=s[n]) p=fail[p];
if (!son[p][c]){
int np=++nd,q=fail[p];
while (s[n-len[q]-]!=s[n]) q=fail[q];
len[np]=len[p]+; fail[np]=son[q][c]; son[p][c]=np;
}
sz[lst=son[p][c]]++;
} int main(){
freopen("bzoj3676.in","r",stdin);
freopen("bzoj3676.out","w",stdout);
scanf("%s",s+); n=strlen(s+);
fail[]=fail[]=; len[]=-; s[]=-;
rep(i,,n) ext(s[i]-'a',i,s);
for (int i=nd; i; i--) sz[fail[i]]+=sz[i];
rep(i,,nd) ans=max(ans,1ll*sz[i]*len[i]);
printf("%lld\n",ans);
return ;
}

BZOJ3676

[CF17E]Palisection

正难则反,所有回文串对数减去不相交对数。以某个位置结尾的回文子串个数等于它在回文树上代表的节点的深度,后缀和优化一下即可。

 #include<cstdio>
#include<algorithm>
#define rep(i,l,r) for (int i=(l); i<=(r); i++)
#define For(i,x) for (int i=h[x],k; i; i=nxt[i])
typedef long long ll;
using namespace std; const int N=,mod=;
char s[N];
int n,cnt,ans,nd,lst,p1[N],p2[N],dep[N],to[N],nxt[N],val[N],h[N],fail[N],len[N]; void add(int u,int v,int w){ to[++cnt]=v; nxt[cnt]=h[u]; val[cnt]=w; h[u]=cnt; } void init(){
rep(i,,nd) h[i]=fail[i]=len[i]=dep[i]=;
cnt=lst=; nd=; len[]=-; fail[]=fail[]=;
} int son(int x,int c){ For(i,x) if (val[i]==c) return k=to[i]; return ; } void ext(int c,int n,char s[]){
int p=lst;
while (s[n-len[p]-]!=s[n]) p=fail[p];
if (!son(p,c)){
int np=++nd,q=fail[p];
while (s[n-len[q]-]!=s[n]) q=fail[q];
len[np]=len[p]+; fail[np]=son(q,c); dep[np]=dep[fail[np]]+; add(p,np,c);
}
lst=son(p,c);
} int main(){
freopen("cf17e.in","r",stdin);
freopen("cf17e.out","w",stdout);
scanf("%d%s",&n,s+); init();
rep(i,,n) ext(s[i]-'a',i,s),p1[i]=dep[lst],ans=(ans+p1[i])%mod;
ans=1ll*ans*(ans-)/%mod; reverse(s+,s+n+); init();
rep(i,,n) ext(s[i]-'a',i,s),p2[n-i+]=dep[lst];
for (int i=n; i; i--) p2[i]=(p2[i]+p2[i+])%mod;
rep(i,,n) ans=(ans-1ll*p1[i]*p2[i+]%mod+mod)%mod;
printf("%d\n",ans);
return ;
}

CF17E

[Aizu2292]Common Palindromes

给定S,T,询问有多少(l1,r1,l2,r2)使得S[l1,r1]回文且S[l1,r1]=T[l2,r2]。

显然对S建出回文自动机然后T在上面跑,记录每个S中回文串的出现次数以及T中有多少个子串与此串匹配。注意初始x=1(可以认为回文树的根是1),且匹配是不仅要看此节点是否有对应子节点,也要看s[i-len[x]-1]是否等于s[i]。

 #include<cstdio>
#include<cstring>
#include<algorithm>
#define rep(i,l,r) for (int i=(l); i<=(r); i++)
typedef long long ll;
using namespace std; const int N=;
char s[N];
ll ans;
int n,lst,nd=,f[N],son[N][],fail[N],len[N],sz[N]; void ext(int c,int n,char s[]){
int p=lst;
while (s[n-len[p]-]!=s[n]) p=fail[p];
if (!son[p][c]){
int np=++nd,q=fail[p];
while (s[n-len[q]-]!=s[n]) q=fail[q];
len[np]=len[p]+; fail[np]=son[q][c]; son[p][c]=np;
}
sz[lst=son[p][c]]++;
} int main(){
freopen("Aizu2292.in","r",stdin);
freopen("Aizu2292.out","w",stdout);
len[]=-; fail[]=fail[]=;
scanf("%s",s+); n=strlen(s+);
rep(i,,n) ext(s[i]-'A',i,s);
scanf("%s",s+); n=strlen(s+); int x=;
rep(i,,n){
int c=s[i]-'A';
while (x!= && (!son[x][c] || s[i]!=s[i-len[x]-])) x=fail[x];
if (son[x][c] && s[i]==s[i-len[x]-]) x=son[x][c],f[x]++;
}
for (int i=nd; i; i--) f[fail[i]]+=f[i],sz[fail[i]]+=sz[i];
rep(i,,nd) ans+=1ll*f[i]*sz[i];
printf("%lld\n",ans);
return ;
}

Aizu2292

[BZOJ2342][SHOI2011]双倍回文

就是求后半段也为回文串的回文串个数,在fail树上DFS并维护每个长度的回文串个数即可。

或者考虑求half[i]表示节点i的最深祖先满足len[half[i]]<=len[i]/2,这个同样可以在建树的时候求得。

 #include<cstdio>
#include<algorithm>
#define rep(i,l,r) for (int i=(l); i<=(r); i++)
typedef long long ll;
using namespace std; const int N=;
char s[N];
int n,ans,lst,nd=,fail[N],son[N][],len[N],half[N]; void ext(int c,int n,char s[]){
int p=lst;
while (s[n-len[p]-]!=s[n]) p=fail[p];
if (!son[p][c]){
int np=++nd,q=fail[p];
while (s[n-len[q]-]!=s[n]) q=fail[q];
len[np]=len[p]+; fail[np]=son[q][c]; son[p][c]=np;
if (len[np]==) half[np]=;
else{
int pos=half[p];
while (s[n-len[pos]-]!=s[n] || (len[pos]+)*>len[np]) pos=fail[pos];
half[np]=son[pos][c];
}
}
lst=son[p][c];
} int main(){
freopen("bzoj2342.in","r",stdin);
freopen("bzoj2342.out","w",stdout);
scanf("%d%s",&n,s+); len[]=-; fail[]=fail[]=;
rep(i,,n) ext(s[i]-'a',i,s);
rep(i,,nd) if (len[half[i]]*==len[i] && len[i]%==) ans=max(ans,len[i]);
printf("%d\n",ans);
return ;
}

BZOJ2342