zoukankan      html  css  js  c++  java
  • 回文树&后缀自动机&后缀数组

    • KMP,扩展KMP和Manacher就不写了,感觉没多大意思。
       
    • 之前感觉后缀自动机简直可以解决一切,所以不怎么写后缀数组。
       
    • 马拉车主要是通过对称中心解决问题,有的时候要通过回文串的边界解决问题,这个时候回文树就用到了,又好写,又强大。
       
    • 之前整理过一篇后缀自动机的。感觉还要整理一下,顺便把回文树,后缀数组也整理一下
    • 很久没写专题了,如果有空,把回文自动机也学习一下。

    一:回文树

    前置技能: 马拉车,KMP。 其实也没关系,就是对比一下。

                       马拉车是利用对称的思想,得到每个点或者空格为对称中心的最长回文。

                       KMP有失配指针,回文树也有,指向失配后最长的回文串。

    前置理论: 一个字符串的本质不同的回文串最多有|S|个。所以,回文树的空间和时间是线性的。

    保存信息:我们把每个回文串对应到一个节点里。

         struct node{
               int len,num,fail,son[26],dep;  
         }t[maxn];

                  其中,len是回文串的长度;num是出现次数;son是儿子指针,用于匹配,看是否能增加长度;dep是指这个回文串失配次数,即以它的最后一个字符为尾的回文串个数; fail是失配指针;

    功能:我们可以得到所有的回文串; 所有本质不同的回文串; 回文串的出现次数; 以某个位置结尾的回文串个数;

    实现:首先,初始化两个节点1号和0号,分别表示奇数偶数长度的回文串。 他们都指向1号节点,而1号节点的长度设置尾-1,这样的话,确保每个位置结尾的回文串长度至少是-1+2=1;

        void init()
        {
            tot=last=1;
            t[0].len=0; t[1].len=-1;
            t[0].fail=t[1].fail=1;
        }

    完整代码:

    struct PAT
    {
        struct node{
            int len,num,fail,son[26];
        }t[maxn];
        int last,n,tot,s[maxn];
        void init()
        {
            memset(t,0,sizeof(t));
            tot=last=1; n=0;
            t[0].len=0; t[1].len=-1;
            t[0].fail=t[1].fail=1;
            s[0]=-1;
        }
        int add(int c){
            int p=last; s[++n]=c;
            while(s[n]!=s[n-1-t[p].len]) p=t[p].fail;
            if(!t[p].son[c]){
                int v=++tot,k=t[p].fail;
                while(s[n]!=s[n-t[k].len-1]) k=t[k].fail;
                t[v].fail=t[k].son[c]; 
                t[v].len=t[p].len+2;
                t[v].num=t[t[v].fail].num+1;
                t[p].son[c]=v;
            }
            last=t[p].son[c];
            return t[last].num;
        }
    }T;

    例题一:HDU5658:CA Loves Palindromic

    题意:给定字符串S,|S|<1000;Q次询问区间不用本质的回文串数量。

    思路:以每个左端点建立回文树即可。

    #include<bits/stdc++.h>
    #define rep(i,a,b) for(int i=a;i<=b;i++)
    using namespace std;
    const int maxn=1010;
    char c[maxn]; int bb[maxn];
    int N,Q,fcy[maxn][maxn];
    struct PT
    {
        struct node{
            int fail,len,son[26];
        }t[maxn];
        int tot,last;
        void init()
        {
            memset(t,0,sizeof(t));
            t[0].fail=t[1].fail=1;
            t[1].len=-1;
            last=1; tot=1; bb[0]=-1; bb[1]=-2;
        }
        void add(int s,int n)
        {
            int p=last; bb[n]=s;
            while(bb[n-t[p].len-1]!=bb[n]) p=t[p].fail;
            if(!t[p].son[s])
            {
                int v=++tot,k=t[p].fail;
                t[v].len=t[p].len+2;
                while(bb[n-t[k].len-1]!=bb[n]) k=t[k].fail;
                t[v].fail=t[k].son[s];
                t[p].son[s]=v;
            }
            last=t[p].son[s];
        }
    }T;
    void solve()
    {
        rep(i,1,N){
            T.init();
            rep(j,i,N) {
                T.add(c[j]-'a',j-i+1);
                fcy[i][j]=T.tot-1;
            }
        }
        scanf("%d",&Q);
        rep(i,1,Q){
           int L,R; scanf("%d%d",&L,&R);
           printf("%d
    ",fcy[L][R]);
        }
    }
    int main()
    {
        int T;scanf("%d",&T);
        while(T--){
            memset(c,0,sizeof(c));
            scanf("%s",c+1);
            N=strlen(c+1);
            solve();
        }
        return 0;
    }
    View Code

    例题二:HDU - 5157 :Harry and magic string

    题意: 多组输入,每次给定字符串S(|S|<1e5),求多少对不相交的回文串。

    思路:可以用回文树求出以每个位置结尾的回文串数,那么累加得到前缀和; 倒着再做一遍得到每个位置为开头的回文串数,乘正向求出的前缀和即可。

    #include<bits/stdc++.h>
    #define ll long long
    #define rep(i,a,b) for(int i=a;i<=b;i++)
    #define rep2(i,a,b) for(int i=a;i>=b;i--)
    using namespace std;
    const int maxn=100010;
    struct PAT
    {
        struct node{
            int len,num,fail,son[26];
        }t[maxn];
        int last,n,tot,s[maxn];
        void init()
        {
            memset(t,0,sizeof(t));
            tot=last=1; n=0;
            t[0].len=0; t[1].len=-1;
            t[0].fail=t[1].fail=1;
            s[0]=-1;
        }
        int add(int c){
            int p=last; s[++n]=c;
            while(s[n]!=s[n-1-t[p].len]) p=t[p].fail;
            if(!t[p].son[c]){
                int v=++tot,k=t[p].fail;
                while(s[n]!=s[n-t[k].len-1]) k=t[k].fail;
                t[v].fail=t[k].son[c];
                t[v].len=t[p].len+2;
                t[v].num=t[t[v].fail].num+1;
                t[p].son[c]=v;
            }
            last=t[p].son[c];
            return t[last].num;
        }
    }T;
    ll ans,sum[maxn];char c[maxn];
    int main()
    {
        while(~scanf("%s",c+1)){
            int N=strlen(c+1);
            T.init(); ans=0;
            rep(i,1,N) sum[i]=sum[i-1]+T.add(c[i]-'a');
            T.init();
            rep2(i,N,1) ans+=sum[i-1]*T.add(c[i]-'a');
            printf("%lld
    ",ans);
        }
        return 0;
    }
    View Code

    例题三:HDU - 5785:Interesting

    题意:累计i*k的和,如果[i,j],[j+1,k]都是回文串;

    思路:统计以j为结尾的回文串个数numj,以及他们的长度和addj; 以j+1为首....;那么这个位置的贡献就是(numj*(j+1)-addj)*(numj+1*j+addj+1);

    (此题要节约空间,所以不要开longlong的数组。

    #include<bits/stdc++.h>
    #define ll long long
    #define rep(i,a,b) for(int i=a;i<=b;i++)
    #define rep2(i,a,b) for(int i=a;i>=b;i--)
    using namespace std;
    const int maxn=1000002;
    const int Mod=1e9+7;
    struct PAT
    {
        struct node{
            int len,num,fail,son[26],add;
        }t[maxn];
        int last,n,tot,s[maxn];
        void init()
        {
            memset(t,0,sizeof(t));
            tot=last=1; n=0;
            t[0].len=0; t[1].len=-1;
            t[0].fail=t[1].fail=1;
            s[0]=-1;
        }
        void add(int c){
            int p=last; s[++n]=c;
            while(s[n]!=s[n-1-t[p].len]) p=t[p].fail;
            if(!t[p].son[c]){
                int v=++tot,k=t[p].fail;
                while(s[n]!=s[n-t[k].len-1]) k=t[k].fail;
                t[v].fail=t[k].son[c];
                t[v].len=t[p].len+2;
                t[v].num=t[t[v].fail].num+1;
                t[v].add=(t[t[v].fail].add+t[v].len)%Mod;
                t[p].son[c]=v;
            }
            last=t[p].son[c];
        }
    }T;
    int ans,sum[maxn];char c[maxn];
    int main()
    {
        while(~scanf("%s",c+1)){
            int N=strlen(c+1);
            T.init(); ans=0;
            rep(i,1,N) {
                T.add(c[i]-'a');
                sum[i]=(1LL*T.t[T.last].num*(i+1)-T.t[T.last].add)%Mod;
            }
            T.init();
            rep2(i,N,1){
                T.add(c[i]-'a');
                ans+=(ll)(T.t[T.last].add+1LL*T.t[T.last].num*(i-1)%Mod)*sum[i-1]%Mod;
                ans%=Mod;
            }
            printf("%d
    ",ans);
        }
        return 0;
    }
    View Code

    例题四:HDU - 5421:Victor and String

    题意:多组输入,开始字符串为空,支持4中操作: 1,在字符串首加字符; 2,在字符串尾加字符; 3,查询字符串不同本质的回文串个数; 4,查询回文串个数总和

    思路:因为支持首尾加入,所以和常规的回文树有些不同。 参考了YYB的博客。 发现首尾互相影响,当且仅当整个字符串是回文串。 其他情况两头正常加即可。

    也就是现在有两个last,除了完全对称,其他操作都一样。

    #include<bits/stdc++.h>
    #define ll long long
    #define rep(i,a,b) for(int i=a;i<=b;i++)
    #define rep2(i,a,b) for(int i=a;i>=b;i--)
    using namespace std;
    const int maxn=100010;
    struct PAT
    {
        struct node{
            int len,num,fail,son[26];
        }t[maxn];
        int n,tot,s[maxn<<1],L,R,suf,pre; ll ans;
        void init()
        {
            memset(t,0,sizeof(t));
            memset(s,-1,sizeof(s));
            tot=1; L=100000; R=L-1;
            suf=pre=0; ans=0;
            t[1].len=-1; t[0].fail=t[1].fail=1;
        }
        void add(int c,int n,int &last,int op){
            int p=last; s[n]=c;
            while(s[n]!=s[n-op-op*t[p].len]) p=t[p].fail;
            if(!t[p].son[c]){
                int v=++tot,k=t[p].fail;
                while(s[n]!=s[n-op*t[k].len-op]) k=t[k].fail;
                t[v].fail=t[k].son[c];
                t[v].len=t[p].len+2;
                t[v].num=t[t[v].fail].num+1;
                t[p].son[c]=v;
            }
            last=t[p].son[c]; ans+=t[last].num;
            if(t[last].len==R-L+1) suf=pre=last;
        }
    }T;
    int main()
    {
        int N;
        while(~scanf("%d",&N)){
            T.init(); int opt;
            rep(i,1,N){
                scanf("%d",&opt); char s[3];
                if(opt==1){
                    scanf("%s",s);
                    T.add(s[0]-'a',--T.L,T.pre,-1);
                }
                else if(opt==2){
                    scanf("%s",s);
                    T.add(s[0]-'a',++T.R,T.suf,1);
                }
                else if(opt==3)
                    printf("%d
    ",T.tot-1);
                else printf("%lld
    ",T.ans);
            }
        }
        return 0;
    }
    View Code

    例题五:Gym - 101981M:(南京) Mediocre String Problem

    题意:给字符串S和T,累计(i结尾的回文串个数)*(i+1开始匹配T的长度)。

    思路:第一部分用回文树,第二部分exkmp。

    #include<bits/stdc++.h>
    #define ll long long
    #define rep(i,a,b) for(int i=a;i<=b;i++)
    using namespace std;
    const int maxn=1000010;
    char S[maxn],T[maxn];
    struct PT
    {
        struct in{
            int dep,fail,len,son[26];
        }p[maxn];
        int cnt,last;
        void init()
        {
            //memset(p,0,sizeof(p));
            cnt=last=1;p[0].dep=p[1].dep=0;
            p[0].fail=p[1].fail=1;
            p[0].len=0; p[1].len=-1;
        }
        int add(int c,int n)
        {
            int np=last;
            while(S[n]!=S[n-1-p[np].len]) np=p[np].fail;
            if(!p[np].son[c]){
                int v=++cnt,k=p[np].fail; p[v].len=p[np].len+2;
                while(S[n]!=S[n-p[k].len-1]) k=p[k].fail;
                p[v].fail=p[k].son[c];
                p[np].son[c]=v; //这一句放前面会出现矛盾,因为np可能=k
                p[v].dep=p[p[v].fail].dep+1;
            }
            last=p[np].son[c];
            return p[last].dep;
        }
    }Tree;
    int N,M,num[maxn],Next[maxn],extand[maxn]; ll ans;
    void getnext(){// next[i]: 以第i位置开始的子串与T的公共前缀长度
         int i,length=strlen(T+1);
         Next[1]=length;
         for(i=0;i+1<length&&T[i+1]==T[i+2];i++);
         Next[2]=i;
         int a=2;   //!
         for(int k=3;k<=length;k++){//长度+1,位置-1。
              int p=a+Next[a]-1, L=Next[k-a+1];
              if(L>=p-k+1){
                  int j=(p-k+1)>0?(p-k+1):0;//中断后可能是负的
                  while(k+j<=length&&T[k+j]==T[j+1]) j++;// 枚举(p+1,length) 与(p-k+1,length) 区间比较
                  Next[k]=j, a=k;
              }
              else Next[k]=L;
        }
    }
    void getextand(){
        memset(Next,0,sizeof(Next));
        getnext();
        int Slen=strlen(S+1),Tlen=strlen(T+1),a=0;
        int MinLen=Slen>Tlen?Tlen:Slen;
        while(a<MinLen&&S[a+1]==T[a+1]) a++;
        extand[1]=a; a=1;
        for(int k=2;k<=Slen;k++){
            int p=a+extand[a]-1,L=Next[k-a+1];
            if(L>=p-k+1){
                int j=(p-k+1)>0?(p-k+1):0;
                while(k+j<=Slen&&j+1<=Tlen&&S[k+j]==T[j+1]) j++;
                extand[k]=j;a=k;
            }
            else extand[k]=L;
        }
    }
    int main()
    {
        scanf("%s%s",S+1,T+1);
        N=strlen(S+1); M=strlen(T+1);
        reverse(S+1,S+N+1); Tree.init();
        rep(i,1,N) num[i]=Tree.add(S[i]-'a',i);
        getextand();
        rep(i,1,N) ans+=(ll)num[i-1]*extand[i];
        printf("%lld
    ",ans);
        return 0;
    }
    View Code

    二:后缀自动机

    技能前置:由于本质不同的回文串最多|S|个,所以回文树的(时间和)空间是线性的。

                     后缀自动机也也差不多,不过空间开两倍。 但是并不是代表本质不同的字符串最多2|S|个,因为后缀自动机保存的是集合。

    保存信息:回文树保存的是回文串的信息,而后缀自动机保存的是集合的信息。 集合里最长的串长度为maxlen[i];集合大小为maxlen[i]-maxlen[fa[i]];

    而fa和ch数组的交叉复杂的DAG图,感觉初学有点难以理解的,原理还是自己去看吧。。。

    功能:对于单个字符串S。可以求,不同子串个数; 个子串的长度; 最小表示法; etc。

              对于多个串。 我们可以求最长公共子串; 以及各种匹配(匹配的长度需要记录,注意和集合最长长度maxlen区分);etc。

    构造:增量法,每次加入一个字符,由于一定会增加一个新串s[1,i],所以节点数cnt++,sz[cnt]=1,maxlen[cnt]=i;然后往上走,直到有冲突,或者遇到根 。

    1,遇到根(root=1),表示全是可以接纳的,那么fa[np]=1;

    2,一开始就遇到冲突的,即maxlen[q]==maxlen[p]+1,那么fa[np]=q即可。

    3,中途遇到冲突,由于每个集合保存的信息的一样的,那么需要分叉(新的nq节点继续跑)。

        int add(int x)
        {
           int np=++cnt,p=last; last=np;
           maxlen[np]=maxlen[p]+1; sz[np]=1;
           while(p&&!ch[p][x]) ch[p][x]=np,p=fa[p];
           if(!p) fa[np]=1;
           else {
             int q=ch[p][x];
             if(maxlen[q]==maxlen[p]+1) fa[np]=q;
             else {
                int nq=++cnt; maxlen[nq]=maxlen[p]+1;
                fa[nq]=fa[q]; fa[q]=fa[np]=nq;
                memcpy(ch[nq],ch[q],sizeof(ch[q]));
                while(p&&ch[p][x]==q) ch[p][x]=nq,p=fa[p];
             }
           }
        }

    topo排序:和SA一样,基数较小,我们可以基数排序。 得到每个集合里的串出现次数。

        void sort()
        {
           rep(i,1,cnt) a[i]=0;
           rep(i,1,cnt) a[maxlen[i]]++;
           rep(i,1,cnt) a[i]+=a[i-1];
           rep(i,1,cnt) b[a[maxlen[i]]--]=i;
           for(int i=cnt;i>=1;i--) sz[fa[b[i]]]+=sz[b[i]];
        }

    例题一:UVA - 719Glass Beads

    题意:给定字符串,求最小表示法的起始位置。 |S|<1e4

    思路:我们把字符串加倍,丢到自动机里面。 由于我加倍了,所以去匹配的时候可以保证长度不小于|S|,即从第一位到第|S|位,每次匹配最小的即可,保证得到的是最小的。

    #include<bits/stdc++.h>
    #define rep(i,a,b) for(int i=a;i<=b;i++)
    using namespace std;
    const int maxn=100010;
    char chr[maxn];int L;
    struct SAM
    {
        int fa[maxn],ch[maxn][26],maxlen[maxn],tot,last;
        void init()
        {
            tot=last=1; maxlen[1]=fa[1]=0;
            memset(ch[1],0,sizeof(ch[1]));
        }
        void add(int x)
        {
              int np=++tot,p=last;last=np;
              maxlen[np]=maxlen[p]+1;
              memset(ch[np],0,sizeof(ch[np]));
              while(p&&!ch[p][x]) ch[p][x]=np,p=fa[p];
              if(!p) fa[np]=1;
              else {
                    int q=ch[p][x];
                    if(maxlen[q]==maxlen[p]+1) fa[np]=q;
                    else {
                        int nq=++tot;
                        memcpy(ch[nq],ch[q],sizeof(ch[q]));
                        fa[nq]=fa[q],fa[np]=fa[q]=nq;
                        maxlen[nq]=maxlen[p]+1;
                        while(p&&ch[p][x]==q) ch[p][x]=nq,p=fa[p];
                    }
             }
        }
        void solve()
        {
            int Now=1;
            rep(i,0,L-1)
              rep(j,0,25){
                if(ch[Now][j]) {
                    Now=ch[Now][j];break;
                }
            }
            printf("%d
    ",maxlen[Now]-L+1);
        }
    }S;
    int main()
    {
        int n;
        while(~scanf("%d",&n)){
            while(n--){
               S.init();
               scanf("%s",chr);
               L=strlen(chr);
               rep(i,0,L-1) S.add(chr[i]-'a');
               rep(i,0,L-1) S.add(chr[i]-'a');
               S.solve();
            }
        }
        return 0;
    }
    View Code

    例题二:SPOJ - LCS Longest Common Substring

    题意:给定字符串S,T,求最长公共子串。|S|,|T|<25e4

    思路:SAM的基操,一个串建立SAM,一个串跑。

    注意这个跑的时候,其长度是需要记录的,因为自动机里面记录的长度是“集合”的最长长度,而我们匹配的时候是对应的集合中的一个。

    #include<bits/stdc++.h>
    #define rep(i,a,b) for(int i=a;i<=b;i++)
    using namespace std;
    const int maxn=500010;
    char c[maxn];
    struct SAM
    {
        int ch[maxn][26],fa[maxn],maxlen[maxn],cnt,last;
        void init()
        {
            cnt=1; last=1;
            memset(ch[1],0,sizeof(ch[1]));
        }
        void add(int x)
        {
           int np=++cnt,p=last;
           last=np; maxlen[cnt]=maxlen[p]+1;
           memset(ch[np],0,sizeof(ch[np]));
           while(p&&!ch[p][x])ch[p][x]=np,p=fa[p];
           if(!p) fa[np]=1;
           else {
             int q=ch[p][x];
             if(maxlen[q]==maxlen[p]+1) fa[np]=q;
             else {
                int nq=++cnt; maxlen[nq]=maxlen[p]+1;
                memcpy(ch[nq],ch[q],sizeof(ch[q]));
                fa[nq]=fa[q], fa[q]=fa[np]=nq;
                while(p&&ch[p][x]==q) ch[p][x]=nq,p=fa[p];
             }
          }
        }
        int match()
        {
           int np=1,len=0,ans=0,N;
           scanf("%s",c+1); N=strlen(c+1);
           rep(i,1,N){
              int x=c[i]-'a';
              while(np&&!ch[np][x]) np=fa[np],len=maxlen[np];
              if(!np) np=1,len=0;
              if(ch[np][x]) np=ch[np][x],len++;
              ans=max(ans,len);
           }
           return ans;
        }
    }T;
    int main()
    {
        int N,Q,K;
        scanf("%s",c+1); N=strlen(c+1);
        T.init();
        rep(i,1,N) T.add(c[i]-'a');
        printf("%d
    ",T.match());
        return 0;
    }
    View Code

    例题三:SPOJ - LCS2 Longest Common Substring II

    题意:给定最多N(N<10)个串,求最长公共字串。|每个串|<1e5。

    思路:可以和上一题一样,用一个建SAM,剩下的去匹配。

    这里也可以用另外的一种方法:广义后缀自动机。 这个东西表示,多个串,我丢到一个自动机里,这样的话会会节省很多空间。每个串丢进去之前把last设为1;

    而且更方便的是,我们可以一遍插入,一遍更新,如果一个点被更新了N次,则属于公共子串。  由于每个点最多被更新一次,所以复杂度是线性的。

    #include<bits/stdc++.h>
    #define rep(i,a,b) for(int i=a;i<=b;i++)
    using namespace std;
    const int maxn=2000010;
    char c[maxn]; int ch[maxn][26],fa[maxn],maxlen[maxn],cnt,last,T,ans;
    int vis[maxn],num[maxn];
    void insert(int x)
    {
        int np=++cnt,p=last;
        last=np; maxlen[cnt]=maxlen[p]+1;
        memset(ch[np],0,sizeof(ch[np]));
        while(p&&!ch[p][x])ch[p][x]=np,p=fa[p];
        if(!p) fa[np]=1;
        else {
            int q=ch[p][x];
            if(maxlen[q]==maxlen[p]+1) fa[np]=q;
            else {
                int nq=++cnt; maxlen[nq]=maxlen[p]+1;
                memcpy(ch[nq],ch[q],sizeof(ch[q]));
                fa[nq]=fa[q], fa[q]=fa[np]=nq; vis[nq]=vis[q]; num[nq]=num[q];
                while(p&&ch[p][x]==q) ch[p][x]=nq,p=fa[p];
            }
        }
        while(np&&vis[np]!=T) vis[np]=T,num[np]++,np=fa[np];
    }
    int main()
    {
        int N; cnt=1; memset(ch[1],0,sizeof(ch[1]));
        while(~scanf("%s",c+1)) {
            N=strlen(c+1); last=1; T++;
            rep(i,1,N) insert(c[i]-'a');
        }
        rep(i,2,cnt) if(num[i]==T) ans=max(ans,maxlen[i]);
        printf("%d
    ",ans);
        return 0;
    }
    View Code

    例题四:SPOJ - NSUBSTR Substrings

    题意:给定字符串S。对于i=1到|S|,输出出现最多次数的长度为i字串出现的次数。 |S|<25e4

    思路:先得到自动机,然后topo得到所有字串的出现次数。 注意一点就是,长度越短,对应的出现次数一定不减。我们可以从长到短依次更新即可。

    #include<bits/stdc++.h>
    #define rep(i,a,b) for(int i=a;i<=b;i++)
    using namespace std;
    const int maxn=500010;
    char c[maxn];int ans[maxn];
    struct SAM
    {
        int ch[maxn][26],fa[maxn],maxlen[maxn];
        int a[maxn],b[maxn],sz[maxn],cnt,last;
        void init()
        {
            cnt=last=1;
            memset(ch[0],0,sizeof(ch[0]));
        }
        void add(int x)
        {
          int np=++cnt,p=last; sz[np]=1;
          last=np; maxlen[cnt]=maxlen[p]+1;
          memset(ch[np],0,sizeof(ch[np]));
          while(p&&!ch[p][x])ch[p][x]=np,p=fa[p];
          if(!p) fa[np]=1;
          else {
            int q=ch[p][x];
            if(maxlen[q]==maxlen[p]+1) fa[np]=q;
            else {
                int nq=++cnt; maxlen[nq]=maxlen[p]+1;
                memcpy(ch[nq],ch[q],sizeof(ch[q]));
                fa[nq]=fa[q], fa[q]=fa[np]=nq;
                while(p&&ch[p][x]==q) ch[p][x]=nq,p=fa[p];
            }
          }
       }
       void sort()
       {
          rep(i,1,cnt) a[i]=0;
          rep(i,1,cnt) a[maxlen[i]]++;
          rep(i,1,cnt) a[i]+=a[i-1];
          rep(i,1,cnt) b[a[maxlen[i]]--]=i;
          for(int i=cnt;i>=1;i--) sz[fa[b[i]]]+=sz[b[i]];
          rep(i,1,cnt) ans[maxlen[i]]=max(ans[maxlen[i]],sz[i]);
          for(int i=cnt;i>=1;i--) ans[i]=max(ans[i],ans[i+1]);
       }
    }T;
    int main()
    {
        int N;
        scanf("%s",c+1); N=strlen(c+1);
        T.init();
        rep(i,1,N) T.add(c[i]-'a');
        T.sort();
        rep(i,1,N) printf("%d
    ",ans[i]);
        return 0;
    }
    View Code

    例题五:SPOJ - SUBLEXLexicographical Substring Search

    题意:给定字符串S,现在将所有不同字串按字典序排序后,询问字典序第K大的字串,Q次询问。 |S|<9e4,Q<500, K<2^31

    思路:最先的肯定的'a'开头的,然后是'b'开头的....,如果‘a’开头的大于等于K个,然后往‘a’的子树里走,即第一位是‘a’; 否则,K-=sum[a],继续看'b'....

    直到K<=0;   所以我们现在只要得到topo,倒序每个字符的子树大小即可。

    #include<bits/stdc++.h>
    #define rep(i,a,b) for(int i=a;i<=b;i++)
    using namespace std;
    const int maxn=200010;
    char c[maxn];
    struct SAM
    {
        int ch[maxn][26],fa[maxn],maxlen[maxn],cnt,last;
        int a[maxn],b[maxn],sz[maxn];
        void init()
        {
           cnt=1; last=1; memset(ch[1],0,sizeof(ch[1]));
        }
        void add(int x)
        {
           int np=++cnt,p=last; //sz[np]=1;
           last=np; maxlen[cnt]=maxlen[p]+1;
           memset(ch[np],0,sizeof(ch[np]));
           while(p&&!ch[p][x])ch[p][x]=np,p=fa[p];
           if(!p) fa[np]=1;
           else {
              int q=ch[p][x];
              if(maxlen[q]==maxlen[p]+1) fa[np]=q;
              else {
                 int nq=++cnt; maxlen[nq]=maxlen[p]+1;
                 memcpy(ch[nq],ch[q],sizeof(ch[q]));
                 fa[nq]=fa[q], fa[q]=fa[np]=nq;
                 while(p&&ch[p][x]==q) ch[p][x]=nq,p=fa[p];
              }
           }
        }
        void sort()
        {
           rep(i,1,cnt) sz[i]=1;//maxlen[i]-maxlen[fa[i]];
           rep(i,1,cnt) a[i]=0;
           rep(i,1,cnt) a[maxlen[i]]++;
           rep(i,1,cnt) a[i]+=a[i-1];
           rep(i,1,cnt) b[a[maxlen[i]]--]=i;
           for(int i=cnt;i>=1;i--){
             rep(j,0,25) if(ch[b[i]][j])
               sz[b[i]]+=sz[ch[b[i]][j]];
           }
       }
       void dfs(int np,int K)
       {
          if(np>1) K-=1; if(K<=0) return ;
          rep(i,0,25){
            if(!ch[np][i]) continue;
            if(sz[ch[np][i]]<K) K-=sz[ch[np][i]];
            else{
                putchar('a'+i);
                dfs(ch[np][i],K);
                return ;
            }
          }
       }
    }T;
    int main()
    {
        int N,Q,K;
        scanf("%s",c+1); N=strlen(c+1);
        T.init();
        rep(i,1,N) T.add(c[i]-'a');
        T.sort();
        scanf("%d",&Q);
        while(Q--){
            scanf("%d",&K);
            T.dfs(1,K);
            putchar('
    ');
        }
        return 0;
    }
    View Code

    例题六:HDU - 4622  Reincarnation

    题意:给定字符串S,Q次询问区间不同子串。|S|<1000; Q<10000;

    思路:左端点一样的一起搞就行了。 |S|次建立自动机,O(|S|^2);

    #include<bits/stdc++.h>
    #define rep(i,a,b) for(int i=a;i<=b;i++)
    using namespace std;
    const int maxn=2010;
    int ch[maxn<<1][26],fa[maxn<<1],ans[maxn*5],maxlen[maxn<<1],cnt,last,sum; char c[maxn];
    struct in{
        int L,R,id;
        bool friend operator <(in w,in v){
            if(w.L==v.L) return w.R<v.R;
            return w.L<v.L;
        }
    }s[maxn*5];
    void insert(int x)
    {
        int np=++cnt,p=last; last=np;
        maxlen[np]=maxlen[p]+1;
        memset(ch[np],0,sizeof(ch[np]));
        while(p&&!ch[p][x]) ch[p][x]=np,p=fa[p];
        if(!p) fa[np]=1;
        else{
            int q=ch[p][x];
            if(maxlen[q]==maxlen[p]+1) fa[np]=q;
            else {
                int nq=++cnt; maxlen[nq]=maxlen[p]+1;
                memcpy(ch[nq],ch[q],sizeof(ch[q]));
                fa[nq]=fa[q]; fa[q]=fa[np]=nq;
                while(p&&ch[p][x]==q) ch[p][x]=nq,p=fa[p];
            }
        } sum+=maxlen[np]-maxlen[fa[np]];
    }
    int main()
    {
        int T,N,Q;
        scanf("%d",&T);
        while(T--){
            scanf("%s",c+1); N=strlen(c+1);
            scanf("%d",&Q);
            rep(i,1,Q) scanf("%d%d",&s[i].L,&s[i].R),s[i].id=i;
            sort(s+1,s+Q+1);
            rep(i,1,Q){
                last=1; cnt=1; sum=0;
                memset(ch[1],0,sizeof(ch[1]));
                int pos=i;
                rep(j,s[i].L,N){
                    insert(c[j]-'a');
                    while(s[pos].L==s[i].L&&j==s[pos].R&&pos<=Q) ans[s[pos].id]=sum,pos++;
                }
                i=pos-1;
            }
            rep(i,1,Q) printf("%d
    ",ans[i]);
        }
        return 0;
    }
    View Code

    例题七:HDU - 4436str2int

    题意:给定N个数字串,求所有不同子串10进制下的和,结果膜2012。

    思路:建立广义后缀自动机,然后从根节点,顺序遍历所有节点即可,num为到达节点的个数,val为节点的前缀和。

    #include<bits/stdc++.h>
    #define rep(i,a,b) for(int i=a;i<=b;i++)
    using namespace std;
    const int maxn=200010;
    const int Mod=2012;
    char c[maxn];
    struct SAM{
        int ch[maxn][10],fa[maxn],maxlen[maxn],cnt,last;
        int a[maxn],b[maxn],ans,num[maxn],val[maxn];
        void init()
        {
            cnt=1; memset(ch[1],0,sizeof(ch[1]));
        }
        int add(int x)
        {
           int np=++cnt,p=last; last=np;
           maxlen[np]=maxlen[p]+1;memset(ch[np],0,sizeof(ch[np]));
           while(p&&!ch[p][x]) ch[p][x]=np,p=fa[p];
           if(!p) fa[np]=1;
           else {
             int q=ch[p][x];
             if(maxlen[q]==maxlen[p]+1) fa[np]=q;
             else {
                int nq=++cnt; maxlen[nq]=maxlen[p]+1;
                fa[nq]=fa[q]; fa[q]=fa[np]=nq;
                memcpy(ch[nq],ch[q],sizeof(ch[q]));
                while(p&&ch[p][x]==q) ch[p][x]=nq,p=fa[p];
             }
           }
        }
       void sort()
       {
         rep(i,1,cnt) a[i]=0;
         rep(i,1,cnt) a[maxlen[i]]++;
         rep(i,1,cnt) a[i]+=a[i-1];
         rep(i,1,cnt) b[a[maxlen[i]]--]=i;
       }
       void solve()
       {
          sort();
          rep(i,1,cnt) num[i]=val[i]=0;
          num[1]=1; ans=0;
          rep(i,1,cnt){
             int p=b[i];
             rep(j,0,9){
                if((i==1&&j==0)||!ch[p][j]) continue;
                (num[ch[p][j]]+=num[p])%=Mod;
                (val[ch[p][j]]+=val[p]*10+num[p]*j)%=Mod;
            }
            (ans+=val[p])%=Mod;
          }
        }
    }T;
    int main()
    {
        int N,L;
        while(~scanf("%d",&N)){
            T.init();
            rep(i,1,N) {
                scanf("%s",c+1);
                L=strlen(c+1); T.last=1;
                rep(j,1,L) T.add(c[j]-'0');
            }
            T.solve();
            printf("%d
    ",T.ans);
        }
        return 0;
    }
    View Code

    例题八:CodeForces - 235CCyclical Quest

    题意:给定字符串S,Q次询问,每次询问给定字符串x,问S中有多少子串,可以通过循环同构=x;|x总和|<1e6。

    思路:先把S建立自动机,然后topt得到每个节点的次数。 然后再想办法处理匹配,显然x需要每个点作为起点跑一遍自动机,然后重复的指统计一次。 但是这样复杂度是O(x^2); 正确的方法是,把x加倍,然后在自动机上面跑,如果跑得的长度大于|x|,则失配,向fa跑。 整个过程中跑到的点只统计一次。 复杂度是线性的。

    #include<bits/stdc++.h>
    #define rep(i,a,b) for(int i=a;i<=b;i++)
    using namespace std;
    const int maxn=2000010;
    char c[maxn];
    struct SAM{
        int ch[maxn][26],fa[maxn],maxlen[maxn],cnt,last;
        int a[maxn],b[maxn],sz[maxn],val[maxn],vis[maxn],ans;
        SAM(){cnt=1;last=1;}
        int add(int x)
        {
           int np=++cnt,p=last; last=np;
           maxlen[np]=maxlen[p]+1; sz[np]=1;
           while(p&&!ch[p][x]) ch[p][x]=np,p=fa[p];
           if(!p) fa[np]=1;
           else {
             int q=ch[p][x];
             if(maxlen[q]==maxlen[p]+1) fa[np]=q;
             else {
                int nq=++cnt; maxlen[nq]=maxlen[p]+1;
                fa[nq]=fa[q]; fa[q]=fa[np]=nq;
                memcpy(ch[nq],ch[q],sizeof(ch[q]));
                while(p&&ch[p][x]==q) ch[p][x]=nq,p=fa[p];
             }
           }
        }
        void sort()
        {
          rep(i,1,cnt) a[i]=0;
          rep(i,1,cnt) a[maxlen[i]]++;
          rep(i,1,cnt) a[i]+=a[i-1];
          rep(i,1,cnt) b[a[maxlen[i]]--]=i;
          for(int i=cnt;i>=1;i--) sz[fa[b[i]]]+=sz[b[i]];
        }
        int match(int T)
        {
            scanf("%s",c+1);
            int L=strlen(c+1);
            rep(i,1,L) c[i+L]=c[i];
            int np=1,len=0,ans=0;
            rep(i,1,L+L){
                int x=c[i]-'a';
                if(ch[np][x]) np=ch[np][x],len++;
                else {
                   while(np&&!ch[np][x]) np=fa[np],len=maxlen[np];
                   if(!np) np=1,len=0;
                   if(ch[np][x]) np=ch[np][x],len++;
                }
                while(len>L){
                    if(len-1>maxlen[fa[np]]) len--;
                    else np=fa[np],len=maxlen[np];
                    //注意这里,截去头部的两种情况。
                }
                if(len==L&&vis[np]!=T) ans+=sz[np],vis[np]=T;
            }
            return ans;
        }
    }T;
    int main()
    {
        int N,L,Cas=0;
        scanf("%s",c+1); L=strlen(c+1);
        rep(j,1,L) T.add(c[j]-'a');
        T.sort();
        scanf("%d",&N);
        while(N--){
            int ans=T.match(++Cas);
            printf("%d
    ",ans);
        }
        return 0;
    }
    View Code

    本来还有一些经典的例题,但是感觉没必要放太多例题。。。 上面的题都放到VJ-后缀自动机上面了

     明天又要开始打gym了,后缀数组会尽量更新。 02-19------分界线------

    后缀数组

    上面我们说到,后缀自动机并不是万能的,虽然后缀自动机优美,简洁,强大......(超喜欢SAM)下面我会尽可能的和后缀自动机做比较,如果可以,找一些后缀数组可以实现,后缀自动机不太好做的题。

    有的时候我们得用后缀数组来解决问题。 因为后缀数组是排序后的数组,可以结合线段树,二分,单调队列...来解决一些RMQ问题。

    认识:

            1,sa数组:按照字典序排序后的数组,第i位表示第i大的后缀对应在字符串中的下标。

            2,ht数组:height数组,表示排序后和前一个的最长公共前缀。

            3,rank数组:字符串后缀的排名。上面两个数组是针对的排序后,rank针对的字符串本身。

            在求出sa和rank之后,其中sa和rank可以互相转化,他们一一对应,因为后缀的长度都不同,所以不可能有字典序相同的后缀,所以sa对应1到N。 

    排序:

            基数排序,倍增。 对于字符串S,假如只有小写字母,对应了1到26;

             一开始,我们对长度为1的字符串进行基数排序,这个时候sa数组对应1到N,而Rank数组对应1到26,显然不够一一对应,即排序未完成;  

             所以我们按照长度为2的继续排序,那么他们可以把后面一位当作第二关键字.....直到Rank对应1到N,说明排序完毕。

    height数组功能:

           假设要求suf(i)和suf(j)的最长公共前缀LCP,假设rank[i]<rank[j],那么ans=min(ht[rank[i]+1],ht[rank[i]+2]....ht[j]);

           这是显然的,因为我们已经按字典序排序了。   而区间最小值我们可以ST预处理,O(1)查询。

    本质不同的子串数量:

          1,回文树的本质不同的回文串:就是数组大小,即tot。   

          2,后缀自动机的本质不同的子串:集合大小之和,即Σ(maxlen[i]-maxlen[fa[i]])。

          3,后缀数组:我们统计每一个下标作为开头的子串的贡献。 对于下标i, S[i,i]; S[i,i+1],S[i,i+2]....S[i,len];个数为len-i+1,然后我们知道了它和之前的重复数量,去重就是减去ht[rank[i]];

    两个串的最长公共子串:

         求S和T的最长公共子串,我们把他们连接S+‘&’+T,这样的话就对应了|S|+|T|+1个后缀,求后缀数组。 如果sa[i]和sa[i-1]对应的下标分别是S和T,则可以用ht[i]更新答案。  因为区间越长,min(height)越小。

    实现:cntA,A统计第一关键字,cntB,B统计第二关键字。 tsa是按照第二关键字排序的结果,然后把用来排第一关键字。

    void getsa()
        {
            N=strlen(ch+1);
            rep(i,0,300) cntA[i]=0;//注意这里是字符范围,不是N
            rep(i,1,N) cntA[ch[i]]++;
            rep(i,1,300) cntA[i]+=cntA[i-1];
            rep2(i,N,1) sa[cntA[ch[i]]--]=i;
            Rank[sa[1]]=1;
            rep(i,2,N) Rank[sa[i]]=Rank[sa[i-1]]+(ch[sa[i]]==ch[sa[i-1]]?0:1);
            for(int l=1;Rank[sa[N]]<N;l<<=1){
                rep(i,1,N) cntA[i]=cntB[i]=0;
                rep(i,1,N) cntA[A[i]=Rank[i]]++;
                rep(i,1,N) cntB[B[i]=i+l<=N?Rank[i+l]:0]++;
                rep(i,1,N) cntA[i]+=cntA[i-1],cntB[i]+=cntB[i-1];
                rep2(i,N,1) tsa[cntB[B[i]]--]=i;
                rep2(i,N,1) sa[cntA[A[tsa[i]]]--]=tsa[i];
                Rank[sa[1]]=1;
                rep(i,2,N) Rank[sa[i]]=Rank[sa[i-1]]+(A[sa[i]]==A[sa[i-1]]&&B[sa[i]]==B[sa[i-1]]?0:1);
            }
        }

    求height数组:利用相邻关系,有height[rank[i]] ≥ height[rank[i-1]]-1

        void getht()
        {
            for(int i=1,j=0;i<=N;i++){
                if(j) j--;
                while(ch[i+j]==ch[sa[Rank[i]-1]+j]) j++;
                ht[Rank[i]]=j;
            }
        }

    (目前暂未出现后缀自动机不能实现的功能.....其他功能请看例题。)

  • 相关阅读:
    公共服务领域英文译写规范
    [.NET Core]
    [WebAPI]
    [C#]
    [ES]
    [.Net Core]
    [SourceTree]
    如何使用一个库中不存在的函数
    在使用masm32 rc编译资源文件.rc出现的问题
    MSDN 2005 安装问题
  • 原文地址:https://www.cnblogs.com/hua-dong/p/10402714.html
Copyright © 2011-2022 走看看