Chgtaxihe's blog Chgtaxihe's blog
首页
练习
算法学习
读书笔记
小玩意

Chgtaxihe

首页
练习
算法学习
读书笔记
小玩意
  • 数论

  • 动态规划

  • 暴力

  • 图论

  • 数据结构

  • 字符串

    • AC自动机学习笔记
    • KMP/扩展KMP学习笔记
    • Lyndon分解学习笔记
    • Manacher学习笔记
    • 后缀数组
    • 后缀自动机学习笔记
      • 前置定义
        • endpos
        • 后缀链接 suffix link
      • 构造SAM
      • 1. 计算不同子串的个数
      • 2. 求每个子串出现的次数
      • 3. 求第k大子串
      • 4. 判断两串最长公共子串
    • 回文树
    • 子序列自动机
    • 字符串基础
  • 几何

  • 博弈论

  • 其他

后缀自动机学习笔记

# 前言

看见字符串问题就头疼

# 后缀自动机(suffix automaton, SAM)

如果要在一个DAG上表示出一个字符串SSS的所有子串,该如何维护?

一个最简单的方法就是建立一个字典树,并将字符串的所有后缀Sufi(S)Suf_i(S)Sufi​(S)插入其中。

显然,在绝大多数情况下,上述方案的时空复杂度都是不可接受的。


后缀自动机是一个 DAG图 与 一棵由suffix link构成的树 的组合。

下面先列出它的部分性质:

  • 每一个节点均代表一个状态,边即为状态的转移(有限自动机)
  • 每个转移都为一个字符(如'a', 'b'),同一个节点的各个转移互不相同。
  • 存在终止状态,如果从**初始状态t0t_0t0​**出发,最终转移到终止状态,则转移路径上的所有字符连接起来一定是SSS的后缀。
  • SSS的每一个后缀均可以用一条从t0t_0t0​出发的路径构成。

# 前置定义

# endpos

对于串SSS的非空子串ttt,记endpos(t)endpos(t)endpos(t)为其在SSS中的所有结束为止。例如:SSS="abcxabc",则endpos("bc")=<2,6>endpos(\text{"bc"})=<2, 6>endpos("bc")=<2,6>。

可以根据endpos,将串SSS的子串分为若干个等价类,上例中,'bc' 与 'abc'属于同一个类。

SAM中,每一个状态对应一类endpos。

endpos有以下几个性质(理解suffix link的时候要用)

  • 如果非空子串u,vu,vu,v的endpos相同,当且仅当uuu出现时,总是vvv的后缀。

    • 结论显然,证明略
  • 对于非空子串u,vu,vu,v(假设∣u∣<∣v∣\mid u\mid < \mid v\mid∣u∣<∣v∣),uuu为vvv的后缀则endpos(v)⊆endpos(u)endpos(v) \subseteq endpos(u)endpos(v)⊆endpos(u),。

    否则,endpos(u)∩endpos(v)=∅endpos(u)\cap endpos(v)=\emptysetendpos(u)∩endpos(v)=∅

    • 证明:如果endpos(u),endpos(v)endpos(u),endpos(v)endpos(u),endpos(v)至少有一个公共元素,由于uuu是vvv的后缀,因此每当vvv出现时,uuu也会出现。

# 后缀链接 suffix link

对于一个状态vvv对应的等价类,记其对应的串的集合为st(v)st(v)st(v),其中最长的串为www。

可知www的前几个后缀(w[1:], w[2:], ...)也属于该等价类,而其他所有后缀(至少有一个,即空后缀)属于其他等价类。记不属于vvv的最长后缀所在的等价类为ttt,则边v→tv\to tv→t即为后缀链接。

举个例子:字符串"aabbab",A: endpos('aabbab')={5},B: endpos('ab')={2, 5},C: endpos('b')={2, 3, 5},D: endpos('')={0, 1, 2, 3, 4, 5}。使用后缀链接,依次连接A→B→C→DA\to B\to C\to DA→B→C→D

