对回文自动机/回文树的浅薄理解

由于我现在会不是非常理解回文自动机的原理,我们先详谈回文自动机的功能和使用,浅谈一下原理。
回文自动机的功能有:
1.求以下标为i的字符为右端点的回文串的个数。
2.求以下标为i的字符为右端点的最长回文串的长度。
3.求以下标为i的字符为右端点的最长回文串在整个串中出现的次数
4.求前缀字符串中的本质不同的回文串种类
学习链接:Palindromic Tree——回文树【处理一类回文串问题的强力工具】

len[i]:节点i的回文串的长度
next[i][c]:节点i的回文串在两边添加字符c以后变成的回文串的编号(和字典树的next指针类似)
fail[i]:类似于AC自动机的fail指针,指向失配后需要跳转到的节点(即为i的最长回文后缀且不为i)
cnt[i]:在最后统计后它可以表示形如以i节点为结尾的回文串中最长的那个串个数/节点i表示的回文串在S中出现的次数(建树时求出的不是完全的,count()加上子节点以后才是正确的)
num[i]:表示以i结尾的回文串的种类数/表示以节点i回文串的末尾字符结尾的但不包含本条路径上的回文串的数目。(也就是fail指针路径的深度)
last:指向以字符串中上一个位置结尾的回文串 cur:
指向由next构成的树中当前回文串的父亲节点(即当前回文串是cur左右两边各拓展一个字符得来) p:在树上添加的节点个数
S[i]:第i次添加的字符 n:将字符串添加的字符个数
除了S[i]表示字符串上的节点外,其他都表示树上的节点。例如abacdaba后面的a,b,ab,ba,aba在树上的节点和前面a,b,ab,ba,aba共用,因为它不构成新的回文串,但是会在前面a,b,ab,ba,aba节点的cnt做上记数

 	int fail[maxn];//fail指针,失配后跳转到fail指针指向的节点
    int trie[maxn][N];
    int cnt[maxn];//表示以节点i结尾的最长的本质不同的串的个数(建树时求出的不是完全的,最后count()函数跑一遍以后是整个串的)
    int num[maxn];//表示以节点i表示的最长回文串的最右端点为回文串结尾的回文串个数。
    int len[maxn];//节点i表示的回文串的长度
    int S[maxn];//存放添加的字符
    int last,p,n;
    int newnode(int l){//新建节点
        for(int i=0;i<N;++i){
            trie[p][i]=0;
        }
        cnt[p]=num[p]=0;
        len[p]=l;
        return p++;
    }

未添加字符时,回文树有两个节点,一个偶根节点0用来表示偶回文串len长度为0,奇根节点1表示奇回文串len长度为-1。回文树的每个节点表示一个回文串,他们的fail指针互指。

void init(){//初始化
        p=0;
        newnode(0);
        newnode(-1);
        last=0;
        n=0;
        S[n]=-1;
        fail[0]=1;
    }

回文树也有fail指针,它指向当前节点最长回文后缀。

int getFail(int x){//找fail指针
        while(S[n-len[x]-1]!=S[n]) x=fail[x];
        return x;
    }

当添加一个字符c时要向两边同时扩展一个c,那么就需要找到:c(i-len-1)-回文串cur(长度为len)-c(i)。由于我们令节点1的len为-1,所以找到节点1时一定能找到fail,因为一定有S[n-1-1]==S[n],即最坏情况下len=1。找到cur后,就在cur两边扩展了。如果这个回文串没出现过,说明出现了一个新的本质不同的回文串,需要创建一个新节点,并顺着父节点的fail指针找到它的最长回文后缀节点,将fail指向这个最长回文后缀。注意先找fail,再连接到父节点上,因为fail不能连接到自身。

void addWord(char c){
        c-='a';
        S[++n]=c;
        int cur=getFail(last);//找到可以和新建节点连接的cur节点
        if (!trie[cur][c]){//如果这个回文串没有出现过,说明出现了一个新的本质不同的回文串
            int now=newnode(len[cur]+2);
            fail[now]=trie[getFail(fail[cur])][c];//再父节点上找fail指针
            trie[cur][c]=now;//先找fail再连接防止连接到自身
            num[now]=num[fail[now]]+1;
        }
        last=trie[cur][c];
        cnt[last]++;
    }

统计本质相同的回文串的出现次数,父节点短,子节点长,一个子节点一定包含多个与父节点一样的回文串,因为回文串前缀与后缀相同

void Count(){//统计本质相同的回文串的出现次数
        for(int i=p-1;i>=0;--i){//逆序累加,保证每个点都会比它的父亲节点先算完,于是父亲节点能加到所有子孙
            cnt[fail[i]]+=cnt[i];
        }
    }

