后缀自动机
后缀自动机基本描述
后缀自动机:对于一个字符串,它对应的后缀自动机是一个最小的确定有限状态自动机,接受且仅接受的后缀。
栗子:对于字符串S = “aabbabd”,它的后缀自动机:
其中红色状态是终结状态。对于的后缀,都可以从状态出发沿着字符标识路径转移,最后到达终结状态。特别的,对于的子串,最终会走到一个合法状态;若不是的子串,最后会无路可走。
后缀自动机中的状态
首先介绍子串的结束位置集合:对于的一个子串,为在中所有出现的结束位置集合。
把所有子串的求出后,相同的集合归为一个状态。
栗子:S = “aabbabd”
状态 | 子串 | endpos |
---|---|---|
S | 空串 | {0, 1, 2, 3, 4, 5, 6} |
1 | a | {1, 2, 5} |
2 | aa | {2} |
3 | aab | {3} |
4 | aabb, abb, bb | {4} |
5 | b | {3, 4, 6} |
6 | aabba, abba, bba, ba | {5} |
7 | aabbab, abbab, bbab, bab | {6} |
8 | ab | {3, 6} |
9 | aabbabd, abbabd, bbabd, babd, abd, bd, d | {7} |
正好对应上图的状态。
设状态为,表示状态中的所有子串集合,表示中最长的子串,表示中最短的子串。
一些性质:
- 对于的两个子串、,不妨设,那么是的后缀当且仅当,不是的后缀当且仅当。
- 对于一个状态,和任意,都有是的后缀。
- 对于一个状态,和任意的后缀,如果满足,那么。即包含的一系列连续后缀。
后缀自动机的Suffix Links
包含的一系列连续后缀,但是这连续后缀会在某个地方断掉。如,后缀到bab时,下一个ab没有出现在中,因为ab的集合大于bab的集合,于是被分配到一个新状态。
可以发现一条状态序列,对应即aabbab的后缀依次在7、8、5、S中。用Suffix links连起来,即是上图中的绿色虚线。
一个状态的Suffix Link连接到中最长的后缀,且其集合与的集合不同。因此,其中表示的Suffix Link。
后缀自动机的Transition Function
对于一个状态,将遇到的下一个字符集合记作。
可以发现中的子串接上一个字符后,新的子串仍属于同一个状态。
因此定义转移函数:
线性时间构造后缀自动机
用增量法构造对应的SAM。假设已经构造好了的SAM,这时要添加字符,因此新增了个后缀,这些后缀从和空串转移过来。设对应的状态是,这些串对应的状态正好是到初始状态的由Suffix Links连接起来路径上的状态。称这条路径上的所有状态集合是suffix-path (u -> S)。
显然这个后缀不能被识别,因此至少要增加一个状态,至少包含这个后缀。
设为转移函数,为状态的Suffix Link
分情况讨论:
-
对于suffix-path (u -> S)的任意状态,都有,此时只要令,即可。
栗子:
-
存在suffix-path (u -> S)中的状态,使得。这意味着已经存在某个字符串,其中是的后缀,且已经作为的子串出现。则的Suffix Link应连向一个状态,这个状态中,。这时要分两种情况:
-
直接将的Suffix Link连向即可。
栗子:
-
这时不只对应长度为的后缀,只能将状态拆开。
新建一个状态,
将原先转移指向的状态中的状态指向,的状态指向,并把的Suffix Link指向原先的Suffix Link,将和的Suffix Link指向。
栗子:
-
后缀自动机模板
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e6 + 10 ;
int sz, last ;
char s[maxn] ;
struct node {
int len, link, nxt[26] ;
}st[maxn] ;
void sam_init () {
st[0].len = 0; st[0].link = -1 ;
sz ++; last = 0 ;
}
void insert (int c) {
int cur = sz ++ ;
st[cur].len = st[last].len + 1 ;
int p = last ;
while (p != -1 && !st[p].nxt[c]) {
st[p].nxt[c] = cur; p = st[p].link ;
}
if (p == -1) {
st[cur].link = 0; last = cur; return ;
}
int q = st[p].nxt[c] ;
if (st[p].len + 1 == st[q].len) {
st[cur].link = q ;
} else {
int clone = sz ++ ;
st[clone].len = st[p].len + 1 ;
for (int i = 0; i < 26; i ++) st[clone].nxt[i] = st[q].nxt[i] ;
st[clone].link = st[q].link ;
while (p != -1 && st[p].nxt[c] == q) {
st[p].nxt[c] = clone; p = st[p].link ;
}
st[q].link = st[cur].link = clone ;
}
last = cur ;
}
int main() {
scanf("%s", s + 1) ;
int len = strlen (s + 1) ;
sam_init () ;
for (int i = 1; i <= len; i ++) insert (s[i] - 'a') ;
return 0 ;
}
其中状态中的表示,为Suffix Link,为转移函数。
例题
hihocoder 1445 后缀自动机二·重复旋律5
**题意:**求一个字符串中不同子串的数量。
**题解:**对于后缀自动机中的两个状态、,。因此即求。又知道到与一一对应,,因此最后求。建出后缀自动机后暴力求和即可。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e6 + 10 ;
int sz, last ;
char s[maxn] ;
struct node {
int len, link, nxt[26] ;
}st[maxn] ;
void sam_init () {
st[0].len = 0; st[0].link = -1 ;
sz ++; last = 0 ;
}
void insert (int c) {
int cur = sz ++ ;
st[cur].len = st[last].len + 1 ;
int p = last ;
while (p != -1 && !st[p].nxt[c]) {
st[p].nxt[c] = cur; p = st[p].link ;
}
if (p == -1) {
st[cur].link = 0; last = cur; return ;
}
int q = st[p].nxt[c] ;
if (st[p].len + 1 == st[q].len) {
st[cur].link = q ;
} else {
int clone = sz ++ ;
st[clone].len = st[p].len + 1 ;
for (int i = 0; i < 26; i ++) st[clone].nxt[i] = st[q].nxt[i] ;
st[clone].link = st[q].link ;
while (p != -1 && st[p].nxt[c] == q) {
st[p].nxt[c] = clone; p = st[p].link ;
}
st[q].link = st[cur].link = clone ;
}
last = cur ;
}
int main() {
scanf("%s", s + 1) ;
int len = strlen (s + 1) ;
sam_init () ;
for (int i = 1; i <= len; i ++) insert (s[i] - 'a') ;
long long ans = 0 ;
for (int i = 1; i < sz; i ++) ans += st[i].len - st[st[i].link].len ;
cout << ans << endl ;
return 0 ;
}
hihocoder 1449 后缀自动机三·重复旋律6
**题意:**给定字符串,求对于所有的,长度为的出现次数最多的子串的出现次数。
**题解:**后缀自动机中状态的集合的大小即为的出现次数。考虑如何算出每个状态的集合大小。
先构建后缀自动机:以S = "aabbabd"为例
不考虑转移函数,只考虑Suffix Links。并且,如果一个状态能接受的前缀,将该状态标为绿色。
状态 | 子串 | endpos |
---|---|---|
S | 空串 | {0, 1, 2, 3, 4, 5, 6} |
1 | a | {1, 2, 5} |
2 | aa | {2} |
3 | aab | {3} |
4 | aabb, abb, bb | {4} |
5 | b | {3, 4, 6} |
6 | aabba, abba, bba, ba | {5} |
7 | aabbab, abbab, bbab, bab | {6} |
8 | ab | {3, 6} |
9 | aabbabd, abbabd, bbabd, babd, abd, bd, d | {7} |
Suffix Links把后缀自动机中的状态连成了一棵树,且祖孙之间的有包含关系,非祖孙之间交为空集。
从两个例子来看这个问题:
- ,
当一个状态为绿色节点即它能接受的一个前缀时,它的集合大小比它的儿子的集合大小和大1。
即如果一个状态包含的一个前缀,那么一定有,并且不能继承自的儿子。这时需要+1。
如何标记绿色状态:构造后缀自动机时,每次新建的节点一定是绿色状态,因为对应这个前缀;复制出的状态一定不是绿色状态。
求出每个状态的集合大小后,因为最后的答案一定关于递减,因此每次只需要更新处的值,并从后往前更新一遍。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e6 + 10 ;
int sz, last ;
struct node {
int len, link, nxt[26] ;
}st[maxn] ;
char s[maxn] ;
int head[maxn], to[maxn], nxt[maxn], tot = 1 ;
int ans[maxn], f[maxn] ;
bool flag[maxn] ;
void addEdge (int u, int v) {
to[++ tot] = v; nxt[tot] = head[u]; head[u] = tot ;
}
void sam_init () {
st[0].len = 0; st[0].link = -1 ;
sz ++; last = 0 ;
}
void insert (int c) {
int cur = sz ++; flag[cur] = 1 ;
st[cur].len = st[last].len + 1 ;
int p = last ;
while (p != -1 && !st[p].nxt[c]) {
st[p].nxt[c] = cur; p = st[p].link ;
}
if (p == -1) {
st[cur].link = 0; last = cur; return ;
}
int q = st[p].nxt[c] ;
if (st[p].len + 1 == st[q].len) {
st[cur].link = q ;
} else {
int clone = sz ++ ;
st[clone].len = st[p].len + 1 ;
for (int i = 0; i < 26; i ++) st[clone].nxt[i] = st[q].nxt[i] ;
st[clone].link = st[q].link ;
while (p != -1 && st[p].nxt[c] == q) {
st[p].nxt[c] = clone; p = st[p].link ;
}
st[q].link = st[cur].link = clone ;
}
last = cur ;
}
int dfs (int v) {
if (f[v]) return f[v] ;
int res = 0 ;
for (int i = head[v]; i; i = nxt[i])
res += dfs (to[i]) ;
return f[v] = res + flag[v] ;
}
int main() {
scanf("%s", s + 1) ;
int len = strlen (s + 1) ;
sam_init () ;
for (int i = 1; i <= len; i ++) insert (s[i] - 'a') ;
//for (int i = 0; i < sz; i ++) printf("node %d: len:%d link:%d a->%d b->%d\n", i, st[i].len, st[i].link, st[i].nxt[0], st[i].nxt[1]) ;
for (int i = 1; i < sz; i ++) addEdge (st[i].link, i) ;
dfs (0) ;
for (int i = 1; i < sz; i ++) ans[st[i].len] = max (ans[st[i].len], f[i]) ;
for (int i = len - 1; i >= 1; i --) ans[i] = max (ans[i], ans[i + 1]) ;
for (int i = 1; i <= len; i ++) printf("%d\n", ans[i]) ;
return 0 ;
}
hihocoder 1457 后缀自动机四·重复旋律7
**题意:**给定个数字串,求每个串中所有不同的数字子串在十进制下的总和。
**题解:**将个数字串连在一起,中间用":"隔开,构建猴嘴自动机。设为状态中的子串和。
其中表示状态中不含":"的子串个数,恰好等于从初始状态到状态所有的非冒号边的数量。
按照拓扑排序转移即可。注意拓扑排序前度数不能将只有经过冒号边才能到达的边计算进去。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll ;
const int maxn = 4e6 + 10, mod = 1e9 + 7 ;
int sz, last ;
struct node {
int len, link, nxt[11] ;
}st[maxn] ;
char s[maxn] ;
int deg[maxn], cnt[maxn] ;
ll f[maxn] ;
bool vis[maxn] ;
void sam_init () {
st[0].len = 0; st[0].link = -1 ;
sz ++; last = 0 ;
}
void insert (int c) {
int cur = sz ++ ;
st[cur].len = st[last].len + 1 ;
int p = last ;
while (p != -1 && !st[p].nxt[c]) {
st[p].nxt[c] = cur; p = st[p].link ;
}
if (p == -1) {
st[cur].link = 0; last = cur; return ;
}
int q = st[p].nxt[c] ;
if (st[p].len + 1 == st[q].len) {
st[cur].link = q ;
} else {
int clone = sz ++ ;
st[clone].len = st[p].len + 1 ;
for (int i = 0; i < 11; i ++) st[clone].nxt[i] = st[q].nxt[i] ;
st[clone].link = st[q].link ;
while (p != -1 && st[p].nxt[c] == q) {
st[p].nxt[c] = clone; p = st[p].link ;
}
st[cur].link = st[q].link = clone ;
}
last = cur ;
}
void bfs () {
queue<int> que ;
que.push (0); vis[0] = 1 ;
while (!que.empty()) {
int v = que.front(); que.pop() ;
for (int i = 0; i < 10; i ++) {
int to = st[v].nxt[i] ;
if (!to) continue ;
if (!vis[to]) que.push (to) ;
deg[to] ++; vis[to] = 1 ;
}
}
}
void topsort () {
queue<int> que ;
que.push (0); cnt[0] = 1 ;
while (!que.empty()) {
int v = que.front(); que.pop() ;
for (int i = 0; i < 10; i ++) {
int to = st[v].nxt[i] ;
if (!to) continue ;
if (-- deg[to] == 0) que.push (to) ;
cnt[to] += cnt[v] ;
f[to] = (f[to] + f[v] * 10 + cnt[v] * i) % mod ;
}
}
}
int main() {
int n ;
scanf("%d", &n) ;
sam_init () ;
for (int i = 1; i <= n; i ++) {
scanf("%s", s + 1) ;
int len = strlen (s + 1) ;
for (int j = 1; j <= len; j ++) insert (s[j] - '0') ;
if (i != n) insert (10) ;
}
bfs () ;
topsort () ;
//for (int i = 1; i < sz; i ++) printf("node:%d cnt:%d f:%lld\n", i, cnt[i], f[i]) ;
ll ans = 0 ;
for (int i = 1; i < sz; i ++) ans = (ans + f[i]) % mod ;
printf("%lld\n", ans) ;
return 0 ;
}
hihocoder 1465 后缀自动机五·重复旋律8
**题意:**给定一个大串,多次询问一个小串和大串中的多少子串循环相似。
**题解:**首先对大串建后缀自动机。每个询问的小串将自己的一份复制接在后面。设小串长度为。
先考虑如何用后缀自动机求串以第个字符结尾的与串的最长公共子串。
设一个二元组表示当前在后缀自动机上的状态,最长公共子串长度为。初始化,
求出的后,如何求的:
- 若存在:,
- 若不存在:沿着suffix-path (u -> S)找到一个状态满足存在,令,。若也不存在,令,。
若,那么就得到一个循环同构的公共子串。
还要注意两个特殊情况:
- 的循环同构串可能会有相同的情况,需要记录状态中的有没有在时到达过。
- 属于状态。当时,可能不属于状态。因此沿着suffix-path (u -> S)上找离最近的使,统计。找到后令。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 10 ;
int sz, last ;
struct node {
int len, link, nxt[26] ;
}st[maxn] ;
char s[maxn] ;
int head[maxn], to[maxn], nxt[maxn], tot = 1 ;
int f[maxn] ;
bool vis[maxn], flag[maxn] ;
void sam_init () {
st[0].len = 0; st[0].link = -1 ;
sz ++; last = 0 ;
}
void insert (int c) {
int cur = sz ++; flag[cur] = 1 ;
st[cur].len = st[last].len + 1 ;
int p = last ;
while (p != -1 && !st[p].nxt[c]) {
st[p].nxt[c] = cur; p = st[p].link ;
}
if (p == -1) {
st[cur].link = 0; last = cur; return ;
}
int q = st[p].nxt[c] ;
if (st[p].len + 1 == st[q].len) {
st[cur].link = q ;
} else {
int clone = sz ++ ;
st[clone].len = st[p].len + 1 ;
for (int i = 0; i < 26; i ++) st[clone].nxt[i] = st[q].nxt[i] ;
st[clone].link = st[q].link ;
while (p != -1 && st[p].nxt[c] == q) {
st[p].nxt[c] = clone; p = st[p].link ;
}
st[cur].link = st[q].link = clone ;
}
last = cur ;
}
void addEdge (int u, int v) {
to[++ tot] = v; nxt[tot] = head[u]; head[u] = tot ;
}
int dfs (int v) {
int res = 0 ;
for (int i = head[v]; i; i = nxt[i])
res += dfs (to[i]) ;
return f[v] = res + flag[v] ;
}
int main() {
sam_init () ;
scanf("%s", s + 1) ;
int len = strlen (s + 1) ;
for (int i = 1; i <= len; i ++) insert (s[i] - 'a') ;
for (int i = 1; i < sz; i ++) addEdge (st[i].link, i) ;
dfs (0) ;
int n ;
scanf("%d", &n) ;
for (int i = 1; i <= n; i ++) {
memset (vis, 0, sizeof vis) ;
scanf("%s", s + 1) ;
len = strlen (s + 1) ;
for (int j = len + 1; j < 2 * len; j ++) s[j] = s[j - len] ;
int u = 0, l = 0, ans = 0 ;
for (int j = 1; j <= len * 2 - 1; j ++) {
while (u != 0 && !st[u].nxt[s[j] - 'a']) {
u = st[u].link; l = st[u].len ;
}
if (st[u].nxt[s[j] - 'a']) {
u = st[u].nxt[s[j] - 'a'], l ++ ;
} else {
u = 0; l = 0 ;
}
if (l > len) {
while (st[st[u].link].len >= len) {
u = st[u].link; l = st[u].len ;
}
}
if (l >= len && !vis[u]) {
vis[u] = 1 ;
ans += f[u] ;
}
}
printf("%d\n", ans) ;
}
return 0 ;
}
上一篇: POI报表新手入门案例(一)