后綴自動機(SAM)


*在學習后綴自動機之前需要熟練掌握WA自動機、RE自動機與TLE自動機*

 

什么是后綴自動機

后綴自動機 Suffix Automaton (SAM) 是一個用 O(n) 的復雜度構造,能夠接受一個字符串所有后綴的自動機。

它最早在陳立傑的 2012 年 noi 冬令營講稿中提到。

在2013年的一場多校聯合訓練中,陳立傑出的 hdu 4622 可以用 SAM 輕松水過,由此 SAM 流行了起來。

一般來說,能用后綴自動機解決的問題都可以用后綴數組解決。但是后綴自動機也擁有自己的優點。

 

1812.  Longest Common Substring II
題目大意:給出N(N <= 10)個長度不超過100000的字符串,求他們的最長公共連續子串。
時限:SPOJ上的2s

陳立傑的講稿中用了 spoj 的1812作為例子,由於 spoj 太慢,所以只有O(n)的算法才能過掉本題,這時就要用到SAM了。

 

后綴自動機的構造

參考網上的各種模板即可。

后綴自動機的性質

裸的后綴自動機僅僅是一個可以接收子串的自動機,在它的狀態結點上維護的性質才是解題的關鍵。

一個構造好的 SAM 實際上包含了兩個圖:由 go 數組組成的 DAG 圖;由 par 指針構成的 parent 樹。

 

SAM 的狀態結點包含了很多重要的信息:

max:即代碼中 val 變量,它表示該狀態能夠接受的最長的字符串長度。

min:表示該狀態能夠接受的最短的字符串長度。實際上等於該狀態的 par 指針指向的結點的 val + 1。

max-min+1:表示該狀態能夠接受的不同的字符串數。

right:即 end-set 的個數,表示這個狀態在字符串中出現了多少次,該狀態能夠表示的所有字符串均出現過 right 次。

par:par 指向了一個能夠表示當前狀態表示的所有字符串的最長公共后綴的結點。所有的狀態的 par 指針構成了一個 parent 樹,恰好是字符串的逆序的后綴樹。

parent 樹的拓撲序:序列中第i個狀態的子結點必定在它之后,父結點必定在它之前。

 

后綴自動機的經典問題

 

uva 719 - Glass Beads 最小循環串

后綴自動機的遍歷。

給一個字符串S,每次可以將它的第一個字符移到最后面,求這樣能得到的字典序最小的字符串。

將字符串S拼接為SS,構造自動機,從根結點開始每次走最小編號的邊,移動length(S)步就可以找到字典序最小的串。

由於 SAM 可以接受 SS 所有的子串,而字典序最小的字符串也必定是 SS 的子串,因此按照上面的規則移動就可以找到一個字典序最小的子串。

 

spoj 1811 Longest Common Substring 最長公共子串

給兩個長度小於100000的字符串 A 和 B,求出他們的最長公共連續子串。

先將串 A 構造為 SAM ,然后用 B 按如下規則去跑自動機。

用一個變量 lcs 記錄當前的最長公共子串,初始化為0。

設當前狀態結點為 p,要匹配的字符為 c,若 go[c] 中有邊,說明能夠轉移狀態,則轉移並 lcs++;

若不能轉移則將狀態移動到 p 的 par ,如果仍然不能轉移則重復該過程直到 p 回到根節點,並將 lcs 置為 0;

如果在上一個過程中進入了能夠轉移的狀態,則設 lcs 為當前狀態的 val。

為什么失配后要移向 par 呢?因為在狀態 p 上失配說明該狀態的 [min,max] 所表示字符串都不是 B 中的子串,但是比它們短的后綴仍有可能是 B 的子串,而 par 指針恰好指向了該狀態的后綴。

 

spoj 1812 Longest Common Substring II 多個串的最長公共子串

在上一題中我們知道了如何求兩個串的最長公共子串,本題則是要求多個串的最長公共子串。

本題要用到 parent 樹的拓撲序。

首先用第一個串構造 SAM,然后用其他的串匹配它。

SAM 的狀態要多維護兩個信息:lcs,當多個串的最長公共子串的最后一個字符落在該狀態上的長度;nlcs,當前串的最長公共子串的最后一個字符落在該狀態上的長度。

我們對每個串的匹配之后,要對每個狀態的 lcs 進行維護,顯然 lcs=min(lcs, nlcs),而我們最后所求的就是所有狀態中 lcs 的最大值。

匹配的過程與上一題相同,但是在匹配過程中,到達狀態 p 時得到的 nlcs 未必就是該狀態能表示的最長公共子串長,因為如果一個子串出現了n次,那么子串的所有后綴也至少出現了n次。

因此在每個串匹配之后求要按照拓撲序的逆序維護每個狀態的 nlcs,使 p->par->nlcs=max(p->nlcs, p->par->nlcs)。

 

