后缀自动机
后缀自动机(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 }
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 }
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 }
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 }
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 }
8.BZOJ 3172: [Tjoi2013]单词
中间弄个分隔符,建出SAM然后求$|endpos(st)|$。
五、
从$init$到$st$路径的条数等于$maxlen(st)-minlen(st)+1$。$s$出现的次数即为$|endpos(st)|(s \in substring(st))$。