\(PAM\)
回文树(也叫回文自动机),是一个可以存储一个串中所有回文子串的高效数据结构,回文树由转移边与 \(fail\) 指针构成,每个节点保存一个回文子串。转移边类似 \(trie\) 上的边, 但又并非代表在原节点代表的回文串后加一个字符,而是在其前后各加一个字符;\(fail\) 指针指向的是当前节点所代表的回文子串的最长回文后缀所在的节点。我们还需要维护每个节点的 \(len\),同时根据需要还可以维护 \(cnt\) (当前节点所代表的回文子串的出现次数),\(trans\) 指针(指向小于等于当前节点所代表的回文串长度一半的最长回文后缀所在节点)。
回文树可以高效解决下列问题:
\(\bullet\; 1\) 求原串中本质不同的回文串个数
\(\bullet\; 2\) 求原串中本质不同的回文串出现次数
\(\bullet\; 3\) 最小回文划分
\(\bullet\; 4\) 原串中以 \(i\) 为结尾的最长回文子串
\(\bullet\; 5\) 一些有关回文串的 \(DP\) 问题
现在来讲一下回文树的具体实现
我们首先需要几样东西
\(trie[MAXN][26]\) 表示我们的转移边(当然你也可以用邻接表实现,邻接表的空间复杂度较低)。
\(fail[MAXN]\) 表示我们的 \(fail\) 指针。
\(len[MAXN]\) 表示我们当前节点所代表的回文子串的长度。
奇根,偶根。因为我们的回文串有偶回文串与奇回文串之分,我们就加入奇根,偶根两个虚根来方便我们操作。
其次我们采用增量法 (就是一个一个向自动机中增添字符)构建 \(PAM\) ,我们考虑如何将一个字符插入回文树。
我们发现我们插入时,其实是在回文树上找一个最长回文后缀,满足他之前一个字符与当前插入字符相同,我们设这个最长回文后缀所在的节点为 \(Fail\),那么我们相当于是将 \(Fail\) 所代表的回文串的前后各加一个当前字符组成了新的一个回文串,如果我们没有出现过这个回文串,我们就新建一个节点 \(x\),代表在 \(Fail\) 所代表的回文串前后新加一个当前字符所组成的回文串,而我们有转移边 \(trie[Fail][u]\)(\(u\) 为当前的字符,相当于这条边的边权为 \(u\)),所指向的节点即为我们当前新建的节点 \(x\);那么我们 \(len_x=len_{Fail}+2\),\(x\) 的 \(fail\) 指针通过接着向上找最长回文后缀满足他之前一个字符与当前插入字符的节点,这个节点即为 \(fail\) 指向的节点,具体实现如下:
int getfail(int x,int pos){
while(pos-len[x]-1<0||s[pos-len[x]-1]!=s[pos])x=fail[x];
return x;
}
void insert(int pos){
int u=s[pos]-'a';
int Fail=getfail(pre,pos);
if(!trie[Fail][u]){
len[++tot]=len[Fail]+2;
fail[tot]=trie[getfail(fail[Fail],pos)][u];
trie[Fail][u]=tot;
}
pre=trie[Fail][u];
}
当然,我们需要注意边界条件,即为 \(fail_{even}=odd,len_{odd}=-1,tot=1\),也就是说,我们偶根向上的 \(fail\) 指针为奇根(因为奇根不可能失配),奇根的长度初始化为 \(-1\),方便以后的处理。
那么我们就建好了一个回文自动机,他的时间复杂度很优秀,为 \(O(|S|)\),但空间复杂度很差,大约为 \(O(C|S|)\) (\(C\) 为字符集大小),所以在内存小的时候通常考虑使用邻接表存图。
关于一个字符串 \(aabaa\),他的回文自动机建好大概是长这样,有边权的为转移边,无边权的为 \(fail\) 指针。
我们讲一下实际应用。
首先通过回文自动机,我们可以找到一个字符串中所有的本质不同的回文子串,回文自动机保存的其实就是一个字符串所有的本质不同的回文子串。
其次,我们可以通过回文自动机方便的求出每一个回文子串的出现次数,和 \(AC\) 自动机不一样的是,\(AC\) 自动机的构建没有用到增量法,\(fail\) 指针无序,所以他的统计是需要通过拓扑实现的。而回文自动机先天要求 \(fail\) 有序,即由编号大的节点指向编号小的节点,那么我们就可以通过 \(for\) 循环方便快捷的求出每一个回文子串出现的次数。
for(int i=tot;i>=2;i--){
cnt[fail[i]]+=cnt[i];
}
通常我们还需要找到一个 \(trans\) 指针,指向长度小于等于当前节点所代表的字符串的最长回文后缀的节点,我们下面会讲几道例题来学习一下。
\(NO.1\)
这是道模板题,我们可以发现因为我们是用增量法构建回文树的,那么就支持动态求解。考虑对于以一个点结尾的回文串个数,其实就是他在 \(fail\) 树上跳的次数,那么他的个数就是其 \(fail\) 指针所指向节点的答案加 \(1\)。
\(code\)
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
int ans=0;
char s[N];
struct PAM{
int trie[N][26],fail[N],res[N],pre,tot,len[N];
inline void init(){fail[0]=1,len[1]=-1,tot=1;}
int getfail(int x,int pos){
while(pos-len[x]-1<0||s[pos-len[x]-1]!=s[pos])x=fail[x];
return x;
}
inline void insert(int u,int pos){
int Fail=getfail(pre,pos);
if(!trie[Fail][u]){
len[++tot]=len[Fail]+2;
fail[tot]=trie[getfail(fail[Fail],pos)][u];
res[tot]=res[fail[tot]]+1;
trie[Fail][u]=tot;
}
pre=trie[Fail][u];
ans=res[pre];
printf("%d ",ans);
}
}A;
int main(){
scanf("%s",s+1);
int len=strlen(s+1);
A.init();
for(int i=1;i<=len;i++){
int t=(s[i]-97+ans)%26;
s[i]=t+97;
A.insert(t,i);
}
return 0;
}
\(NO.2\)
这题显然比板子还板子,我们发现我们需要统计的就是我们上文提到的 \(cnt\) 数组与 \(len\) 数组的乘积最大,那么我们可以通过回文自动机快速求解。
\(code\)
#include<bits/stdc++.h>
using namespace std;
const int N=3e5+10;
long long ans=0;
struct PAM{
int trie[N][26],cnt[N],fail[N],len[N],tot,pre,n;
char s[N];
void init(){
fail[0]=1,len[1]=-1,tot=1;
scanf("%s",s+1);
n=strlen(s+1);
}
int getfail(int x,int pos){
while(pos-len[x]-1<0||s[pos-len[x]-1]!=s[pos])x=fail[x];
return x;
}
void insert(int pos){
int u=s[pos]-'a';
int Fail=getfail(pre,pos);
if(!trie[Fail][u]){
len[++tot]=len[Fail]+2;
fail[tot]=trie[getfail(fail[Fail],pos)][u];
trie[Fail][u]=tot;
}
pre=trie[Fail][u];
cnt[pre]++;
}
void Insert(){
for(int i=1;i<=n;i++)insert(i);
}
void print(){
for(int i=tot;i>=2;i--){
cnt[fail[i]]+=cnt[i];
ans=max(ans,1ll*len[i]*cnt[i]);
}
printf("%lld\n",ans);
}
}A;
int main(){A.init(),A.Insert(),A.print();return 0;}
\(NO.3\)
这道题让我们求出两个字符串的最长公共回文串的长度及个数,我们可以关于每个字符串建一个回文自动机,然后同时在两个回文自动机上 \(DFS\),找出公共的最长的字符串个数及长度即可。
注意回文自动机有两个根,一个奇根,一个偶根,注意两个根都得遍历。
\(code\)
#include<bits/stdc++.h>
using namespace std;
const int N=3e5+10;
int tong[N];
inline int read(){int x=0,f=0;char ch;while(!isdigit(ch=getchar()))if(ch=='-')f=1;do{x=(x<<1)+(x<<3)+(ch^48);}while(isdigit(ch=getchar()));return f?-x:x;}
struct PAM{
int trie[N][26],fail[N],len[N],pre,tot,n;
char s[N];
void init(){
fail[0]=1,len[1]=-1,tot=1;
scanf("%s",s+1);
n=strlen(s+1);
}
int getfail(int x,int pos){
while(pos-len[x]-1<0||s[pos-len[x]-1]!=s[pos])x=fail[x];
return x;
}
void insert(int pos){
int u=s[pos]-'a';
int Fail=getfail(pre,pos);
if(!trie[Fail][u]){
len[++tot]=len[Fail]+2;
fail[tot]=trie[getfail(fail[Fail],pos)][u];
trie[Fail][u]=tot;
}
pre=trie[Fail][u];
}
void Insert(){
for(int i=1;i<=n;i++)insert(i);
}
}A,B;
void dfs(int x,int y){
if(x+y>2)tong[A.len[x]]++;
for(int i=0;i<26;i++){
if(A.trie[x][i]&&B.trie[y][i]){
dfs(A.trie[x][i],B.trie[y][i]);
}
}
}
int main(){
int lim=(read(),read());
A.init(),A.Insert();
B.init(),B.Insert();
dfs(0,0),dfs(1,1);
for(int i=lim;i>=1;i--){
if(tong[i]){
printf("%d %d\n",i,tong[i]);
return 0;
}
}
return 0;
}
\(NO.4\)
这个题与上面两道很相似,做法就是同时在两个回文自动机上 \(DFS\),并统计答案即可。
\(code\)
#include<bits/stdc++.h>
using namespace std;
const int N=5e4+10;
long long ans=0;
struct PAM{
int trie[N][26],fail[N],tot,pre,n,cnt[N],len[N];
char s[N];
void init(){
fail[0]=1,len[0]=0,len[1]=-1,tot=1;
scanf("%s",s+1);
n=strlen(s+1);
}
int getfail(int x,int pos){
while(pos-len[x]-1<0||s[pos-len[x]-1]!=s[pos])x=fail[x];
return x;
}
void insert(int pos){
int u=s[pos]-'A';
int Fail=getfail(pre,pos);
if(!trie[Fail][u]){
len[++tot]=len[Fail]+2;
fail[tot]=trie[getfail(fail[Fail],pos)][u];
trie[Fail][u]=tot;
}
pre=trie[Fail][u];
cnt[pre]++;
}
void Insert(){for(int i=1;i<=n;i++)insert(i);}
void get(){for(int i=tot;i;i--)cnt[fail[i]]+=cnt[i];}
}A,B;
void dfs(int x,int y){
if(x+y>2)ans+=1ll*A.cnt[x]*B.cnt[y];
for(int i=0;i<26;i++){
if(A.trie[x][i]&&B.trie[y][i]){
dfs(A.trie[x][i],B.trie[y][i]);
}
}
}
int main(){
A.init(),A.Insert(),A.get();
B.init(),B.Insert(),B.get();
dfs(1,1);dfs(0,0);
printf("%lld\n",ans);
return 0;
}
\(No.5\)
这个题很好地运用了 \(trans\) 指针,我们对于一个字符串,他的 \(trans\) 指向的节点所对应的字符串的长度为他的一半,且 \(trans\) 指针所对应的字符串是一个偶回文串时,这个回文串就是一个双倍回文,求解 \(trans\) 指针后判断即可。
\(code\)
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
struct PAM{
int trie[N][26],fail[N],trans[N],tot,pre,n,len[N];
char s[N];
void init(){
fail[0]=1,len[1]=-1,tot=1;
scanf("%d%s",&n,s+1);
}
int getfail(int x,int pos){
while(pos-len[x]-1<0||s[pos-len[x]-1]!=s[pos])x=fail[x];
return x;
}
int gettrans(int x,int pos){
while(((len[x]+2)<<1)>len[tot]||s[pos-len[x]-1]!=s[pos])x=fail[x];
return x;
}
void insert(int pos){
int u=s[pos]-'a';
int Fail=getfail(pre,pos);
if(!trie[Fail][u]){
len[++tot]=len[Fail]+2;
fail[tot]=trie[getfail(fail[Fail],pos)][u];
trie[Fail][u]=tot;
if(len[tot]<=2)trans[tot]=fail[tot];
else{
int Trans=gettrans(trans[Fail],pos);
trans[tot]=trie[Trans][u];
}
}
pre=trie[Fail][u];
}
void Insert(){for(int i=1;i<=n;i++)insert(i);}
void print(){
long long ans=0;
for(int i=1;i<=tot;i++){
if(len[trans[i]]%2==0&&len[trans[i]]*2==len[i])ans=max(ans,1ll*len[i]);
}
printf("%lld\n",ans);
}
}A;
int main(){A.init(),A.Insert(),A.print();return 0;}
\(No.6\)
一道 \(DP\),我们可以发现最后的串一定是由一个偶回文串加上若干零散字符构成的(因为这样用到了 \(2\) 操作,而用 \(2\) 操作一定比用若干个 \(1\) 操作更优),那么我们考虑对整个串建出回文自动机,那么我们设 \(dp_i\) 为拼出 \(i\) 这个回文串需要的最少步数,那么我们考虑转移。
首先,我们设 \(i\) 为 \(j\) 在回文树上的儿子,那么我们有转移
\[dp_i=min(dp_j+1) \]因为我们可以通过先拼出 \(j\) 的一半,再加一个新的字符,构成 \(i\) 的一半,再用 \(2\) 操作拼出 \(i\),即为 \(dp_i=(dp_j-1)+1+1\)
其次,我们设 \(p\) 为 \(i\) 的 \(trans\) 指针,那么我们有转移
\[dp_i=min(dp_p+len_i/2-len_p+1) \]意味我们可以通过他的 \(trans\) 指针所指向的回文串,再在上面加上 \(len_i/2-len_p\) 个字符,再用一次 \(2\) 操作拼出 \(i\)。
\(code\)
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
struct PAM{
int fail[N],trie[N][4],trans[N],len[N],dp[N],q[N],l,r,tot,pre,n,ans;
char s[N];
int change(char c){
if(c=='A')return 0;
else if(c=='T')return 1;
else if(c=='C')return 2;
return 3;
}
void init(){
fail[0]=1,len[1]=-1,tot=1,pre=0,l=1,r=0;
scanf("%s",s+1);
n=strlen(s+1);
ans=n;
}
int getfail(int x,int pos){
while(pos-len[x]-1<0||s[pos-len[x]-1]!=s[pos])x=fail[x];
return x;
}
int gettrans(int x,int pos){
while(((len[x]+2)<<1)>len[tot]||s[pos-len[x]-1]!=s[pos])x=fail[x];
return x;
}
void insert(int pos){
int u=change(s[pos]);
int Fail=getfail(pre,pos);
if(!trie[Fail][u]){
len[++tot]=len[Fail]+2;
fail[tot]=trie[getfail(fail[Fail],pos)][u];
trie[Fail][u]=tot;
if(len[tot]<=2)trans[tot]=fail[tot];
else{
int Trans=gettrans(trans[Fail],pos);
trans[tot]=trie[Trans][u];
}
}
pre=trie[Fail][u];
}
void Insert(){for(int i=1;i<=n;i++)insert(i);}
void getans(){
for(int i=2;i<=tot;i++)dp[i]=len[i];
l=1,r=0;
q[++r]=0,dp[0]=1;
while(l<=r){
int cc=q[l++];
dp[cc]=min(dp[cc],dp[trans[cc]]+len[cc]/2-len[trans[cc]]+1);
ans=min(ans,dp[cc]+n-len[cc]);
for(int i=0;i<4;i++){
int v=trie[cc][i];
if(!v)continue;
dp[v]=min(dp[v],dp[cc]+1);
q[++r]=v;
}
}
printf("%d\n",ans);
for(int i=0;i<=tot;i++)for(int j=0;j<4;j++)trie[i][j]=0;
}
}A;
int main(){
int T=0;
scanf("%d",&T);
while(T--)A.init(),A.Insert(),A.getans();
return 0;
}
标签:tot,int,pos,len,学习,笔记,fail,PAM,回文
From: https://www.cnblogs.com/hxqasd/p/16894663.html