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 }
AC代码

各种例题:

弦论  生成魔咒  品酒大会  差异  优秀的拆分 


广义后缀自动机:

对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  你的名字 

 

posted @ 2019-01-04 15:43  huyufeifei  阅读(304)  评论(1编辑  收藏  举报
试着放一个广告栏(虽然没有一分钱广告费)

『Flyable Heart 応援中!』 HHG 高苗京铃 闪十PSS 双六 電動伝奇堂 章鱼罐头制作组 はきか 祝姬 星降夜