后缀自动机

后缀自动机(Suffix Automaton,简称SAM)。对于一个字符串$S$,对应的后缀自动机是一个最小的确定有限状态自动机(DFA),接受且只接受$S$的后缀。

一、SAM的States

0.定义子串的结束位置集合endpos。对于S的一个子串s,endpos(s)=s在S中所有出现的结束位置集合。如果两个子串的endpos集合相等,咱就把他们放到一个状态里去。

1.对于S的两个子串s1,s2,不妨设$|s1|<|s2|$,则$\text{s1 is a suffix of s2} \iff endpos(s1) \supseteq endpos(s2)$,$\text{s1 is not a suffix of s2} \iff endpos(s1) \cap endpos(s2) = \varnothing$。

证明:先证明$\text{s1 is a suffix of s2} \Longrightarrow endpos(s1) \supseteq endpos(s2)$。因为s1是s2的后缀,所以每次s2出现的时候s1也会出现,所以有$endpos(s1) \supseteq endpos(s2)$。再证明$\text{s1 is a suffix of s2} \Longleftarrow endpos(s1) \supseteq endpos(s2)$。对于s的子串s2,$endpos(s2) \neq \varnothing$,所以$endpos(s1) \supseteq endpos(s2) \Longrightarrow $存在结束位置x使s1,s2都结束于x。因为$|s1|<|s2|$,所以s1是s2的后缀。

 2.咱用$substring(st)$表示状态$st$中包含的所有子串的集合,$longest(st)$表示$st$包含的最长的子串,$shortest(st)$表示$st$包含的最短的子串。

对于一个状态$st$,以及任意$s \in substring(st)$,都有$s$是$longest(st)$的后缀。

对于一个状态$st$,以及任意的$longest(st)$的后缀$s$,如果$|shortest(st)| \leqslant |s| \leqslant |longest(st)|$,则$s \in substring(st)$。

所以$substring(st)$包含的是$longest(st)$的一系列连续后缀。

二、SAM的Suffix Links

上面说$substring(st)$包含的是$longest(st)$的一系列连续后缀。这连续的后缀会在某处断掉。当$longest(s)$的某后缀$s$在新的地方出现时,就会断掉,$s$会属于新的状态。咱用Suffix Links从某一状态$st$一直连到初始状态$init$,这条路上的所有$substring$恰好即为$longest(st)$的所有后缀。

如果$st$的Suffix Link指向$fa$,那么$|shortest(st)|=|longest(fa)|+1$。

以Suffix Links为边,所有的节点组成了一棵树,咱称为前缀树。

三、SAM的Transition Function

$u$到$v$有一条字符为$c$的转移边,表示$u$所表示的所有子串加上$c$后,都可以由$v$表示。但不一定$v$所表示的所有子串都是由$u$的转移而来。

四、习题

1.求$S$的子串的数目。

$ANS=\sum_{st}(|longest(st)|-|shortest(st)|+1)$。

2.求$S$的长度为$1...|S|$的子串出现次数最多的子串出现的次数。

SAM所有状态的字符串的并就是$S$的所有子串。$|endpos(st)|$就是$s \in substring(st)$的子串$s$出现的次数。

求$|endpos(st)|$的方法:对于一个状态,如果它表示了一个新的后缀,则它的$endpos$集合中多一个位置,否则它的$endpos$集合为儿子(前缀树)的$endpos$集合的并。咱只要求出拓扑序,递推出$|endpos|$就行。

一个很朴素的想法如下:

FOREACH STATE st:
FOR i = minlen(st) ... maxlen(st):
	ans[i] = max(ans[i], |endpos(st)|)

显然会TLE。咱注意到$ans[i]$随$i$递增而递减。所以咱对于每个状态$st$都只更新$ans[longest(st)]$,然后倒着扫描一遍取max。

FOREACH STATE st:
	ans[maxlen(st)] = |endpos(st)|
FOR i = |S| - 1 ... 1
	ans[i] = max(ans[i], ans[i+1]);

3.求两个字符串的最长公共子串

