zoukankan      html  css  js  c++  java
  • HihoCoder 后缀自动机入门1-6题解

    比起后缀数组,我觉得后缀自动机比较好理解也。。。

    #1441 : 后缀自动机一·基本概念

    endpos集合相同的子串才是一个同一个状态。暴力模拟即可。

    #include<bits/stdc++.h>
    #include<tr1/unordered_map>
    using namespace std;
    typedef long long ll;
    tr1::unordered_map<string,ll> mmp;
    tr1::unordered_map<ll,string> shortest,longest; 
    int main(){
        string s,ss;
        cin>>s;
        int n,lens=s.size();
        ll state;
        for(int i=0;i<lens;i++)
            for(int j=1;j<=lens-i;j++){
                state=0;
                ss=s.substr(i,j);
                for(int k=0;k<=lens-j;k++){
                    if(ss==s.substr(k,j)) state|=(1ll<<(k+j-1));
                }
                if(!shortest.count(state)||shortest[state].size()>j) shortest[state]=ss;
                if(!longest.count(state)||longest[state].size()<j) longest[state]=ss;
                mmp[ss]=state;
            }
        cin>>n;
        while(n--){
            cin>>ss;
            state=mmp[ss];
            cout<<shortest[state]<<" "<<longest[state];
            for(int i=0;i<lens;i++){
                if(state&(1ll<<i)) printf(" %d",i+1);
            }
            printf("
    ");
        }
        return 0;
    } 
    String模拟

    #1445 : 后缀自动机二·重复旋律5

    求不同子串的个数,而每个状态中len[i]为这个状态的最长子串长度,而它最短子串长度为len[link[i]]+1(因为它的后缀是在link[i]处断的嘛)

    所以每个状态最长子串长度减去最短子串长度+1就是这个状态有多少种不同子串,然后全部加起来即可。

    #include<bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int N=2e6+11;
    char s[N];
    int size,last,maxlen[N];//minlen[N];
    //拥有相同endpos集合的为同一状态
    //对于同一状态中的字符串,他们都是该状态最长子串的后缀 
    //size总状态数,last上一个状态编号,maxlen[i]:i状态包含的最长子串长度 
    int link[N],trans[N][31];
    //trans[i][j] 转移函数,为i状态遇到j字符会转移到哪个状态
    //link[i] SuffixLinks,i状态的连续后缀在哪个状态断开 
    void initsam(int n){
        size=last=1;
        for(int i=0;i<=n;i++){
            link[i]=maxlen[i]=0;//minlen[i]=0;
            for(int j=0;j<26;j++) trans[i][j]=0;
        }
    }
    void extend(int x){
        int cur=++size,u;
        maxlen[cur]=maxlen[last]+1;
        //Suffixpath(cur-S)路径上没有对x的转移的状态,添加到cur的转移 
        for(u=last;u&&!trans[u][x];u=link[u]) trans[u][x]=cur;
        //若Suffixpath(cur-S)路径上的状态都没有对x的转移,那么此时curlink到初状态即可 
        if(!u) link[cur]=1;
        else{
            //若Suffixpath(cur-S)路径存在有对x转移的状态u
            //而v是u遇到x后转移到的状态 
            int v=trans[u][x];
            //若v中最长的子串添加上x便是u的最长子串,此时将curlink到v 
            //也就是v状态中的子串都是cur状态中的后缀,且cur的后缀序列刚好在v处断开 
            if(maxlen[v]==maxlen[u]+1) link[cur]=v;
            else{
                //否则创建个中间状态进行转移 
                //也就是cur状态和v状态都有着部分相同的后缀,而之前这些后缀保存在v状态 
                //而v状态中还有些状态不是cur状态的后缀的,所以需要个新状态表示他们共有的后缀 
                int clone=++size;
                maxlen[clone]=maxlen[u]+1;
                memcpy(trans[clone],trans[v],sizeof(trans[v]));
                link[clone]=link[v];
            //    minlen[clone]=maxlen[link[clone]]+1;
                //原先添加x后转移到v的状态,现在都转移到中间状态 
                for(;u&&trans[u][x]==v;u=link[u]) trans[u][x]=clone;
                //最后,因为cur状态和v状态的后缀都在中间状态这里断开
                //所以cur和v都link到中间状态 
                link[cur]=link[v]=clone;
            //    minlen[v]=maxlen[link[v]]+1;
            }
        }
    //    minlen[cur]=maxlen[link[cur]]+1;
        last=cur;
        return ;
    }
    int main(){
        scanf("%s",s);
        int lens=strlen(s);
        initsam(2*lens);
        for(int i=0;i<lens;i++) extend(s[i]-'a');
        ll ans=0;
        for(int i=2;i<=size;i++) ans+=maxlen[i]-maxlen[link[i]];
        printf("%lld
    ",ans);
        return 0;
    }
     
    入门往往那么容易

    #1449 : 后缀自动机三·重复旋律6

    要求每个长度的出现个数,endpos集合就是每个状态里子串出现的个数,那么么endpos怎么求呢,直接搬运HihoCoder的讲解了,感觉讲得很好,侵删。。。。。。

    小Ho:我们明白了。一个状态st对应的|endpos(st)|至少是它儿子的endpos大小之和。这一点还是比较容易证明的。假设x和y是st的两个儿子,那么根据Suffix Link的定义,我们知道st中的子串都是x中子串的后缀,也是y中子串的后缀。所以endpos(st) ⊇ endpos(x) 并且 endpos(st) ⊇ endpos(y)。又根据Suffix Link的定义我们知道x中的子串肯定不是y中子串的后缀,反之亦然,所以endpos(x) ∩ endpos(y) = ∅。所以|endpos(st)| >= |endpos(x)| + |endpos(y)|。

    小Hi:那么|endpos(st)|可能比st儿子的endpos大小之和大多少呢?

    小Ho:最多就大1。并且大1的情况当且仅当st是上文提到的绿色状态,即st包含S的某个前缀时才发生。我们分析endpos(1)={1, 2, 5}就会发现,它比endpos(2) ∪ endpos(6) = {2, 5}多出来的结束位置1的原因就是状态1还包含S的长度为1的前缀"a"。更一般的情形是如果某个状态st包含S的一个前缀S[1..l],那么一定有l∈endpos(st),并且l不能从st的儿子中继承过来。这时就需要+1。

    小Hi:没错。那么我们如何判断哪些状态应该标记成绿色状态呢?

    小Ho:可以在构造SAM的时候顺手做了。回顾我们构造SAM的算法,当新加入一个字符的时候,我们至少会新建一个状态z(还可能新建一个状态y),这个状态z一定是绿色状态(y一定不是)。

    小Hi:没错,我们回顾一下。先构造SAM,顺手把绿色状态标记出来。然后再对Suffix Link连成的树"自底向上"求出每一个状态的|endpos(st)|,这一步"自底向上"可以通过拓扑排序完成,我们很早之前就讲过,不再赘述。

    所以就是每个添加字符的那个状态endpos大小是1,然后再对SuffixLink树进行拓扑排序,就可以得到每个状态的endpos大小了。(dfs回溯也可以)

    知道每个状态endpos之后,我们又知道每个状态的最长子串长度,它的影响范围就是小于等于它的长度,所以记录下相应长度的endpos再从后往前求最大值即可。

    #include<bits/stdc++.h>
    using namespace std;
    const int N=2e6+11;
    struct Side{
        int v,ne;
    }S[N];
    char s[N];
    int sn,head[N];
    int size,last,len[N],link[N],trans[N][31];
    int endpos[N],ans[N];
    void initS(int n){
        sn=0;
        for(int i=0;i<=n;i++) head[i]=-1;
    }
    void addS(int u,int v){
        S[sn].v=v;
        S[sn].ne=head[u];
        head[u]=sn++;
    } 
    void initsam(int n){
        size=last=1;
        for(int i=0;i<n;i++){
            len[i]=link[i]=endpos[i]=0;
            for(int j=0;j<26;j++) trans[i][j]=0;
        }
    }
    void extend(int x){
        int cur=++size,u;
        endpos[cur]=1;
        len[cur]=len[last]+1;
        for(u=last;u&&!trans[u][x];u=link[u]) trans[u][x]=cur;
        if(!u) link[cur]=1;
        else{
            int v=trans[u][x];
            if(len[v]==len[u]+1) link[cur]=v;
            else{
                int clone=++size;
                len[clone]=len[u]+1;
                link[clone]=link[v];
                memcpy(trans[clone],trans[v],sizeof(trans[v]));
                for(;u&&trans[u][x]==v;u=link[u]) trans[u][x]=clone;
                link[cur]=link[v]=clone;
            }
        }
        last=cur;
    }
    void dfs(int u){
        for(int i=head[u];~i;i=S[i].ne){
            int v=S[i].v;
            dfs(v);
            endpos[u]+=endpos[v]; 
        }
    }
    void solve(int lens){
        initS(size);
        for(int i=1;i<=size;i++) addS(link[i],i);
        dfs(1);
        for(int i=2;i<=size;i++) ans[len[i]]=max(ans[len[i]],endpos[i]);
        for(int i=lens-1;i>=1;i--) ans[i]=max(ans[i],ans[i+1]); 
    }
    int main(){
        scanf("%s",s);
        int lens=strlen(s);
        initsam(2*lens);
        for(int i=0;i<lens;i++) extend(s[i]-'a');
        solve(lens);
        for(int i=1;i<=lens;i++) printf("%d
    ",ans[i]);
        return 0;
    } 
    进门了却出不去

    #1457 : 后缀自动机四·重复旋律7

     先不管两个串,就单有一个串的时候,我们怎么算它的不同子串权值和呢,这就涉及动态规划了。

    比如我们知道子串12的权值为12,那么怎么得到子串123的权值呢,很简单,12*10+3嘛。而有些状态不一定只包含一个子串,但它们加上一个新字符x后的转移状态是相同的。

    也就是说,如果我们知道了某个状态的所有子串权值和sum(u),而trans[u][x]=v(u中的所有子串加上x后就变成v中的部分子串),那么sum(v)+=sum(u)*10+x*u中所有子串的个数。

    知道这个转移过程之后,我们就可以根据trans确定拓扑顺序,然后在上面进行转移。

    那如果是两个串,我们也可以像前面后缀数组一样,用一个不会出现的字符间隔开,然后把它们连接起来。这里使用:,因为:的ascii码值为9的ascii值+1,好处理。

    然后有些状态中就会含有一些含:的子串,而这些子串是不合法的,所以我们转移的时候跳过这些不合法的子串即可,怎么跳过呢,就是不对trans[u][:]进行处理。

    那么新的转移过程就是sum(v)+=sum(u)*10+x*u中所有合法子串的个数。

    #include<bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int N=2e6+11,md=1e9+7;
    char s[N];
    ll sum[N];
    queue<int> q;
    int du[N],fcnt[N];
    int size,last,len[N],link[N],trans[N][21];
    void initsam(int n){
        size=last=1;
        for(int i=0;i<=n;i++){
            len[i]=link[i]=0;
            for(int j=0;j<11;j++) trans[i][j]=0;
        }
    }
    void extend(int x){
        int cur=++size,u;
        len[cur]=len[last]+1;
        for(u=last;u&&!trans[u][x];u=link[u]) trans[u][x]=cur;
        if(!u) link[cur]=1;
        else{
            int v=trans[u][x];
            if(len[v]==len[u]+1) link[cur]=v;
            else{
                int clone=++size;
                len[clone]=len[u]+1;
                link[clone]=link[v];
                memcpy(trans[clone],trans[v],sizeof(trans[v]));
                for(;u&&trans[u][x]==v;u=link[u]) trans[u][x]=clone;
                link[cur]=link[v]=clone;
            }
        }
        last=cur;
        return ;
    }
    void solve(){
        for(int i=1;i<=size;i++){
            sum[i]=fcnt[i]=0;
            for(int j=0;j<11;j++){
                if(trans[i][j]) du[trans[i][j]]++;
            }
        }
        fcnt[1]=1;
        q.push(1);
        while(!q.empty()){
            int u=q.front();
            q.pop();
            for(int i=0;i<11;i++){
                int v=trans[u][i];
                if(!v) continue;
                if(i!=10){
                    fcnt[v]+=fcnt[u];
                    sum[v]=(sum[v]+(sum[u]*10%md+i*fcnt[u])%md)%md;
                }//不转移含:的子串 
                du[v]--;
                if(!du[v]) q.push(v);
            }
        }
    }
    int main(){
        int n;
        initsam(N-1);
        scanf("%d",&n);
        for(int i=0;i<n;i++){
            scanf("%s",s);
            int lens=strlen(s);
            for(int j=0;j<lens;j++) extend(s[j]-'0');
            if(i!=n-1) extend(10);//类似后缀数组中用#分隔两个串 
        }
        solve();
        ll ans=0;
        for(int i=1;i<=size;i++) ans=(ans+sum[i])%md;
        printf("%lld
    ",ans);
        return 0;
    }
    如果要说个建议

    #1465 : 后缀自动机五·重复旋律8

    如果串不循环旋转的话,那就是T串在S串中出现的次数,也就是看T串在S串的SAM中是哪个状态u,那么endpos[u]就是答案了。

    而找T串在S串的SAM中的状态的过程,其实也类似于找T串和S串的LCS(最长公共子串),如果到达某个状态的LCS是T串的长度,这时就找到了。

    怎么用SAM找S串和T串的LCS呢,我们对S串建SAM,那么接下来用T串在S串上面匹配。一开始u等于初始状态,而lcs=0。

    对于T[i],如果trans[u][T[i]]不为空的话,很明显lcs++,然后u=trans[u][T[i]]。而当trans[u][T[i]]为空怎么办 ,我们就可以根据link[u],suffix-path(u->S)向前找trans[u][T[i]]不为空的状态。

    而这个过程就类似于KMP中失配时,按next数组往回找的过程。若一直到最初状态,rans[u][T[i]]依旧为空,那么说明S串中无T[i]字符,让u为最初状态,lcs为0。

    while(u!=1&&!trans[u][x]) u=link[u],lcs=len[u];//往回找trans[u][x]不为空的状态
    if(trans[u][x]) lcs++,u=trans[u][x];
    else u=1,lcs=0;

    而这个的T串还会进行循环,对于循环串的一种解决办法就是把它拆成一条链,把原来的串拷贝一份放到后面。T[i]'=T'[n+i]=T[i]

    然后遍历T'[i],求出在每个位置T'[i]结束的最长公共子串,可以知道u和lcs。如果这时lcs>=T串的长度n,那我们就得到了一个公共子串T'[i-lcs+1 .. i]。

    这个子串在S中出现的次数是|endpos(u)|,又恰好包含T的循环同构串T'[i-n+1 .. i]。而像aaa串,它某些循环串是相同的,这时就每个状态u只统计一次即可。

    但还有一种情况,要区分T'[i-lcs+1 .. i]出现次数和T'[i-n+1 .. i]的出现次数。lcs>=n,T'[i-n+1 .. i]是T'[i-lcs+1 .. i]不一定在同一个状态u。

    T'[i-n+1 .. i]是T'[i-lcs+1 .. i]长度为n的后缀,可能在suffix-path(u->S)上,出现次数比T'[i-lcs+1 .. i]多(HihoCoder中这里应该是打错了)。

    这时也好处理,我们顺着suffix-path(u->S)往回找,找到最靠近S且最长子串长度仍然大于等于n的即可。

    #include<bits/stdc++.h>
    using namespace std;
    const int N=2e5+11;
    struct Side{
        int v,ne;
    }S[N];
    char s[N],ss[N];
    int sn,head[N];
    int size,last,len[N],link[N],trans[N][31];
    int endpos[N],vis[N],tu[N];
    void initS(int n){
        sn=0;
        for(int i=0;i<=n;i++) head[i]=-1;
    }
    void addS(int u,int v){
        S[sn].v=v;
        S[sn].ne=head[u];
        head[u]=sn++;
    }
    void initsam(int n){
        size=last=1;
        for(int i=0;i<n;i++){
            len[i]=link[i]=endpos[i]=0;
            for(int j=0;j<26;j++) trans[i][j]=0;
        }
    }
    void extend(int x){
        int cur=++size,u;
        endpos[cur]=1;
        len[cur]=len[last]+1;
        for(u=last;u&&!trans[u][x];u=link[u]) trans[u][x]=cur;
        if(!u) link[cur]=1;
        else{
            int v=trans[u][x];
            if(len[v]==len[u]+1) link[cur]=v;
            else{
                int clone=++size;
                len[clone]=len[u]+1;
                link[clone]=link[v];
                memcpy(trans[clone],trans[v],sizeof(trans[v]));
                for(;u&&trans[u][x]==v;u=link[u]) trans[u][x]=clone;
                link[cur]=link[v]=clone;
            }
        }
        last=cur;
    }
    void dfs(int u){
        for(int i=head[u];~i;i=S[i].ne){
            int v=S[i].v;
            dfs(v);
            endpos[u]+=endpos[v]; 
        }
    }
    void tp(){
        initS(size);
        for(int i=1;i<=size;i++) addS(link[i],i);
        dfs(1);
    }
    int main(){
        scanf("%s",s);
        int n,lens=strlen(s);
        initsam(2*lens);
        for(int i=0;i<lens;i++) extend(s[i]-'a');
        tp();
        scanf("%d",&n);
        while(n--){
            scanf("%s",ss);
            int lenss=strlen(ss),u=1,lcs=0,ans=0,cnt=0;
            for(int i=0;i<lenss-1;i++) ss[lenss+i]=ss[i];
            for(int i=0;i<2*lenss-1;i++){
                int x=ss[i]-'a';
                while(u!=1&&!trans[u][x]) u=link[u],lcs=len[u];
                if(trans[u][x]) lcs++,u=trans[u][x];
                else u=1,lcs=0;
                //处理T[i-lcs+1]跟T[i-n+1]不同状态的情况 
                if(lcs>lenss){
                    while(len[link[u]]>=lenss) u=link[u];
                    lcs=len[u];
                }
                //每个状态只统计一次 
                if(lcs>=lenss&&!vis[u]){
                    vis[u]=1;
                    tu[cnt++]=u;
                    ans+=endpos[u];
                }
            }
            for(int i=0;i<cnt;i++) vis[tu[i]]=0;
            printf("%d
    ",ans);
        }
        return 0;
    } 
    那就是不要进去

    #1466 : 后缀自动机六·重复旋律9

    不知道怎么解释,看代码吧,等语言表达能力提升,再来更新。

    #include<bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int N=2e5+11;
    struct Sam{
        int size,last,len[N],link[N],trans[N][31],sg[N];
        ll cnt[N][31];
        //cnt[i][j]为以该状态为前缀,sg函数为j的子串个数 
        Sam(){
            size=last=1;
            sg[1]=-1;
        }
        void extend(int x){
            int cur=++size,u;
            sg[cur]=-1;
            len[cur]=len[last]+1;
            for(u=last;u&&!trans[u][x]; u=link[u]) trans[u][x]=cur;
            if(!u) link[cur]=1;
            else{
                int v=trans[u][x];
                if(len[v]==len[u]+1) link[cur]=v;
                else{
                    int clone=++size;
                    sg[clone]=-1;
                    len[clone]=len[u]+1;
                    link[clone]=link[v];
                    memcpy(trans[clone],trans[v],sizeof(trans[v]));
                    for(;u&&trans[u][x]==v;u=link[u]) trans[u][x]=clone;
                    link[cur]=link[v]=clone;
                }
            }
            last=cur;
        }
        int Sg(int u){
            if(sg[u]!=-1) return sg[u];
            int vis[31];
            for(int i=0;i<30;i++) vis[i]=0;
            for(int i=0;i<26;i++){
                int v=trans[u][i];
                if(v){
                    vis[Sg(v)]=1;
                    for(int j=0;j<30;j++) cnt[u][j]+=cnt[v][j];
                }
            }
            for(int i=0;i<30;i++){
                if(!vis[i]){
                    sg[u]=i;
                    cnt[u][i]++;
                    break;
                }
            }
            for(int i=0;i<30;i++) cnt[u][30]+=cnt[u][i];
            return sg[u];
        }
    }A,B;
    ll k;
    char a[N],b[N],ansa[N],ansb[N]; 
    int solvea(int u,int p){
        //因为先找A串,B串此时为空串,这里就是看 
        //B串sg不为A当前构造的这个串的sg的串有多少个
        ll sum=B.cnt[1][30]-B.cnt[1][A.sg[u]];
        //如果sum大于等于k,说明接下来再去构造B串即可
        //此时A串就是字典序最小的
        if(sum>=k){
            ansa[p]='';
            return u;    
        }
        k-=sum;
        for(int i=0;i<26;i++){
            int v=A.trans[u][i];
            if(v){
                sum=0;
                //这里就是算当A串的p位为'a'+i时,B串可能的串有多少种 
                for(int j=0;j<30;j++){
                    sum+=A.cnt[v][j]*(B.cnt[1][30]-B.cnt[1][j]);
                }
                //如果sum小于k,说明A串的p位为'a'+i的话,不能达到k 
                //还得往下一个字符找 
                if(sum<k) k-=sum;
                else{
                    //否则,A串的p位为'a'+i,继续去找p+1为 
                    ansa[p]='a'+i;
                    return solvea(v,p+1); 
                }
            }
        }
        return 0;
    }
    void solveb(int u,int p,int x){
        k-=(B.sg[u]!=x); 
        if(!k){
            ansb[p]='';
            return ;
        }
        for(int i=0;i<26;i++){
            int v=B.trans[u][i];
            //这里就是看,B串的p位为'a'+i接下来能有多少能可能的串
            ll sum=B.cnt[v][30]-B.cnt[v][x];
            //同A串 
            if(sum<k) k-=sum;
            else{
                ansb[p]='a'+i;
                solveb(v,p+1,x);
                return ;
            }
        }
    }
    int main(){
        scanf("%lld%s%s",&k,a,b);
        int lena=strlen(a),lenb=strlen(b);
        for(int i=0;i<lena;i++) A.extend(a[i]-'a');
        for(int i=0;i<lenb;i++) B.extend(b[i]-'a');
        //预处理出两个字符串的每个状态的sg和cnt 
        A.Sg(1);B.Sg(1); 
        int u=solvea(1,0);
        if(!u) printf("NO
    ");
        else{
            solveb(1,0,A.sg[u]);
            printf("%s
    %s
    ",ansa,ansb);
        }
        return 0;
    } 
    两行泪
  • 相关阅读:
    读文章论文
    安装并使用SourceMonitor检测代码复杂度
    FindBug安装与使用
    PMD安装与使用
    Checkstyle安装与使用
    文章主题
    GitHub账号
    PICT的安装与使用
    Junit的安装与使用
    SourceMonitor的安装及使用
  • 原文地址:https://www.cnblogs.com/LMCC1108/p/13338495.html
Copyright © 2011-2022 走看看