欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

后缀自动机

程序员文章站 2022-04-30 18:25:47
...

后缀自动机基本描述

后缀自动机:对于一个字符串SS,它对应的后缀自动机是一个最小的确定有限状态自动机,接受且仅接受SS的后缀。

栗子:对于字符串S = “aabbabd​”,它的后缀自动机:

后缀自动机

其中红色状态是终结状态。对于SS的后缀,都可以从SS状态出发沿着字符标识路径转移,最后到达终结状态。特别的,对于SS的子串,最终会走到一个合法状态;若不是SS的子串,最后会无路可走。


后缀自动机中的状态

首先介绍子串的结束位置集合endposendpos:对于SS的一个子串ssendpos(s)endpos(s)ssSS中所有出现的结束位置集合。

SS所有子串的endposendpos求出后,相同的endposendpos集合归为一个状态。

栗子: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}

正好对应上图的状态。

设状态为ststsubstrings(st)substrings (st)表示状态stst中的所有子串集合,longest(st)longest (st)表示stst中最长的子串,shortest(st)shortest (st)表示stst中最短的子串。

一些性质:

  1. 对于SS的两个子串s1s1s2s2,不妨设len(s1)len(s2)len (s1) \le len (s2),那么s1s1s2s2的后缀当且仅当endpos(s2)endpos(s1)endpos (s2) \subseteq endpos (s1)s1s1不是s2s2的后缀当且仅当endpos(s1)endpos(s2)=endpos (s1) \cap endpos (s2) = \varnothing
  2. 对于一个状态stst,和任意ssubstrings(st)s \in substrings (st),都有sslongest(st)longest (st)的后缀。
  3. 对于一个状态stst,和任意longest(st)longest (st)的后缀ss,如果ss满足len(shortest(st))len(s)len(longest(st))len (shortest (st)) \le len (s) \le len (longest(st)),那么ssubstrings(st)s \in substrings (st)。即substrings(st)substrings (st)包含longest(st)longest (st)的一系列连续后缀。

后缀自动机的Suffix Links

substrings(st)substrings (st)包含longest(st)longest (st)的一系列连续后缀,但是这连续后缀会在某个地方断掉。如st=7st=7,后缀到bab时,下一个ab没有出现在stst中,因为ab的endposendpos集合大于bab的endposendpos集合,于是被分配到一个新状态。

可以发现一条状态序列785S7 - 8 - 5 - S,对应longest(7)longest(7)即aabbab的后缀依次在7、8、5、S中。用Suffix links连起来,即是上图中的绿色虚线。

一个状态stst的Suffix Link连接到longest(st)longest (st)中最长的后缀,且其endposendpos集合与ststendposendpos集合不同。因此len(shortest(st))=len(longest(slink[st]))+1len (shortest (st)) = len (longest (slink [st])) + 1,其中slink[st]slink[st]表示stst的Suffix Link。


后缀自动机的Transition Function

对于一个状态stst,将stst遇到的下一个字符集合记作next(st)next (st)next(st)={S[i+1],iendpos(st)}next(st) = \{ S[i+1], i \in endpos (st) \}

可以发现substrings(st)substrings (st)中的子串接上一个字符cc后,新的子串仍属于同一个状态。

因此定义转移函数:trans(st,c)=x,longest(st)+csubstrings(x)trans (st, c) = x, longest (st) + c \in substrings (x)


线性时间构造后缀自动机

用增量法构造SS对应的SAM。假设已经构造好了S[1i]S[1…i]的SAM,这时要添加字符S[i+1]S[i+1],因此新增了i+1i+1个后缀S[1i+1],S[2i+1],,S[i+1]S[1…i+1], S[2…i+1], …, S[i + 1],这些后缀从S[1i],S[2i],,S[i]S[1…i], S[2 … i], …, S[i]和空串转移过来。设S[1i]S[1 … i]对应的状态是uu,这些串对应的状态正好是uu到初始状态SS的由Suffix Links连接起来路径上的状态。称这条路径上的所有状态集合是suffix-path (u -> S)。

显然S[1i+1]S[1 … i + 1]这个后缀不能被识别,因此至少要增加一个状态zzzz至少包含S[1i+1]S[1 … i + 1]这个后缀。

trans(st,c)trans (st, c)为转移函数,slink(st)slink (st)为状态stst的Suffix Link

分情况讨论:

  1. 对于suffix-path (u -> S)的任意状态vv,都有trans(v,S[i+1])=NULLtrans (v, S[i + 1]) = NULL,此时只要令trans(v,S[i+1])=ztrans (v, S[i + 1]) = zslink(st)=Sslink (st) = S即可。

    栗子:

    后缀自动机

  2. 存在suffix-path (u -> S)中的状态vv,使得trans(v,S[i+1])=xtrans (v, S[i + 1]) = x。这意味着已经存在某个字符串s+cs+c,其中ssSS的后缀,且s+cs+c已经作为SS的子串出现。则zz的Suffix Link应连向一个状态xx,这个状态中longest(x)=s+clongest (x) = s + clen(longest(x))=len(longest(v))+1len (longest (x)) = len (longest (v)) + 1。这时要分两种情况:

    1. len(longest(x))=len(longest(v))+1len (longest (x)) = len (longest (v)) + 1

      直接将zz的Suffix Link连向xx即可。

      栗子:

      后缀自动机

    2. len(longest(x))>len(longest(v))+1len (longest (x)) \gt len (longest (v)) + 1

      这时xx不只对应长度为len(longest(v))+1len (longest (v)) + 1的后缀,只能将xx状态拆开。

      新建一个状态yylen(longest(y))=len(longest(v))+1len (longest (y)) = len (longest (v)) + 1

      将原先转移指向xx的状态中len(longest(w))len(longest(v))+1len (longest (w)) \le len (longest (v)) + 1的状态ww指向yylen(longest(w))>len(longest(v))+1len (longest (w)) \gt len (longest (v)) + 1的状态ww指向xx,并把yy的Suffix Link指向原先xx的Suffix Link,将xxzz的Suffix Link指向yy

      后缀自动机

      栗子:

      后缀自动机