假设现在我们有串S = abbaabba。可以构造这样一个回文树:
在这里插入图片描述
最后还要明确一点,方便做题时开内存:对于任何一个串S,它的本质不同的回文串的个数不会超过|S|个,所以添加的节点数不会超过|S|
还有是:当添加的一个节点的最长回文串已经出现,那么不会再树上添加新的节点了。
完整模板

#include<bits/stdc++.h>
using namespace std;
const int maxn=300010;
const int N=26;
struct palindromeAutomata{
    int fail[maxn];//fail指针,失配后跳转到fail指针指向的节点
    int trie[maxn][N];
    int cnt[maxn];//表示以节点i结尾的最长的本质不同的串的个数(建树时求出的不是完全的,最后count()函数跑一遍以后是整个串的)
    int num[maxn];//表示以节点i表示的最长回文串的最右端点为回文串结尾的回文串个数。
    int len[maxn];//节点i表示的回文串的长度
    int S[maxn];//存放添加的字符
    int last,p,n;
    int newnode(int l){//新建节点
        for(int i=0;i<N;++i){
            trie[p][i]=0;
        }
        cnt[p]=num[p]=0;
        len[p]=l;
        return p++;
    }
    void init(){//初始化
        p=0;
        newnode(0);
        newnode(-1);
        last=0;
        n=0;
        S[n]=-1;
        fail[0]=1;
    }
    int getFail(int x){//找fail指针
        while(S[n-len[x]-1]!=S[n]) x=fail[x];
        return x;
    }
    void addWord(char c){
        c-='a';
        S[++n]=c;
        int cur=getFail(last);//找到可以和新建节点连接的cur节点
        if (!trie[cur][c]){//如果这个回文串没有出现过,说明出现了一个新的本质不同的回文串
            int now=newnode(len[cur]+2);
            fail[now]=trie[getFail(fail[cur])][c];//再父节点上找fail指针
            trie[cur][c]=now;//先找fail再连接防止连接到自身
            num[now]=num[fail[now]]+1;
        }
        last=trie[cur][c];
        cnt[last]++;
    }
    void Count(){//统计本质相同的回文串的出现次数
        for(int i=p-1;i>=0;--i){//逆序累加,保证每个点都会比它的父亲节点先算完,于是父亲节点能加到所有子孙
            cnt[fail[i]]+=cnt[i];
        }
    }
}Run;
char s[300010];
int main(){
    scanf("%s",&s);
    Run.init();
    for(int i=0;s[i];++i){
        Run.addWord(s[i]);
    }
    Run.Count();
}

例题:
回文串

题意:求最大的两个不相交非空回文串长度之和。
做法:用回文自动机对字符串正反计算一遍字符串每个下标的前缀串的最长回文长度,注意不相交和非空。
求前缀串最长回文长度要在每次加点的过程求,last为新添加的节点,len[last]的长度代表新回文串的长度。

#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+10;
const int N=26;
struct palindromeAutomata{
    int fail[maxn];//fail指针,失配后跳转到fail指针指向的节点
    int trie[maxn][N];
    int cnt[maxn];//表示以节点i结尾的最长的本质不同的串的个数(建树时求出的不是完全的,最后count()函数跑一遍以后是整个串的)
    int num[maxn];//表示以节点i表示的最长回文串的最右端点为回文串结尾的回文串个数。
    int len[maxn];//节点i表示的回文串的长度
    int S[maxn];//存放添加的字符
    int last,p,n;
    int newnode(int l){//新建节点
        for(int i=0;i<N;++i){
            trie[p][i]=0;
        }
        cnt[p]=num[p]=0;
        len[p]=l;
        return p++;
    }
    void init(){//初始化
        p=0;
        newnode(0);
        newnode(-1);
        last=0;
        n=0;
        S[n]=-1;
        fail[0]=1;
    }
    int getFail(int x){//找fail指针
        while(S[n-len[x]-1]!=S[n]) x=fail[x];
        return x;
    }
    void addWord(char c,int *a){
        c-='a';
        S[++n]=c;
        int cur=getFail(last);//找到可以和新建节点连接的cur节点
        if (!trie[cur][c]){//如果这个回文串没有出现过,说明出现了一个新的本质不同的回文串
            int now=newnode(len[cur]+2);
            fail[now]=trie[getFail(fail[cur])][c];//再父节点上找fail指针
            trie[cur][c]=now;//先找fail再连接防止连接到自身
            num[now]=num[fail[now]]+1;
        }
        last=trie[cur][c];
        a[n]=max(a[n-1],len[last]);
    }
}Run1,Run2;
char s[maxn];
int pre[maxn],hei[maxn];
int main(){
    scanf("%s",s);
    Run1.init();
    int ls=strlen(s);
    for(int i=0;i<ls;++i){
        Run1.addWord(s[i],pre);
    }
    Run2.init();
    for(int i=ls-1;i>=0;--i){
        Run2.addWord(s[i],hei);
    }
    int sum=0;
    for(int i=1;i<=ls;++i){
        if(pre[i]==0||hei[ls-i]==0) continue;
        sum=max(pre[i]+hei[ls-i],sum);
    }
    printf("%d",sum);
    return 0;
}