对$A$建SAM。当前走到的状态为$st$,最长公共子串的长度为$l$。先开始$st=init,l=0$。处理到$B_i$时,如果存在$trans(st, B_i)$,那就走下去并把l++。如果不存在就向Suffix Link上跳。如果$trans(init,B_i)$都不存在,就重新令$st=init, l = 0$,否则$l=maxlen(st)+1, st=trans(st, B_i)$。

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<iostream>
 4 #include<algorithm>
 5 using namespace std;
 6 template < int N >
 7 struct SuffixAutomaton {
 8     struct Node {
 9         Node *ch[26], *fa; int mx;    
10     } *start, *last, *C, mem[N<<1];
11     inline Node* newNode(int v = 0) {
12         memset(C, 0, sizeof(*C));
13         C->mx = v; return C++;
14     }
15     void extend(int c) {
16         Node *u = newNode(last->mx + 1), *v = last;
17         for (; v && !v->ch[c]; v = v->fa) v->ch[c] = u;
18         if (!v) u->fa = start;
19         else if (v->ch[c]->mx == v->mx + 1) u->fa = v->ch[c];
20         else {
21             Node *n = newNode(v->mx + 1), *o = v->ch[c];
22             memcpy(n->ch, o->ch, sizeof(o->ch));
23             n->fa = o->fa; o->fa = u->fa = n;
24             for (; v && v->ch[c] == o; v = v->fa) v->ch[c] = n;
25         } last = u;
26     }
27     inline void init() {
28         C = mem; start = last = newNode();
29     }
30     void solve(int n, const char *str) {
31         Node *u = start; int ans = 0, l = 0;
32         for (int c, i = 0; i < n; ++i) {
33             c = str[i] - 'a';
34             if (u->ch[c]) {
35                 u = u->ch[c], ++l;
36             } else {
37                 while (u && !u->ch[c]) u = u->fa;
38                 if (!u) u = start, l = 0;
39                 else l = u->mx + 1, u = u->ch[c];
40             }
41             ans = max(ans, l);
42         }
43         printf("%d\n", ans);
44     }
45 };
46 SuffixAutomaton < 250005 > SAM;
47 char s[250005];
48 int main() {
49     SAM.init();
50     scanf("%s", s); int n = strlen(s);
51     for (int i = 0; i < n; ++i)
52         SAM.extend(s[i] - 'a');
53     scanf("%s", s);
54     SAM.solve(strlen(s), s);
55     return 0;    
56 }
View Code

4.求多个字符串的最长公共子串

对其中一个建SAM,其他的就在SAM上跑,跟上面的类似但又有一些不同,就是更新$st$的同时还要更新$fa_{st}$。

 1 #include<cstdio>
 2 #include<vector>
 3 #include<cstring>
 4 #include<iostream>
 5 #include<algorithm>
 6 using namespace std;
 7 inline void gmin(int &x, int y) {
 8     if (x > y) x = y;    
 9 }