后缀自动机模板

#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 ;
}

其中状态vv中的lenlen表示len(longest(v))len (longest (v))linklink为Suffix Link,nxtnxt为转移函数。


例题

hihocoder 1445 后缀自动机二·重复旋律5

题面:hihocoder 1445

**题意:**求一个字符串中不同子串的数量。

**题解:**对于后缀自动机中的两个状态xxyysubstrings(x)substrings(y)=substrings (x) \cap substrings (y) = \varnothing。因此即求stsubstrings(st)\sum _{st} |substrings (st)|。又知道shortest(st)shortest (st)longest(st)longest (st)substrings(st)substrings (st)一一对应,len(shortest(st))=len(longest(slink[st]))+1len (shortest (st)) = len (longest (slink [st])) + 1,因此最后求stlen(st)len(link[st])+1\sum _{st} len (st) - len (link[st]) + 1。建出后缀自动机后暴力求和即可。

#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

题面:hihocoder 1449

**题意:**给定字符串SS,求对于所有的1klen(S)1 \le k \le len (S),长度为kk的出现次数最多的子串的出现次数。

**题解:**后缀自动机中状态ststendposendpos集合的大小即为substrings(st)substrings (st)的出现次数。考虑如何算出每个状态的endposendpos集合大小。

先构建后缀自动机:以S = "aabbabd"为例

后缀自动机

不考虑转移函数,只考虑Suffix Links。并且,如果一个状态能接受SS的前缀,将该状态标为绿色。

后缀自动机

状态 子串 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把后缀自动机中的状态连成了一棵树,且祖孙之间的endposendpos有包含关系,非祖孙之间交为空集。

从两个例子来看这个问题:

  1. endpos(8)=endpos(3)endpos(7)endpos (8) = endpos (3) \cup endpos (7)endpos(8)=endpos(3)+endpos(7)|endpos (8)| = |endpos (3)| + |endpos (7)|
  2. endpos(1)=endpos(2)endpos(6){1}endpos (1) = endpos (2) \cup endpos (6) \cup \{1\}

当一个状态为绿色节点即它能接受SS的一个前缀时,它的endposendpos集合大小比它的儿子的集合大小和大1。

即如果一个状态stst包含SS的一个前缀S[1l]S[1 … l],那么一定有lendpos(st)l \in endpos (st),并且ll不能继承自stst的儿子。这时需要+1。

如何标记绿色状态:构造后缀自动机时,每次新建的zz节点一定是绿色状态,因为对应S[1i+1]S[1 … i +1]这个前缀;复制出的状态yy一定不是绿色状态。

求出每个状态的endposendpos集合大小后,因为最后的答案一定关于kk递减,因此每次只需要更新len(longest(st))len (longest (st))处的值,并从后往前更新一遍。

#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

题面:hihocoder 1457

**题意:**给定nn个数字串,求每个串中所有不同的数字子串在十进制下的总和。

**题解:**将nn个数字串连在一起,中间用":"隔开,构建猴嘴自动机。设sum(st)sum (st)为状态stst中的子串和。

sum(st)=trans[x][c]=stsum[x]10+ccnt(x)sum (st) = \sum _{trans[x][c] = st} sum[x] * 10 + c * cnt (x)

其中cnt(x)cnt(x)表示状态xx中不含":"的子串个数,恰好等于从初始状态SS到状态xx所有的非冒号边的数量。

按照拓扑排序转移即可。注意拓扑排序前度数不能将只有经过冒号边才能到达的边计算进去。

#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

题面:hihocoder 1465

**题意:**给定一个大串,多次询问一个小串和大串中的多少子串循环相似。

**题解:**首先对大串建后缀自动机。每个询问的小串将自己的一份复制接在后面。设小串长度为nnT[n+i]=T[i]T[n + i] = T[i]

先考虑如何用后缀自动机求TT串以第ii个字符结尾的与SS串的最长公共子串。

设一个二元组(u,l)(u, l)表示当前在后缀自动机上的状态uu,最长公共子串长度为ll。初始化u=Su=Sl=0l = 0

求出T[i1]T[i - 1](u,l)(u, l)后,如何求T[i]T[i](u,l)(u, l)

  1. 若存在trans(u,T[i])trans (u, T[i])u=trans(u,T[i])u = trans (u, T[i])l=l+1l = l + 1
  2. trans(u,T[i])trans (u, T[i])不存在:沿着suffix-path (u -> S)找到一个状态vv满足trans(v,T[i])trans (v, T[i])存在,令u=trans(v,T[i])u = trans (v, T[i])l=len[v]+1l = len[v] + 1。若trans(S,T[i])trans (S, T[i])也不存在,令u=Su = Sl=0l = 0

lnl \ge n,那么就得到一个循环同构的公共子串T[il+1..i]T[i - l + 1 .. i]

还要注意两个特殊情况:

  1. TT的循环同构串可能会有相同的情况,需要记录状态(u,l)(u, l)中的uu有没有在lnl \ge n时到达过。
  2. T[il+1i]T[i - l + 1 … i]属于状态uu。当l&gt;nl \gt n时,T[in+1i]T[i - n + 1 … i]可能不属于状态uu。因此沿着suffix-path (u -> S)上找离SS最近的vv使len(v)nlen (v) \ge n,统计endpos(v)|endpos (v)|。找到后令u=vu = v
#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 ;
}