Suffix Automaton
后缀自动机
先上SAM builder,备用链接。之前的垃圾博客,洛谷的某篇教程,饕餮传奇的题单,2012年NOI冬令营陈立杰讲稿。
后缀自动机,点数至多是2n-1!边数至多是3n+k!k是一个小于10的常数。
首先对着代码讲一遍三种插入。
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 } //
假装把插入搞懂了......
关于排序,我的理解是这样的。
首先搞出一个桶并统计前缀和。这样长度为i的那些点的排名就是bin[i - 1] + 1 ~ bin[i]
这些点之间是没有相互关系的,所以每次出来一个长度为i的点,就挑一个排名给它,我们挑的是bin[i]
之后bin[i]--,表示这个排名已经被用掉了,之后剩余的排名从新的bin[i]开始。
注意虽然一号点长度是0但是三个循环都是从1开始,并不会出现问题。
用一道例题加深理解。
例题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 }
各种例题:
广义后缀自动机:
对trie构建后缀自动机。参考资料 资料B (使用正确写法的广义SAM时,不会有多余的节点,即不会遇到资料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 你的名字