所有后缀链接构成一颗以初始状态t0t_0t0​为根的树,这棵树被称为parent tree

# 构造SAM

对于每个节点(状态ttt),我们维护下列信息:

  • ttt所包含的最长子串长度mx_lenmx\_lenmx_len
  • ttt所包含的最短子串长度mn_lenmn\_lenmn_len
  • 转移数组trans[]trans[]trans[]
  • 后缀链接slinkslinkslink

从上一张图片中我们可以看到,mn_len[x]=mx_len[slink[x]]+1mn\_len[x]=mx\_len[slink[x]]+1mn_len[x]=mx_len[slink[x]]+1(证明过程待补充),因此,mn_lenmn\_lenmn_len不必特意维护。

使用增量法构造SAM,从初始状态(空串)开始,逐个插入字符。

分3中情况讨论

记当前到达的状态为uuu,要插入的字符为ccc。我们新建一个状态xxx

  1. 对于后缀路径(u→t0u\to t_0u→t0​)上的任意状态vvv,若均有trans[v][c]=NULLtrans[v][c]=NULLtrans[v][c]=NULL,此时只需要令trans[v][c]=xtrans[v][c]=xtrans[v][c]=x,并设置slink[x]=t0slink[x]=t_0slink[x]=t0​即可。

    由于沿着suffix_link经过的状态均包含原串的后缀,直接添加对应的转移即可。

    如图所示,向“aa”中插入字符"b",实线为transtranstrans,虚线为slinkslinkslink

  2. 否则,即存在trans[v′][c]=ztrans[v'][c]=ztrans[v′][c]=z,记该状态v′=yv'=yv′=y。

    若mx_lenz=mx_leny+1mx\_len_z=mx\_len_y+1mx_lenz​=mx_leny​+1,此时只需要令slink[x]=zslink[x]=zslink[x]=z即可。

  3. 否则,我们需要将状态xxx拆分。

    已知mx_lenz>mx_leny+1mx\_len_z>mx\_len_y+1mx_lenz​>mx_leny​+1,如下图所示

    显然,此时trans[y][c]=ztrans[y][c]=ztrans[y][c]=z对于{s∣len(s)>mx_leny+1,s∈z}\{s\mid len(s)>mx\_len_y+1, s\in z\}{s∣len(s)>mx_leny​+1,s∈z}已不再成立,需要将zzz拆分为{s∣len(s)≤mx_leny+1,s∈z}\{s\mid len(s)\le mx\_len_y+1,s\in z\}{s∣len(s)≤mx_leny​+1,s∈z}和{s∣len(s)>mx_leny+1,s∈z}\{s\mid len(s)>mx\_len_y+1, s\in z\}{s∣len(s)>mx_leny​+1,s∈z}两部分。

    见代码:

    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;
1
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,答案则为每个节点的mx_leni−mn_leni+1mx\_len_i-mn\_len_i+1mx_leni​−mn_leni​+1之和。

例题 (opens new window)

# 2. 求每个子串出现的次数

问题:给定一个串S,求其每个子串出现的次数。

构造SAM,插入时每个新建的节点的cnti=1cnt_i=1cnti​=1,而分裂出的点cnti=0cnt_i=0cnti​=0。最后从parent tree底部开始将自己的cnticnt_icnti​累加到父节点的cntparentcnt_{parent}cntparent​上。

例题 (opens new window)

例题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';
}
1
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大的子串(出现位置不同,视为相同/不同字串)。

例题 (opens new window)

# 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
上次更新: 2021/02/24, 03:37:30
后缀数组
回文树

← 后缀数组 回文树→

最近更新
01
深入理解Java虚拟机
03-04
02
DNS工作原理
02-27
03
改用VuePress啦
02-23
更多文章>
Theme by Vdoing | Copyright © 2019-2021
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式