10 inline void gmax(int &x, int y) {
11     if (x < y) x = y;    
12 }
13 template < int N >
14 struct SuffixAutomaton {
15     struct Node {
16         Node *ch[26], *fa; int mx, val, mval;
17     } *start, *last, *C, mem[N<<1];
18     vector < Node* > tp; int T;
19     inline Node* newNode(int v = 0) {
20         memset(C, 0, sizeof(*C));
21         C->mx = v; return C++;
22     }
23     inline void init() {
24         C = mem; start = last = newNode(); T = 0;
25     }
26     void extend(int c) {
27         Node *u = newNode(last->mx + 1), *v = last;
28         for (; v && !v->ch[c]; v = v->fa) v->ch[c] = u;
29         if (!v) u->fa = start;
30         else if (v->ch[c]->mx == v->mx + 1) u->fa = v->ch[c];
31         else {
32             Node *n = newNode(v->mx + 1), *o = v->ch[c];
33             memcpy(n->ch, o->ch, sizeof(o->ch));
34             n->fa = o->fa; o->fa = u->fa = n;
35             for (; v && v->ch[c] == o; v = v->fa) v->ch[c] = n;
36         } last = u;
37     }
38     inline void append(const char *str) {
39         int n = strlen(str);
40         for (int i = 0; i < n; ++i)
41             extend(str[i] - 'a');
42     }
43     inline void query(const char *str) {
44         int n = strlen(str), l = 0;
45         Node *u = start;
46         for (int i = 0, c; i < n; ++i) {
47             c = str[i] - 'a';
48             if (u->ch[c]) u = u->ch[c], ++l;
49             else {
50                 while (u && !u->ch[c]) u = u->fa;
51                 if (!u) u = start, l = 0;
52                 else l = u->mx + 1, u = u->ch[c];
53             } gmax(u->val, l);
54         }
55         for (int i = tp.size() - 1; i; --i) {
56             gmin(tp[i]->mval, tp[i]->val);
57             if (tp[i]->fa)
58                 gmax(tp[i]->fa->val, min(tp[i]->fa->mx, tp[i]->val));
59             tp[i]->val = 0;
60         }
61     }
62     void toposort() {
63         static int cnt[N<<1]; int m = 0;
64         for (Node *u = mem; u != C; ++u)
65             ++cnt[u->mx], gmax(m, u->mx);
66         for (int i = 1; i <= m; ++i) cnt[i] += cnt[i-1];
67         tp.resize(C - mem);
68         for (Node *u = mem; u != C; ++u)
69             tp[--cnt[u->mx]] = u;
70         fill_n(cnt, m + 1, 0);
71         for (Node *u = mem; u != C; ++u)
72             u->mval = u->mx;
73     }
74     void solve() {
75         int ans = 0;
76         for (Node *u = mem; u != C; ++u)
77             gmax(ans, u->mval);
78         printf("%d\n", ans);
79     }
80 };
81 SuffixAutomaton < 100005 > SAM;
82 char s[100005];
83 int main() {
84     SAM.init();
85     scanf("%s", s);
86     SAM.append(s);
87     SAM.toposort();
88     while (~scanf("%s", s))
89         SAM.query(s);
90     SAM.solve();
91     return 0;
92 }
View Code

5.题意自己看

同后缀数组处理很多字符串时的做法一样,咱用一个没有出现的字符连起来,这里选'9'+1即':'。

咱用$sum(st)$表示状态$st$里的子串的和,$valid-substring(st)$表示状态$st$中不含':'的后缀的数量。就有$sum(v)=\sum_{trans(u,c)=v}sum(u) \times 10 + c \times valid-substring(u)$。

$valid-substring(st)$恰为从$init$出发经过不含‘:'的路径到达$st$的路径条数。拓扑排序就行了!

 1 #include<cstdio>
 2 #include<vector>
 3 #include<cstring>
 4 #include<iostream>
 5 #include<algorithm>
 6 using namespace std;
 7 const int mo = 1e9 + 7;
 8 typedef long long ll;
 9 template < int N >
10 struct SuffixAutomaton {
11     struct Node {
12         Node *ch[11], *fa; int mx, sum, cnt;
13     } *start, *last, mem[N<<1], *C;
14     vector < Node* > a;
15     inline Node* newNode(int v = 0) {
16         memset(C, 0, sizeof(*C));
17         C->mx = v; return C++;
18     }
19     void init() {
20         C = mem; start = last = newNode();    
21     }
22     void extend(int c) {
23         Node *u = newNode(last->mx + 1), *v = last;
24         for (; v && !v->ch[c]; v = v->fa) v->ch[c] = u;
25         if (!v) u->fa = start;
26         else if (v->ch[c]->mx == v->mx + 1) u->fa = v->ch[c];
27         else {
28             Node *n = newNode(v->mx + 1), *o = v->ch[c];
29             memcpy(n->ch, o->ch, sizeof(n->ch));
30             n->fa = o->fa; o->fa = u->fa = n;
31             for (; v && v->ch[c] == o; v = v->fa) v->ch[c] = n;
32         } last = u;
33     }
34     inline void append(int n, const char *str) {
35         for (int i = 0; i < n; ++i)
36             extend(str[i] - '0');
37     }
38     void toposort() {
39         static int cnt[N<<1];
40         int m = 0; a.resize(C - mem);
41         for (Node *u = mem; u != C; ++u)
42             ++cnt[u->mx], m = max(m, u->mx);
43         for (int i = 1; i <= m; ++i) cnt[i] += cnt[i-1];
44         for (Node *u = mem; u != C; ++u)
45             a[--cnt[u->mx]] = u;
46         fill_n(cnt, m + 1, 0);
47     }
48     void solve() {
49         toposort(); Node *u, *v; start->cnt = 1;
50         for (int t = 0; t < a.size(); ++t) {
51             u = a[t]; for (int c = 0; c < 10; ++c)
52                 if (v = u->ch[c]) v->cnt += u->cnt;
53         }
54         for (int t = 0; t < a.size(); ++t) {
55             u = a[t]; for (int c = 0; c < 10; ++c) if (v = u->ch[c])
56                 (v->sum += u->sum * 10ll % mo + c * u->cnt % mo) %= mo;
57         }
58         int ans = 0;
59         for (u = mem; u != C; ++u)
60             (ans += u->sum) %= mo;
61         printf("%d\n", ans);
62     }
63 };
64 SuffixAutomaton < 1000005 > SAM;
65 int n;
66 char s[1000010];
67 int main() {
68     scanf("%d", &n); SAM.init();
69     for (int i = 0; i < n; ++i) {
70         scanf("%s", s); if (i) SAM.extend(10);
71         SAM.append(strlen(s), s);
72     } SAM.solve();
73     return 0;    
74 }
View Code

