后缀自动机学习笔记
# 前言
看见字符串问题就头疼
# 后缀自动机(suffix automaton, SAM)
如果要在一个DAG上表示出一个字符串的所有子串,该如何维护?
一个最简单的方法就是建立一个字典树,并将字符串的所有后缀插入其中。
显然,在绝大多数情况下,上述方案的时空复杂度都是不可接受的。
后缀自动机是一个 DAG
图 与 一棵由suffix link
构成的树 的组合。
下面先列出它的部分性质:
- 每一个节点均代表一个状态,边即为状态的转移(有限自动机)
- 每个转移都为一个字符(如'a', 'b'),同一个节点的各个转移互不相同。
- 存在终止状态,如果从**初始状态**出发,最终转移到终止状态,则转移路径上的所有字符连接起来一定是的后缀。
- 的每一个后缀均可以用一条从出发的路径构成。
# 前置定义
# endpos
对于串的非空子串,记为其在中的所有结束为止。例如:="abcxabc",则。
可以根据endpos
,将串的子串分为若干个等价类,上例中,'bc' 与 'abc'属于同一个类。
SAM中,每一个状态对应一类endpos
。
endpos
有以下几个性质(理解suffix link
的时候要用)
如果非空子串的endpos相同,当且仅当出现时,总是的后缀。
- 结论显然,证明略
对于非空子串(假设),为的后缀则,。
否则,
- 证明:如果至少有一个公共元素,由于是的后缀,因此每当出现时,也会出现。
# 后缀链接 suffix link
对于一个状态对应的等价类,记其对应的串的集合为,其中最长的串为。
可知的前几个后缀(w[1:], w[2:], ...)也属于该等价类,而其他所有后缀(至少有一个,即空后缀)属于其他等价类。记不属于的最长后缀所在的等价类为,则边即为后缀链接。
举个例子:字符串"aabbab",A: endpos('aabbab')={5},B: endpos('ab')={2, 5},C: endpos('b')={2, 3, 5},D: endpos('')={0, 1, 2, 3, 4, 5}。使用后缀链接,依次连接
所有后缀链接构成一颗以初始状态为根的树,这棵树被称为parent tree
# 构造SAM
对于每个节点(状态),我们维护下列信息:
- 所包含的最长子串长度
- 所包含的最短子串长度
- 转移数组
- 后缀链接
从上一张图片中我们可以看到,(证明过程待补充),因此,不必特意维护。
使用增量法构造SAM,从初始状态(空串)开始,逐个插入字符。
分3中情况讨论
记当前到达的状态为,要插入的字符为。我们新建一个状态
对于后缀路径()上的任意状态,若均有,此时只需要令,并设置即可。
由于沿着suffix_link经过的状态均包含原串的后缀,直接添加对应的转移即可。
如图所示,向“aa”中插入字符"b",实线为,虚线为
否则,即存在,记该状态。
若,此时只需要令即可。
否则,我们需要将状态拆分。
已知,如下图所示
显然,此时对于已不再成立,需要将拆分为和两部分。
见代码:
int w = new_node(mx_len[cur] + 1, mx_len[slink[z]] + 1, slink[z], trans[z]);// new_node(mx_len, mn_len, slink, tran); slink[z] = slink[x] = w; mn_len[z] = mn_len[x] = mx_len[w] + 1; while(cur != -1 && trans[cur][c] == z){ trans[cur][c] = w; cur = slink[cur]; }
1
2
3
4
5
6
7
8
9
# 模板
struct SuffixAutomate{
const static int maxnode = maxlen * 2;
int mx_len[maxnode], trans[maxnode][26], slink[maxnode];
int node_cnt = 0, last = 0;
void init(){
node_cnt = 0;
last = new_node(0, -1, NULL);
}
inline int new_node(int mxlen, int slk, int * tran){
mx_len[node_cnt] = mxlen;
slink[node_cnt] = slk;
if(tran != NULL){
memcpy(trans[node_cnt], tran, sizeof(trans[node_cnt]));
}else{
memset(trans[node_cnt], -1, sizeof(trans[node_cnt]));
}
return node_cnt++;
}
void insert_char(char c){
c -= 'a';
int cur = last, x = new_node(mx_len[cur] + 1, -1, NULL);
while(cur != -1 && trans[cur][c] == -1){
trans[cur][c] = x;
cur = slink[cur];
}
if(cur == -1){
slink[x] = 0;
}else{
int z = trans[cur][c];
if(mx_len[cur] + 1 == mx_len[z]){
slink[x] = z;
}else{
int w = new_node(mx_len[cur] + 1, slink[z], trans[z]);
slink[z] = slink[x] = w;
while(cur != -1 && trans[cur][c] == z){
trans[cur][c] = w;
cur = slink[cur];
}
}
}
last = x;
}
}sa;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 应用
# 1. 计算不同子串的个数
问题:给定一个串S,求其不同字串的个数。
构造一个SAM,答案则为每个节点的之和。
# 2. 求每个子串出现的次数
问题:给定一个串S,求其每个子串出现的次数。
构造SAM,插入时每个新建的节点的,而分裂出的点。最后从parent tree
底部开始将自己的累加到父节点的上。
例题AC代码(部分):
char buffer[maxlen];
int cnt[maxlen << 1];
struct SuffixAutomate{
const static int maxnode = maxlen * 2;
int mx_len[maxnode], trans[maxnode][26], slink[maxnode];
int node_cnt = 0, last = 0;
void init(){
node_cnt = 0;
last = new_node(0, -1, NULL);
}
inline int new_node(int mxlen, int slk, int * tran){
mx_len[node_cnt] = mxlen;
slink[node_cnt] = slk;
if(tran != NULL){
memcpy(trans[node_cnt], tran, sizeof(trans[node_cnt]));
}else{
memset(trans[node_cnt], -1, sizeof(trans[node_cnt]));
}
return node_cnt++;
}
void insert_char(char c){
c -= 'a';
int cur = last, x = new_node(mx_len[cur] + 1, -1, NULL);
cnt[x] = 1; // 新节点cnt=1
while(cur != -1 && trans[cur][c] == -1){
trans[cur][c] = x;
cur = slink[cur];
}
if(cur == -1){
slink[x] = 0;
}else{
int z = trans[cur][c];
if(mx_len[cur] + 1 == mx_len[z]){
slink[x] = z;
}else{
// 拆分出来的点cnt设为0,最后dp时得cnt=cnt[z]+cnt[x]+0,
int w = new_node(mx_len[cur] + 1, slink[z], trans[z]);
slink[z] = slink[x] = w;
while(cur != -1 && trans[cur][c] == z){
trans[cur][c] = w;
cur = slink[cur];
}
}
}
last = x;
}
}sa;
int idlist[sa.maxnode];
void solve() {
cin >> buffer;
int u = sa.new_node(0, -1, NULL);
char * p = buffer;
while(*p != 0){
sa.insert_char(*(p++));
}
ll ans = 0;
for(int i=0; i<sa.node_cnt; i++) idlist[i] = i;
// 若两个串在树中为子孙关系,长者深度一定更大
sort(idlist, idlist + sa.node_cnt, [&](int a, int b){
return sa.mx_len[a] > sa.mx_len[b];
});
int nrof = sa.node_cnt;
for(int i=0, v; i<nrof; i++){
v = idlist[i];
cnt[sa.slink[v]] += cnt[v];
if(cnt[v] > 1) ans = max(ans, 1ll * cnt[v] * sa.mx_len[v]);
}
cout << ans << '\n';
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# 3. 求第k大子串
问题:给定一个串S,求字典序第k大的子串(出现位置不同,视为相同/不同字串)。
# 4. 判断两串最长公共子串
参考:
- https://oi-wiki.org/string/sam/
- https://www.cnblogs.com/fengzhiyuan/articles/8492566.html
- https://hihocoder.com/problemset/problem/1441
- https://www.luogu.com.cn/problem/solution/P3804