hdu 4622 Reincarnation 統計不同子串個數

這也是許多新人第一次接觸到 SAM 的題。本題可以用各種姿勢 AC,但是用 SAM 最輕松。

給出一個字符串,最長2000,q個詢問,每次詢問[l,r]區間內有多少個不同的字串。

SAM 中的每個狀態能夠表示的不同子串的個數為 val - 父結點的 val。因此在構造自動機時,用變量 total 記錄當前自動機能夠表示的不同子串數,對每一次 extend 都更新 total 的值。將這個過程中的每一個 total 值都記錄下了就能得到一個表示子串個數表。我們對字符串的每一個后綴都重新構造一遍 SAM 就可以得到一個二維的表。

對每次詢問,在表中查找相應的值即可。

 

hdu 4436 str2int 處理不同的子串

給出n個數字,數字很長,用字符串讀入,長度總和為10^5。求這n個字符串的所有子串(不重復)的和取模2012 。

題目要對所有不重復的子串進行處理,考慮使用 SAM 來將解決。

將 n 個數字拼接成一個字符串,用不會出現的數字 10 進行分割。

構造完之后按照拓撲序計算每個狀態上的 sum 與 cnt,sum 表示以當前狀態為結尾的子串的和,cnt 表示有多少種方法到達當前結點。

設父結點為 u 向數字 k 移動到的子結點為 v, 顯然結點 v 的狀態要在 sum 上增加 add=u->sum*10+u->cnt*k。即 u 的能表示的數字總和乘上10再加上到達 v 的方法總數乘上當前的個位數字 k。

最后答案就是將所有狀態的 sum 求和。

 

spoj 8222 Substrings 子串出現次數

給一個字符串S,令F(x)表示S的所有長度為x的子串中,出現次數的最大值。求F(1)..F(Length(S)) 。

在拓撲序的逆序上維護每個狀態的 right,表示當前狀態的出現次數。

最后當前用每個狀態的 right 來更新 f[val],即當前狀態能表示的最長串的出現次數。

最后用 f[i] 依次去更新 f[i-1] 取最大值,因為若一個長度為 i 的串出現了 f[i] 次,那么長度為 i-1 的串至少出現 f[i] 次。

 

poj 3415Common Substrings 子串計數

給出兩個串,問這兩個串的所有的子串中(重復出現的,只要是位置不同就算兩個子串),長度大於等於k的公共子串有多少個。

先對第一個串構造 SAM,通過狀態的 right 與 val 可以輕松求出它能表示的所有子串數。現在的問題是如何滿足條件。

用第二個串對 SAM 做 LCS,當前狀態 LCS >= K 時,維護狀態上的 cnt++,表示該狀態為大於K且最長公共串的結尾的次數為 cnt 次。

統計最長公共子串的狀態中滿足條件的個數 ans+=(lcs-max(K,p->mi)+1)*p->right 

匹配結束后,用拓撲序的逆序維護每個狀態父結點 cnt,此時 cnt 的含義為該狀態被包含的次數。

統計不是最長公共子串的狀態但是被子串包含的個數,ans+=p->cnt*(p->par->val - max(K,p->par->mi)+1)*p->par->right,用父結點被包含的次數乘以滿足條件的串數累加到答案中。

 

spoj 7258 Lexicographical Substring Search 求字典序

給出一個字符串,長度為90000。詢問q次,每次回答一個k,求字典序第k小的子串。

仍然用拓撲序得到每個狀態擁有的不同子串數。

對第k小的子串,按字典序枚舉邊,跳過一條邊則 k 減去該邊指向的狀態的不同子串數,直到不能跳過,然后沿着該邊移動一次,循環這個步驟直到 k變為0。

此時的路徑就是字典序第k小的子串。

 

Codeforces 235C Cyclical Quest 串的出現次數

*這場比賽的出題人是 WJMZBMR 陳立傑*

給出一個字符串s,這里稱之為母串,然后再給出n個子串,n<=10^5,子串長度總和不超過10^6。問,對於每一個子串的所有不同的周期性的同構串在母串中出現的次數總和。

將母串構造 SAM,將子串復制拼接到一起然后去掉最后一個字母去跑 SAM。

對滿足條件的狀態向上維護直到原子串的長度包含在了狀態能表示的長度中並用 mark 標記。 

然后將該狀態的出現次數累加到答案上,如果一個應該累加的狀態已經被 mark 過了,就不再累加。

 

Codeforces 427D Match & Catch 公共串的出現次數

給出兩個長度均不超過5000的字符串s1,s2,求這兩個串中,都只出現一次的最短公共子串。

對第一個串構造 SAM,用第二個串跑。顯然 right 為1的狀態就是在第一個串中出現次數為1的子串。