6.题意自己看

对$S$建一个SAM。处理循环同构咱就只用把$str$再复制一遍接到后面去变成$strstr$就行了。记$T_i$为以$str_i$结尾的$S$和$strstr$的最长公共子串的长度。如果$T_i \geqslant |str|$就可以用来更新答案了。但是要注意的是当前状态(包含了$str[i-l+1...i]$)不一定包含$str[i-n+1...i]$。要沿着Suffix Link跳直到$minlen(st) \leqslant |str| \leqslant maxlen(st)$为止。统计后打一个标记以免重复统计。

 1 #include<vector>
 2 #include<cstdio>
 3 #include<cstring>
 4 #include<iostream>
 5 #include<algorithm>
 6 using namespace std;
 7 template < int N >
 8 struct SuffixAutomaton {
 9     struct Node {
10         Node *ch[26], *fa; int mx, cnt, vis;
11     } *start, *last, *C, mem[N<<1];
12     vector < Node* > tp;
13     inline Node* newNode(int v = 0) {
14         memset(C, 0, sizeof(*C));
15         C->mx = v; return C++;
16     }
17     inline void init() {
18         C = mem; start = last = newNode();    
19     }
20     inline void toposort() {
21         static int cnt[N<<1]; int m = 0;
22         for (Node *u = mem; u != C; ++u)
23             ++cnt[u->mx], m = max(m, u->mx);
24         tp.resize(C - mem);
25         for (int i = 1; i <= m; ++i) cnt[i] += cnt[i-1];
26         for (Node *u = mem; u != C; ++u)
27             tp[--cnt[u->mx]] = u;
28         for (int i = tp.size() - 1; i > 0; --i)
29             tp[i]->fa->cnt += tp[i]->cnt;
30     }
31     void extend(int c) {
32         Node *u = newNode(last->mx + 1), *v = last;
33         for (; v && !v->ch[c]; v = v->fa) v->ch[c] = u;
34         if (!v) u->fa = start;
35         else if (v->ch[c]->mx == v->mx + 1) u->fa = v->ch[c];
36         else {
37             Node *n = newNode(v->mx + 1), *o = v->ch[c];
38             memcpy(n->ch, o->ch, sizeof(o->ch));
39             n->fa = o->fa; o->fa = u->fa = n;
40             for (; v && v->ch[c] == o; v = v->fa) v->ch[c] = n;
41         } u->cnt = 1; last = u;
42     }
43     void solve(const char *str, int n, int m) {
44         static int T = 1;
45         Node *u = start; int l = 0, ans = 0;
46         for (int c, i = 0; i < m; ++i) {
47             c = str[i] - 'a';
48             if (u->ch[c]) u = u->ch[c], ++l;
49             else {
50                 while (u && !u->ch[c]) u = u->fa;
51                 if (!u) u = start, l = 0;
52                 else l = u->mx + 1, u = u->ch[c];
53             }
54             if (l > n) while (u->fa->mx >= n)
55                 u = u->fa, l = u->mx;
56             if (l >= n && u->vis != T)
57                 u->vis = T, ans += u->cnt;
58         }
59         ++T;
60         printf("%d\n", ans);
61     }
62 };
63 SuffixAutomaton < 100005 > SAM;
64 char s[200005];
65 int main() {
66     SAM.init();
67     scanf("%s", s);
68     for (int i = 0; s[i]; ++i)
69         SAM.extend(s[i] - 'a');
70     SAM.toposort();
71     int n; scanf("%d", &n);
72     for (int i = 0; i < n; ++i) {
73         scanf("%s", s);
74         int m = strlen(s);
75         for (int i = 0; i < m - 1; ++i)
76             s[i+m] = s[i];
77         SAM.solve(s, m, 2 * m - 1);
78     }
79     return 0;
80 }
View Code

 7.不重复字典序第k小的字符串

