Suffix Automaton

後綴自動機

先上SAM builder備用連接。以前的垃圾博客,洛谷的某篇教程,饕餮傳奇的題單html

後綴自動機,點數是2n!數組

首先對着代碼講一遍三種插入。ide

 1 inline void insert(char c) { // 
 2         int f = c - 'a'; // 轉移邊
 3         int p = last, np = ++top; // p 是以前的結尾節點,new p是新建的,表明全串及其若干後綴的節點
 4         last = top; // 更新結尾節點
 5         len[np] = len[p] + 1; // 最長長度 + 1
 6         while(p && !tr[p][f]) { // 一路上,若是某個後綴沒有f的轉移邊,就連一條
 7             tr[p][f] = np; // fail[p]是沒法被p表示(right不一樣)的最長後綴們
 8             p = fail[p]; // 
 9         } // 
10         if(!p) { // 
11             fail[np] = 1; // 若是全都沒有,插入結束
12         } // 
13         else { // 此時有一個轉移邊,此時p是某個後綴
14             int Q = tr[p][f]; // Q是某個子串,跟最後若干位相同
15             if(len[Q] == len[p] + 1) { // 若是Q僅僅表示一個串
16                 fail[np] = Q; // 那麼把new p的fail指向Q,告辭
17             } // 
18             else { // 不然Q表明的不是一個串,在p的後面加入一個字符的同時,前面多了些字符
19                 int nQ = ++top; // 此時新建new Q表明串"p+插入的字符",至關於把Q分開成兩部分
20                 len[nQ] = len[p] + 1;  // 長度天然是p + 1
21                 fail[nQ] = fail[Q]; // 分出來的是Q的一個後綴,繼承fail
22                 fail[Q] = fail[np] = nQ; // Q之後就要先跳到new Q,np也是
23                 memcpy(tr[nQ], tr[Q], sizeof(tr[Q])); // 由於是分離,繼承全部轉移邊
24                 while(tr[p][f] == Q) { // 此時的p沒有Q長,p的f轉移邊其實都是到new Q的,只不過之前new Q沒有單獨的節點,因此給了Q
25                     tr[p][f] = nQ; // 如今new Q收回給本身的轉移邊
26                     p = fail[p]; // 
27                 } // 
28             } // 
29         } // 
30         return; // 
31     } // 

 還有實例幫助理解:接下來就要用串*******bca來作示範。函數

 1 inline void insert(char c) { // 
 2         int f = c - 'a'; //  此時插入了*******bc
 3         int p = last, np = ++top; // 正在插入a
 4         last = top; // 
 5         len[np] = len[p] + 1; //      p     bc
 6         while(p && !tr[p][f]) { //    Q    xbca
 7             tr[p][f] = np; //         np ***bca
 8             p = fail[p]; //           nQ    bca
 9         } // 
10         if(!p) { // 這種狀況,以前沒有"bca"或"ca"或"a"出現,如 bcibcbca
11             fail[np] = 1; // 
12         } // 
13         else { // 這種狀況,以前出現過"bca",如今跳到了**bc上,出現了一個a的轉移邊
14             int Q = tr[p][f]; // 此時p是bc  Q是(*)bca
15             if(len[Q] == len[p] + 1) { // 這種狀況,Q就是bca,以前出現了若干個bca並且前一個字符不一樣,致使Q不能表示*bca
16                 fail[np] = Q; // 只能表示bca,例:123xbca456ybca789bc a 
17             } // 此時把new p的fail接到Q上便可
18             else { // 這種狀況,Q表示的是*bca,例如:123xbca456xbca789bc a
19                 int nQ = ++top; // 此時Q表明xbca和bca兩個串,他們的right集合(出現位置徹底相同)
20                 len[nQ] = len[p] + 1;  // 此時多出來了一個單獨的bca,咱們新建一個節點new Q來表示
21                 fail[nQ] = fail[Q]; // new Q表示bca,fail指針與以前*bca的指針相同。
22                 fail[Q] = fail[np] = nQ; // 而Q如今只表示xbca一個串了,fail指向bca
23                 memcpy(tr[nQ], tr[Q], sizeof(tr[Q])); // new p的fail指向bca,而不是更長的*bca,是由於以前跳fail的時候停在了p,
24                 while(tr[p][f] == Q) { // 這就代表最後的bca以前的一個字符不可能跟別的bca相同,不爲x。不然p就是xbc
25                     tr[p][f] = nQ; // new Q bca原本就是Q中的一部分,如今分離出來,就繼承了全部出邊
26                     p = fail[p]; // p轉移到Q,說明p比最短的Q(new Q)短。因此p和以上的全部出邊都不會轉移到Q,由於有最後那一個新加的bca
27                 } // 它前方不爲x,因此bc呀c呀都不會直接到xbca上去
28             } // 
29         } // 
30         return; // 
31     } // 

僞裝把插入搞懂了......ui

關於排序,個人理解是這樣的。spa

首先搞出一個桶並統計前綴和。這樣長度爲i的那些點的排名就是bin[i - 1] + 1 ~ bin[i].net

這些點之間是沒有相互關係的,因此每次出來一個長度爲i的點,就挑一個排名給它,咱們挑的是bin[i]指針

以後bin[i]--,表示這個排名已經被用掉了,以後剩餘的排名重新的bin[i]開始。code

注意雖然一號點長度是0可是三個循環都是從1開始,並不會出現問題。htm

用一道例題加深理解。

例題A:hihocoder1465

題意:給定s,屢次詢問t的全部循環同構串在s中出現的次數。

解:對s創建sam。循環同構的處理方法是把串複製一遍,有點像環形區間DP。

在sam上面跑tt,若是長度比t長了,就跳fail。當前長度等於t時統計答案。每一個節點只會被加一次,因此用vis數組表示。

注意,轉移的時候長度+1,跳fail的時候長度變爲len。

  1 #include <cstdio>
  2 #include <algorithm>
  3 #include <cstring>
  4 
  5 typedef long long LL;
  6 const int N = 1000010;
  7 
  8 int tr[N][26], len[N], fail[N], bin[N], topo[N], cnt[N];
  9 int last, top;
 10 char s[N], pp[N];
 11 bool vis[N];
 12 
 13 inline void init() {
 14     top = last = 1;
 15     return;
 16 }
 17 
 18 inline void insert(char c) {
 19     int f = c - 'a';
 20     int p = last, np = ++top;
 21     last = np;
 22     cnt[np] = 1;
 23     len[np] = len[p] + 1;
 24     while(p && !tr[p][f]) {
 25         tr[p][f] = np;
 26         p = fail[p];
 27     }
 28     if(!p) {
 29         fail[np] = 1;
 30     }
 31     else {
 32         int Q = tr[p][f];
 33         if(len[Q] == len[p] + 1) {
 34             fail[np] = Q;
 35         }
 36         else {
 37             int nQ = ++top;
 38             len[nQ] = len[p] + 1;
 39             fail[nQ] = fail[Q];
 40             fail[Q] = fail[np] = nQ;
 41             memcpy(tr[nQ], tr[Q], sizeof(tr[Q]));
 42             while(tr[p][f] == Q) {
 43                 tr[p][f] = nQ;
 44                 p = fail[p];
 45             }
 46         }
 47     }
 48     return;
 49 }
 50 
 51 inline void sort() {
 52     for(int i = 1; i <= top; i++) {
 53         bin[len[i]]++;
 54     }
 55     for(int i = 1; i <= top; i++) {
 56         bin[i] += bin[i - 1];
 57     }
 58     for(int i = 1; i <= top; i++) {
 59         topo[bin[len[i]]--] = i;
 60     }
 61     return;
 62 }
 63 
 64 inline void count() {
 65     for(int a = top; a >= 1; a--) {
 66         int x = topo[a];
 67         cnt[fail[x]] += cnt[x];
 68     }
 69     return;
 70 }
 71 
 72 inline void solve() {
 73     scanf("%s", pp + 1);
 74     int n = strlen(pp + 1);
 75     for(int i = 1; i <= n; i++) {
 76         pp[n + i] = pp[i];
 77     }
 78     LL ans = 0;
 79     int now = 0, p = 1;
 80     for(int i = 1; i <= n * 2; i++) {
 81         int f = pp[i] - 'a';
 82         while(p && !tr[p][f]) {
 83             p = fail[p];
 84             now = len[p];
 85         }
 86         if(tr[p][f]) {
 87             p = tr[p][f];
 88             now++;
 89         }
 90         else {
 91             p = 1;
 92         }
 93         while(len[fail[p]] >= n) {
 94             p = fail[p];
 95             now  = len[p];
 96         }
 97         //printf("i = %d \n", i);
 98         if(now >= n && !vis[p]) {
 99             ans += cnt[p];
100             vis[p] = 1;
101             //printf("ans += %d \n", cnt[p]);
102         }
103     }
104     printf("%lld\n", ans);
105     return;
106 }
107 
108 int main() {
109     scanf("%s", s + 1);
110     init();
111     int n = strlen(s + 1);
112     for(int i = 1; i <= n; i++) {
113         insert(s[i]);
114     }
115     sort();
116     count();
117     int T;
118     scanf("%d", &T);
119     while(T--) {
120         solve();
121         if(T) {
122             memset(vis, 0, sizeof(vis));
123         }
124     }
125 
126     return 0;
127 }
AC代碼

各類例題:

弦論  生成魔咒  品酒大會  差別  優秀的拆分 


廣義後綴自動機:

對trie構建後綴自動機。參考資料  資料B 

對多個串,常見的兩種方法是每次last歸一和添加分隔符。

正確的方法是每次last歸一,而後把insert魔改一下。

大概長這樣:

 1 inline int split(int p, int f) {
 2     int Q = tr[p][f], nQ = ++tot;
 3     len[nQ] = len[p] + 1;
 4     fail[nQ] = fail[Q];
 5     fail[Q] = nQ; // 這裏不用管fail[np] 
 6     memcpy(tr[nQ], tr[Q], sizeof(tr[Q]));
 7     while(tr[p][f] == Q) {
 8         tr[p][f] = nQ;
 9         p = fail[p];
10     }
11     return nQ;
12 }
13 
14 inline int insert(int p, char c) { // 直接傳入p,返回值是last,下一次當p用。
15     int f = c - 'a';
16     if(tr[p][f]) { //若是有轉移邊了(別的串上有)
17         int Q = tr[p][f];
18         if(len[Q] == len[p] + 1) { // 判斷是否表示這一個,不然新建節點。
19             return Q;
20         }
21         return split(p, f); // split,分離出這個串。
22     }
23     int np = ++tot;
24     len[np] = len[p] + 1;
25     while(p && !tr[p][f]) {
26         tr[p][f] = np;
27         p = fail[p];
28     }
29     if(!p) {
30         fail[np] = 1;
31     }
32     else {
33         int Q = tr[p][f];
34         if(len[Q] == len[p] + 1) {
35             fail[np] = Q;
36         }
37         else {
38             fail[np] = split(p, f); // 這裏直接調用分離函數便可。
39         }
40     }
41     return np;
42 }

例題: 

字符串  bzoj2780  找相同字符  bzoj5137  你的名字 

相關文章
相關標籤/搜索