匹配過程總的每進入一個結點,就將結點上的 cnt 加一,表示該狀態表示的最長公共串在第二個串的出現次數。

最后按拓撲序逆序求出所有狀態的 cnt,若一個結點出現過 cnt 次,那么他的父結點即它的后綴出現次數也要加上 cnt。

最后遍歷所有的狀態,right 等於 1 且 cnt 等於 1 的狀態就是出現次數為1的公共子串,找到其中最短的作為答案即可。

 

我的板子

 

  1 #include <iostream>
  2 #include <cstring>
  3 #include <cstdio>
  4 
  5 using namespace std;
  6 typedef long long LL;
  7 const int maxn=300000;
  8 const int maxm=160000;
  9 /***************
 10     SAM 真·模板
 11 ***************/
 12 struct State {
 13     State *par;
 14     State *go[52];
 15     int val; // max,當前狀態能接收的串的最長長度
 16     int mi; // min,當前狀態能接受的串的最短長度,即 par->val+1
 17     int cnt; // 附加域,用來計數
 18     int right; // right集,表示當前狀態可以在多少個位置上出現
 19     void init(int _val = 0){
 20         par = 0;
 21         val = _val;
 22         cnt=0;
 23         mi=0;
 24         right=0;
 25         memset(go,0,sizeof(go));
 26     }
 27     int calc(){ // 表示該狀態能表示多少中不同的串
 28         if (par==0) return 0;
 29         return val-par->val;
 30     }
 31 };
 32 State *root, *last, *cur;
 33 State nodePool[maxn];
 34 State* newState(int val = 0) {
 35     cur->init(val);
 36     return cur++;
 37 }
 38 //int total; // 不同的子串個數。
 39 void initSAM() {
 40     //total = 0;
 41     cur = nodePool;
 42     root = newState();
 43     last = root;
 44 }
 45 void extend(int w) {
 46     State* p = last;
 47     State* np = newState(p->val + 1);
 48     np->right=1; // 設置right集
 49     while (p && p->go[w] == 0) {
 50         p->go[w] = np;
 51         p = p->par;
 52     }
 53     if (p == 0) {
 54         np->par = root;
 55         //total+=np->calc();
 56     }
 57     else {
 58         State* q = p->go[w];
 59         if (p->val + 1 == q->val) {
 60             np->par = q;
 61             //total+=np->calc();
 62         }
 63         else {
 64             State* nq = newState(p->val + 1);
 65             memcpy(nq->go, q->go, sizeof(q->go));
 66             //total -= q->calc();
 67             nq->par = q->par;
 68             q->par = nq;
 69             np->par = nq;
 70             //total += q->calc()+nq->calc()+np->calc();
 71             while (p && p->go[w] == q) {
 72                 p->go[w] = nq;
 73                 p = p->par;
 74             }
 75         }
 76     }
 77     last = np;
 78 }
 79 
 80 int d[maxm];
 81 State* b[maxn];
 82 void topo(){ // 求出parent樹的拓撲序
 83     int cnt=cur-nodePool;
 84     int maxVal=0;
 85     memset(d,0,sizeof(d));
 86     for (int i=1;i<cnt;i++) maxVal=max(maxVal,nodePool[i].val),d[nodePool[i].val]++;
 87     for (int i=1;i<=maxVal;i++) d[i]+=d[i-1];
 88     for (int i=1;i<cnt;i++) b[d[nodePool[i].val]--]=&nodePool[i];
 89     b[0]=root;
 90 }
 91 
 92 void gaoSamInit(){ // 求出SAM的附加信息
 93     State* p;
 94     int cnt=cur-nodePool;
 95     for (int i=cnt-1;i>0;i--){
 96         p=b[i];
 97         p->par->right+=p->right;
 98         p->mi=p->par->val+1;
 99     }
100 }
101 
102 
103 char s[maxm];
104 const int INF=0x3f3f3f3f;
105 int gao(char s[]){
106     int ans=INF;
107     int cnt=cur-nodePool;
108     int len=strlen(s);
109     int lcs=0;
110     State* p=root;
111 
112     for (int i=0;i<len;i++){
113         int son=s[i]-'a';
114         if (p->go[son]!=0){
115             lcs++;
116             p=p->go[son];
117         }
118         else{
119             while (p&&p->go[son]==0) p=p->par;
120             if (p==0){
121                 lcs=0;
122                 p=root;
123             }
124             else{
125                 lcs=p->val+1;
126                 p=p->go[son];
127             }
128         }
129         // TODO:
130         if (lcs>0) p->cnt++;
131     }
132 
133     for (int i=cnt-1;i>0;i--){
134         p=b[i];
135         // TODO:
136         if (p->right==1&&p->cnt==1) ans=min(ans,p->mi);
137         p->par->cnt += p->cnt;
138     }
139     return ans;
140 }
SAM模板

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM