1. 程式人生 > 實用技巧 >HihoCoder 字尾自動機入門1-6題解

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("\n"); } 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\n",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\n",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\n",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\n",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]='\0';
        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]='\0';
        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\n");
    else{
        solveb(1,0,A.sg[u]);
        printf("%s\n%s\n",ansa,ansb);
    }
    return 0;
} 
兩行淚