2160: 拉拉队排练
题意:求最长的k个回文串(本质可以相同,但要求位置相同)的长度之积。
做法:统计每个节点,记录出现次数和长度,即使:每种本质相同的字符串长度和出现次数。然后对不同本质的字符串按照长度排序。

#include<bits/stdc++.h>
using namespace std;
const int mod=19930726;
const int maxn=1e6+10;
const int N=26;
struct palindromeAutomata{
    int fail[maxn];//fail指针,失配后跳转到fail指针指向的节点
    int trie[maxn][N];
    int cnt[maxn];//表示以节点i结尾的最长的本质不同的串的个数(建树时求出的不是完全的,最后count()函数跑一遍以后是整个串的)
    int num[maxn];//表示以节点i表示的最长回文串的最右端点为回文串结尾的回文串个数。
    int len[maxn];//节点i表示的回文串的长度
    int S[maxn];//存放添加的字符
    int last,p,n;
    int newnode(int l){//新建节点
        for(int i=0;i<N;++i){
            trie[p][i]=0;
        }
        cnt[p]=num[p]=0;
        len[p]=l;
        return p++;
    }
    void init(){//初始化
        p=0;
        newnode(0);
        newnode(-1);
        last=0;
        n=0;
        S[n]=-1;
        fail[0]=1;
    }
    int getFail(int x){//找fail指针
        while(S[n-len[x]-1]!=S[n]) x=fail[x];
        return x;
    }
    void addWord(char c){
        c-='a';
        S[++n]=c;
        int cur=getFail(last);//找到可以和新建节点连接的cur节点
        if (!trie[cur][c]){//如果这个回文串没有出现过,说明出现了一个新的本质不同的回文串
            int now=newnode(len[cur]+2);
            fail[now]=trie[getFail(fail[cur])][c];//再父节点上找fail指针
            trie[cur][c]=now;//先找fail再连接防止连接到自身
            num[now]=num[fail[now]]+1;
        }
        last=trie[cur][c];
        cnt[last]++;
        cnt[last]%=mod;
    }
    void Count(){//统计本质相同的回文串的出现次数
        for(int i=p-1;i>=0;--i){//逆序累加,保证每个点都会比它的父亲节点先算完,于是父亲节点能加到所有子孙
            cnt[fail[i]]=(cnt[fail[i]]+cnt[i])%mod;
        }
    }
}Run;
char s[maxn];
priority_queue< pair<long long,long long> > pq;
long long quickpow(long long t,long long p){
    long long ans=1,tmp=t;
    while(p){
        if(p&1) ans=(ans*tmp)%mod;
        tmp=(tmp*tmp)%mod;
        p/=2;
    }   
    return ans;
}
int main(){
    long long n,k;
    scanf("%lld%lld",&n,&k);
    scanf("%s",&s);
    Run.init();
    for(int i=0;s[i];++i){
        Run.addWord(s[i]);
    }
    Run.Count();
    long long ans=0;
    long long numhw=0;
    for(int i=2;i<Run.p;++i){
        if(Run.len[i]%2==0) continue;
        pq.push(make_pair(Run.len[i],Run.cnt[i]));
        numhw+=Run.cnt[i];
    }
    if(numhw<k){
        puts("-1");
        return 0;
    }
    long long sum=1;
    long long tt=0;
    while(!pq.empty()){
        long long len=pq.top().first,num=pq.top().second;
        pq.pop();
        if(tt+num<=k){
            sum=(sum*quickpow(len,num))%mod;
        }
        else {
            sum=(sum*quickpow(len,k-tt))%mod;
        }
        tt+=num;
        if(tt>=k) break;
    }
    printf("%lld",sum);
    return 0;
}
发布了96 篇原创文章 · 获赞 11 · 访问量 2262

猜你喜欢

转载自blog.csdn.net/weixin_43769146/article/details/104011551