从$init$到每一个点的路径都构成了一个子串,且这些子串互不相同。求出从每个点再往下走不同子串的数目。然后询问的时候暴力枚举每一个点的每一个儿子找第k个就行了。

 1 #include<vector>
 2 #include<cstdio>
 3 #include<cstring>
 4 #include<iostream>
 5 #include<algorithm>
 6 using namespace std;
 7 template < int N >
 8 struct SuffixAutomaton {
 9     struct Node {
10         Node *ch[26], *fa; int mx, cnt;
11     } *start, *last, *C, mem[N<<1];
12     vector < Node* > tp;
13     inline Node* newNode(int v = 0) {
14         memset(C, 0, sizeof(*C));
15         C->mx = v; return C++;
16     }
17     inline void init() {
18         C = mem; start = last = newNode();    
19     }
20     void extend(int c) {
21         Node *u = newNode(last->mx + 1), *v = last;
22         for (; v && !v->ch[c]; v = v->fa) v->ch[c] = u;
23         if (!v) u->fa = start;
24         else if (v->ch[c]->mx == v->mx + 1) u->fa = v->ch[c];
25         else {
26             Node *n = newNode(v->mx + 1), *o = v->ch[c];
27             memcpy(n->ch, o->ch, sizeof(o->ch));
28             n->fa = o->fa; o->fa = u->fa = n;
29             for (; v && v->ch[c] == o; v = v->fa) v->ch[c] = n;
30         } last = u;
31     }
32     void toposort() {
33         static int cnt[N<<1]; int m = 0;
34         tp.resize(C - mem);
35         for (Node *u = mem; u != C; ++u)
36             ++cnt[u->mx], m = max(m, u->mx), u->cnt = 1;
37         for (int i = 1; i <= m; ++i) cnt[i] += cnt[i-1];
38         for (Node *u = mem; u != C; ++u)
39             tp[--cnt[u->mx]] = u;
40         fill_n(cnt, m + 1, 0);
41         for (int i = tp.size() - 1; i > 0; --i) {
42             Node *u = tp[i];
43             for (int c = 0; c < 26; ++c)
44                 if (u->ch[c]) u->cnt += u->ch[c]->cnt;
45         }
46     }
47     void solve(int k) {
48         for (Node *u = start; u && k;) {
49             for (int c = 0; c < 26; ++c) if (u->ch[c]) {
50                 if (u->ch[c]->cnt >= k) {
51                     putchar(c + 'a'); u = u->ch[c]; --k; break;
52                 } else k -= u->ch[c]->cnt;
53             }
54         } putchar('\n');
55     }
56 };
57 SuffixAutomaton < 90005 > SAM;
58 char s[90005];
59 int main() {
60     SAM.init(); scanf("%s", s);
61     for (int i = 0; s[i]; ++i)
62         SAM.extend(s[i] - 'a');
63     SAM.toposort();
64     int n; scanf("%d", &n);
65     for (int t, i = 0; i < n; ++i)
66         scanf("%d", &t), SAM.solve(t);
67     return 0;
68 }
View Code

8.BZOJ 3172: [Tjoi2013]单词

中间弄个分隔符,建出SAM然后求$|endpos(st)|$。

五、

从$init$到$st$路径的条数等于$maxlen(st)-minlen(st)+1$。$s$出现的次数即为$|endpos(st)|(s \in substring(st))$。

posted @ 2018-01-17 22:46  p0ny  阅读(199)  评论(0编辑  收藏  举报