首页 > 其他分享 >SA&SAM 小记

SA&SAM 小记

时间:2024-01-17 09:14:35浏览次数:27  
标签:子串 SAM 后缀 int rk SA 小记 sa mathrm

0. Front

纯笔记,不含教学内容,部分有拓展,部分太简单所以以”显然“带过了,总结了部分 oi-wiki 的内容。

字符串为 \(S\),长度为 \(n\),且应有 \(|\Sigma|\le n\)。

通常来说,大写字母表示为字符串,小写字母表示为字符。

后缀的编号为 \(i\),表示是以 \(i\) 为起点的后缀。

基础小练习

1. 后缀数组(Suffix Array,SA)

定义

落脚到两个数组,\(sa\) 和 \(rank\)。

\(sa[i]\) 表示将所有后缀排序后第 \(i\) 小的后缀的编号,称为后缀数组。

\(rank[i]\) 表示后缀 \(i\) 的排名,是重要的辅助数组,称为排名数组。

显然,\(sa[rank[i]]=rank[sa[i]]=i\)。

如何求后缀数组?

A. 暴力

暴力比较并排序。

时间复杂度 \(O(n^2\log_2{n})\)。

B. 字符串哈希

在比较上对 A. 进行优化,时间复杂度 \(O(n\log_2^2{n})\)。

C. 倍增

定义 \(sa_p[i]\) 表示所有长度为 \(p\) 的子串中排名为 \(i\) 的子串的起点,\(rank_p\) 同理。

会发现在不足的地方可以补上一个无穷小的字符,同时如果 \(p\ge n\) 那么存在 \(sa_p[i],i>n\) 的情况,考虑到我们补上了无穷小的字符,所以应该会被放在第一的排名,最终算 \(rank\) 的真实值时直接减去多余部分就可以了。实际上实现起来可以直接不考虑这个,具体可以参照代码实现。

具体的实现比较简单便不再展开。大概如下:

\(p\) 依次等于 \(2^0,2^1,\cdots,2^{\lceil \log_2{n}\rceil}\),对于每次计算 \(sa_p\) 时候的直接排序考虑大小关系等价于二元组 \((rank_{\frac{p}{2}}[i],rank_{\frac{p}{2}}[i+\frac{p}{2}])\) 依次作为第一二关键字的大小关系。

时间复杂度 \(O(n\log_2^2{n})\)。

D. 倍增 + 基数排序

对于 C. 中的倍增每层的排序,因为是双关键字排序且值域为 \(O(n)\),所以进行基数排序,时间复杂度为每层 \(O(n)\),总时间复杂度为 \(O(n\log_2{n})\)。

Ex. 常数优化

考虑基数排序中的第二关键字实际上只需要改变第一关键字的遍历顺序就可以一并求得。

时间会快上一些。

Code

loj#111. 后缀排序

#include <bits/stdc++.h>
using namespace std;

// [io definition] start
int write_sta[65], write_top;
inline void write (int x){
    if (x < 0)
        putchar ('-'), x = - x;
    write_top = 0;
    do
        write_sta[write_top ++] = x % 10, x /= 10;
    while (x);
    while (write_top)
        putchar (write_sta[-- write_top] + 48);
}
// [io definition] end

const int LN = 62;
int n;
char s[1000005];

int get_id (char a){
    if (a <= '9')
        return a - '0' + 1;
    if (a <= 'Z')
        return a - 'A' + 11;
    if (a <= 'z')
        return a - 'a' + 37;
    return 0;
}

int ton[1000005], sa[1000005], rk[1000005], pos[1000005];
void get_sa (){
    for (int i = 1;i <= LN;++ i)
        ton[i] = 0;
    for (int i = 1;i <= n;++ i)
        ++ ton[rk[i] = get_id (s[i])];
    for (int i = 2;i <= LN;++ i)
        ton[i] += ton[i - 1];
    for (int i = n;i >= 1;-- i)
        sa[ton[rk[i]] --] = i;
    for (int j = 1, tot;j <= n;j <<= 1){
        tot = 0;
        for (int i = n - j + 1;i <= n;++ i)
            pos[++ tot] = i;
        for (int i = 1;i <= n;++ i)
            if (sa[i] > j)
                pos[++ tot] = sa[i] - j;
        for (int i = 1;i <= n;++ i)
            ton[i] = 0;
        for (int i = 1;i <= n;++ i)
            ++ ton[rk[i]];
        for (int i = 2;i <= n;++ i)
            ton[i] += ton[i - 1];
        for (int i = n;i >= 1;-- i)
            sa[ton[rk[pos[i]]] --] = pos[i];
        for (int i = 1;i <= n;++ i)
            pos[i] = rk[i];
        rk[sa[1]] = tot = 1;
        for (int i = 2;i <= n;++ i)
            rk[sa[i]] = (pos[sa[i]] == pos[sa[i - 1]] && pos[sa[i] + j] == pos[sa[i - 1] + j]) ? tot : ++ tot;
    }
}

signed main (){
    scanf ("%s", s + 1);
    n = strlen (s + 1);
    get_sa ();
    for (int i = 1;i <= n;++ i)
        write (sa[i]), putchar (' ');
    puts ("");
    return 0;
}

实际上跑起来很快。

\(\alpha.\) SA-IS 算法

诱导排序与SA-IS算法

\(\beta.\) DC3 算法

不再展开。

应用

最小循环移动位置

JSOI2007 字符加密

Code

#include <bits/stdc++.h>
using namespace std;

int n, top;
char s[200005], t[200005];
int ton[200005], sa[200005], rk[200005], pos[200005];

void get_sa (){
    for (int i = 1;i <= max (n, 256);++ i)
        ton[i] = 0;
    for (int i = 1;i <= n;++ i)
        ++ ton[rk[i] = s[i]];
    for (int i = 2;i <= max (n, 256);++ i)
        ton[i] += ton[i - 1];
    for (int i = n;i >= 1;-- i)
        sa[ton[rk[i]] --] = i;
    for (int j = 1, tot;j <= n;j <<= 1){
        tot = 0;
        for (int i = n - j + 1;i <= n;++ i)
            pos[++ tot] = i;
        for (int i = 1;i <= n;++ i)
            if (sa[i] > j)
                pos[++ tot] = sa[i] - j;
        for (int i = 1;i <= max (n, 256);++ i)
            ton[i] = 0;
        for (int i = 1;i <= n;++ i)
            ++ ton[rk[i]];
        for (int i = 2;i <= max (n, 256);++ i)
            ton[i] += ton[i - 1];
        for (int i = n;i >= 1;-- i)
            sa[ton[rk[pos[i]]] --] = pos[i];
        for (int i = 1;i <= n;++ i)
            pos[i] = rk[i];
        rk[sa[1]] = tot = 1;
        for (int i = 2;i <= n;++ i)
            rk[sa[i]] = (pos[sa[i]] == pos[sa[i - 1]] && pos[sa[i] + j] == pos[sa[i - 1] + j]) ? tot : ++ tot;
    }
}

signed main (){
    scanf ("%s", s + 1);
    n = strlen (s + 1);
    for (int i = 1;i <= n;++ i)
        s[n + i] = s[i];
    n <<= 1;
    get_sa ();
    for (int i = 1;i <= n;++ i)
        if (sa[i] <= (n >> 1))
            t[++ top] = s[sa[i] + (n >> 1) - 1];
    printf ("%s\n", t + 1);
    return 0;
}

部分子串问题

在主串 \(T\) 中寻找模式串 \(S\),要求在线。

可以在 \(sa\) 中二分第一个大于等于 \(S\) 的后缀,这样就可以判断其是否存在 \(S\)。然后在 \(sa\) 中二分第一个在前 \(|S|\) 位上已经大于 \(S\) 的位置,就可以求得其出现次数,因为后缀中前缀为 \(S\) 的后缀在 \(sa\) 中一定连续出现。

部分比较问题

[USACO07DEC] Best Cow Line G

Code

#include <bits/stdc++.h>
using namespace std;

const int SL = 256;
int n;
char s[1000005];
int ton[1000005], pos[1000005], sa[1000005], rk[1000005];

void get_sa (){
    for (int i = 1;i <= SL;++ i)
        ton[i] = 0;
    for (int i = 1;i <= n;++ i)
        ++ ton[rk[i] = s[i]];
    for (int i = 2;i <= SL;++ i)
        ton[i] += ton[i - 1];
    for (int i = n;i >= 1;-- i)
        sa[ton[rk[i]] --] = i;
    for (int j = 1, tot;j <= n;j <<= 1){
        tot = 0;
        for (int i = n - j + 1;i <= n;++ i)
            pos[++ tot] = i;
        for (int i = 1;i <= n;++ i)
            if (sa[i] > j)
                pos[++ tot] = sa[i] - j;
        for (int i = 1;i <= max (SL, n);++ i)
            ton[i] = 0;
        for (int i = 1;i <= n;++ i)
            ++ ton[rk[i]];
        for (int i = 2;i <= max (SL, n);++ i)
            ton[i] += ton[i - 1];
        for (int i = n;i >= 1;-- i)
            sa[ton[rk[pos[i]]] --] = pos[i];
        for (int i = 1;i <= n;++ i)
            pos[i] = rk[i];
        rk[sa[1]] = tot = 1;
        for (int i = 2;i <= n;++ i)
            rk[sa[i]] = (pos[sa[i]] == pos[sa[i - 1]] && pos[sa[i] + j] == pos[sa[i - 1] + j]) ? tot : ++ tot;
    }
}

int top;
char ans[500005];

signed main (){
    scanf ("%d", &n);
    for (int i = 1;i <= n;++ i)
        scanf ("\n%c", &s[i]);
    s[n + 1] = 1;
    for (int i = n + 2;i <= (n << 1) + 1;++ i)
        s[i] = s[((n + 1) << 1) - i];
    n = (n << 1) + 1;
    get_sa ();
    int l = 1, r = n >> 1;
    while (l < r){
        if (rk[l] < rk[n - r + 1]){
            ans[++ top] = s[l];
            ++ l;
        } else {
            ans[++ top] = s[r];
            -- r;
        }
    }
    ans[++ top] = s[l];
    for (int i = 1;i <= top;++ i){
        putchar (ans[i]);
        if (i % 80 == 0)
            putchar ('\n');
    }
    return 0;
}

\(\mathrm{lcp}\) 以及 \(height\) 数组

定义 \(\mathrm{lcp} (A,B)\) 表示 \(A,B\) 最长公共前缀的长度。

特别地,对于 \(\mathrm{lcp}(i,j)\) 表示为后缀 \(i\) 与后缀 \(j\) 的最长公共前缀的长度。

定义 \(height[i]\) 表示 \(\mathrm{lcp}(sa[i-1],sa[i])\)。默认 \(height[1]=0\)

如何求 \(height\) 数组?

引理 1.1 \(height[rk[i]]\ge height[rk[i-1]]-1\)。

证明

考虑 \(height[rk[i-1]]>1\) 时的情况,否则显然成立。

这时有 \(\mathrm{lcp}(sa[rk[i-1]],sa[rk[i-1]-1])=height[rk[i-1]]>1\)。

于是有 \(\mathrm{lcp}(i-1,sa[rk[i-1]-1])=height[rk[i-1]]\)。

设后缀 \(i-1\) 为 \(aAB\),则后缀 \(i\) 为 \(AB\)。

设后缀 \(sa[rk[i-1]-1]\) 为 \(aAD\),则后缀 \(sa[rk[i-1]-1]+1\) 为 \(AD\)。

不妨令 \(B[0] \ne D[0]\) 且 \(height[rk[i-1]] = |A|+1\)。

那么实际上应有 \(\mathrm{lcp}(i,sa[rk[i-1]-1]+1)=|A|=height[rk[i-1]]\)。

会发现在 \(sa\) 中因为已经存在两个前缀为 \(A\) 后缀,所以这两个后缀在 \(sa\) 中设 \(sa[rk[i-1]-1]+1\) 在 \(p\) ,\(i\) 在 \(q\),那么一定有 \(\min\{height[p+1],\cdots,height[q]\}\ge \mathrm{lcp(sa[p],sa[q])}=height[rk[i-1]]-1\)。因为字典序中夹在 \(AD\) 和 \(AB\) 中的字符串一定都是以 \(A\) 开头的。

上式得证。\(\Box\)

所以存在 \(O(n)\) 求出 \(height[]\) 的做法。

for (int i = 1, k = 0;i <= n;++ i){
    if (k)
        -- k;
    while (s[i + k] == s[sa[rk[i] - 1] + k])
        ++ k;
    height[rk[i]] = k;
}

长得跟暴力一样。

应用

两子串的公共前缀

引理 1.2 \(\mathrm{lcp}(sa[i],sa[j])=\min\{height[i+1,\cdots,j]\}\)

证明 实际上在引理 1.1 的证明当中已经提到了 \(\min\{height[p+1],\cdots,height[q]\}\ge \mathrm{lcp(sa[p],sa[q])}\),那么还会发现一定是存在一个位置的 \(height[i]=\mathrm{lcp}(sa[p],sa[q]),i\in [p+1,q]\) 。不存在直接相等的情况,所以后缀 \(sa[p]\) 和 后缀 \(sa[q]\) 的第 \(\mathrm{lcp}(sa[p],sa[q])+1\) 个位置上的字符是不相等的,所以说在 \(sa[p+1,\cdots,q]\) 的这段后缀上这个位置一定发生了变化,发生变化时的 \(height[i]=\mathrm{lcp}(sa[p],sa[q])\)。\(\Box\)

所以实际上就是个 RMQ 问题。

不同子串的数目

引理 1.3 不同子串数目应该等于 \(\frac{n(n+1)}{2}-\sum_{i=1}^{n}height[i]\)。

证明

考虑可以枚举每个后缀,减去当前后缀中与以前重复的前缀。

如果按照 \(sa\) 的顺序遍历的话,实际上 \(sa[i]\) 往前重复的前缀个数就是 \(height[i]\),因为后缀 \(sa[i-1]\) 是前 \(i-1\) 个前缀中与 \(sa[i]\) 的 \(\mathrm{lcp}\) 最大的串(由字典序的性质显然)。

所以实际上不同子串数目应该等于上式。\(\Box\)

比较字符串中两个子串的大小关系

较为简单,不再赘述。

出现至少 \(k\) 次的子串的最大长度

在 \(height[]\) 上使用单调队列扫描即可。

找到最长的子串使其至少不重叠地出现过两次

二分长度,在 \(height[]\) 中找到连续大于等于这个长度的段,分别做 RMQ 找到最大和最小的后缀起点即可。

连续重复串

穷举重复的串长度为 \(k\),是生成连续重复串的串,当且仅当 \(\mathrm{lcp}(1,k+1)=n-k \land k \mid n\)。

SP. [NOI2016] 优秀的拆分 相当有趣的应用。

另一些结合数据结构的应用

[NOI2015] 品酒大会

[AHOI2013] 差异

[HAOI2016] 找相同字符

2. 后缀自动机(Suffix Automaton,SAM)

相关概念

结束位置 \(\textrm{endpos}\)

考虑 \(S\) 的一个非空子串 \(T\),\(\mathrm{endpos}(T)\) 表示 \(T\) 在 \(S\) 中所有出现位置的右端点。

比如 \(S=\textrm{ababa},T=ab\),则 \(\mathrm{endpos}=\{2,4\}\)。

SAM 中一个状态可能对应多个 \(\mathrm{endpos}\) 相同的子串,于是我们把这些子串称为一个等价类。 \(S\) 的所有非空子串可以被划分为若干等价类

引理 2.1 字符串 \(S\) 有两个非空子串 \(u\) 和 \(w\),不妨设 \(|u| \le |w|\)。\(u\) 和 \(w\) 的 \(\mathrm{endpos}\) 相等,当且仅当 \(u\) 每次出现时都是以 \(w\) 的后缀的形式。

证明 显然。\(\Box\)

引理 2.2 两个非空子串 \(u\) 和 \(w\),\(|u|\le|w|\)。要么 \(\mathrm{endpos}(u)\cap \mathrm{endpos}(w)=\empty\),要么 \(\mathrm{endpos}(w)\sube\mathrm{endpos}(u)\),后者成立当且仅当 \(u\) 是 \(w\) 的后缀。

证明 如果 \(\mathrm{endpos}(u)\cap\mathrm{endpos}(w) \ne \empty\),出现 \(w\) 的子串至少出现了一次 \(u\),说明 \(u\) 是 \(w\) 的后缀。如果 \(u\) 是 \(w\) 的后缀,那么出现 \(w\) 时一定出现了 \(u\)。\(\Box\)

引理 2.3 一个等价类中的所有子串,较短的子串一定是较长的子串的后缀,长度相等的子串一定唯一。若等价类长度的值域是一段区间 \([x,y]\),则区间内每个整数都能取到。

证明 前半段显然。对于后半段考虑到一个终点向前选,一定有 \(l>1,\mathrm{endpos}(S[l-1,r])\sube\mathrm{endpos}(S[l,r])\),显然(或根据引理 2.1 和引理 2.2 可以推得)。\(\Box\)

引理 2.4 \(\mathrm{endpos}\) 等价类的个数为 \(O(n)\)。

证明 考虑从 \(t_0\) 开始划分大小为 \(O(n)\) 的不交集合到子树,类推之后最大的情况只有 \(2n-1\) 个。\(\Box\)

后缀链接 \(\mathrm{link}\) 以及 \(\textrm{parent tree}\)

对于 SAM 中一个不为 \(t_0\) 的状态 \(v\)。它唯一对应一个 \(\mathrm{endpos}\) 等价类。我们考虑这个等价类中最长的子串 \(w\),其他子串一定都是 \(w\) 的后缀。

我们将 \(v\) 的后缀链接 \(\mathrm{link}(v)\) 连接到 \(w\) 最长的不在这个 \(\mathrm{endpos}\) 等价类中的后缀的 \(\mathrm{endpos}\) 等价类构成的状态 \(t\) 上。

而初始状态 \(t_0\) 的 \(\mathrm{endpos}\) 我们规定为严格包含所有位置的 \(\mathrm{endpos}\),其单独为一个等价类,可以看作是空串的等价类。

引理 2.5 后缀链接构成了一颗根为 \(t_0\) 的树。

证明 由定义显然。\(\Box\)

引理 2.6 通过 \(\mathrm{endpos}\) 构造的树和通过 \(\mathrm{link}\) 构造的树结构一致。通过 \(\mathrm{endpos}\) 构造意为父节点一定是最小的 \(\mathrm{endpos}\) 对这个状态具有真包含关系的状态。

证明 由 \(\mathrm{endpos}\) 构造树的定义,显然。 \(\Box\)

我们将通过 \(\mathrm{link}\) 构建的树称为 \(\textrm{parent tree}\)。

算法流程

对于如何实现以及实现的正确性,不再赘述,网上的相关优质资源不少。

提供一份代码:

namespace SAM {
    const int N = 2e6 + 5;
    struct state {
        int len, link;
        map < char , int > nex;
        char end_tag;
    } st[N << 1];
    int sz, las;
    void Init (){
        st[0].len = 0;
        st[0].link = - 1;
        sz = 1;
        las = 0;
    }
    void extend (char c){
        int cur;
        st[cur = sz ++].len = st[las].len + 1;
        int p = las;
        while (p != - 1 && st[p].nex[c] == 0){
            st[p].nex[c] = cur;
            p = st[p].link;
        }
        if (p != - 1){
            int q = st[p].nex[c];
            if (st[q].len == st[p].len + 1)
                st[cur].link = q;
            else {
                int clone = sz ++;
                st[clone].len = st[p].len + 1;
                st[clone].link = st[q].link;
                st[clone].nex = st[q].nex;
                while (p != - 1 && st[p].nex[c] == q){
                    st[p].nex[c] = clone;
                    p = st[p].link;
                }
                st[q].link = st[cur].link = clone;
            }
        }
        las = cur;
        st[cur].end_tag = 1;
    }
}

时空复杂度分析

声明

\(\mathrm{longest}(v)\) 表示状态 \(v\) 的所有子串中最长的那个。

\(\mathrm{shortest}(v)\) 表示状态 \(v\) 的所有子串中最短的那个。

\(\mathrm{len}(v)=|\mathrm{longest(v)}|,\mathrm{minlen}(v)=|\mathrm{shortest}(v)|\)。

\(\delta(p,c)\) 表示状态 \(p\) 输入字符 \(c\) 后的转移状态。

状态数上界

由引理 2.3,引理 2.6 可得,上界为 \(O(n)\),而且实际上为 \(2n-1\)。

转移数上界

考虑转移中 \(\mathrm{len}(\delta(v,c))=\mathrm{len}(v)+1\) 的,我们称为连续转移的数量。

考虑到存在一颗生成树一定可以将连续的边全部包括,所以去掉这 \(2n-2\) 条边。

接着估计不连续转移的数量。考虑一个不连续转移 \(p\to q\) ,找到从 \(t_0\) 到 \(p\) 的一条连续转移路径,设表示字符串为 \(u\);找到从 \(q\) 到任意终止位置(一定是整个串的后缀)的连续路径,设表示字符串为 \(v\),考虑 \(s_{p,q}=u+c_{p,q}+v\) 对于 \(p,q\) 互不相同,否则与连续转移相悖。所以 \(s_{p,q}\) 是 \(s\) 的真后缀,而 \(s\) 对应路径连续,因此不连续转移数量不超过 \(n-1\),于是我们得到上界 \(3n-3\)。而实际上真正的上界是 \(3n-4\),达到这个上界的字符串应形如 \(\textrm{abbb...bb}\)。

操作次数上界

思路来自 常见字符串算法 II:自动机相关——Alex Wei ,补足了一些细节问题。

考虑到因为 SAM 的状态数和转移数都是 \(O(n)\) 级别的,所以所有的新建转移和新建点的操作以及为了这些操作所进行的所有遍历时间复杂度均为 \(O(n)\)。

那么考虑不新建转移边的情况,也就是将 \(p\to q\) 的转移修改为 \(p \to clone\) 的转移,我们考虑证明这个的时间复杂度为 \(O(n)\)。

为此引入定义 \(\mathrm{depth}(p)\) 表示 \(p\) 在 \(\textrm{parent tree}\) 上的深度。

引理 2.7 对于 SAM 中任意一条转移 \(p\to q\),有 \(\mathrm{depth}(p)+1\ge\mathrm{depth}(q)\)。

证明 考虑后缀链接路径 \(q\to t_0\) 上的两个不同状态 \(q_1,q_2,q_1\ne q_2\)。设 \(p_1\) 为任意可转移到 \(q_1\) 的状态,\(p_2\) 为任意可转移到 \(q_2\) 的状态。考虑到 \(p\to q\) 的转移,所以 \(\mathrm{longest}(p)\) 是 \(\mathrm{longest}(q)\) 去掉最后一个字符后的串后缀。 \(p_1,p_2\) 于 \(q_1,q_2\) 同理,因此 \(p_1,p_2\) 均在后缀链接路径 \(p\to t_0\) 上。而且因为 \(p_1\to q_1,p_2\to q_2\) 的转移都是通过同一个字符(也就是 \(\mathrm{longest}(q)\) 的最后一个字符)进行的,因为在某个状态上输入字符进行转移的下一个状态时唯一的,因此反推出 \(p_1\ne p_2\)。

发现 \(q\to t_0\) 的后缀链接路径上在除了 \(t_0\) 不存在可转移到这个状态的状态外,其他状态均存在可转移到这个状态的其他状态 。由上文可知,\(q\to t_0\) 路径上的所有状态 \(q'\)(除了 \(t_0\))各自的 \(P(q')=\{可转移到这个状态的状态\}\) 一定非空,集合中的所有状态都在 \(p\to t_0\) 的路径上。不同 \(q'\) 的这个集合一定不交。因此可以得到一个关系,就是:

\[|q\to t_0|-1\le \sum_{q'} |P(q')|=|\bigcup_{q'}P(q')|\le |p\to t_0| \]

式子中 \(-1\) 的原因是 \(t_0\) 不考虑在 \(q'\) 中,但 \(p\to t_0\) 中要考虑 \(t_0\),因为 \(t_0\) 到某个单字符构成的子串显然也是转移。

因为 \(|q\to t_0|=\mathrm{depth}(q),|p\to t_0|=\mathrm{p}\),因此可以得出 \(\mathrm{depth}(p)+1\ge \mathrm{depth}(q)\)。\(\Box\)

我们尝试使用这个引理来估算时间复杂度。

现在考虑从 \(p\) 一直跳到了 \(p'\),并将中途所有状态原本的 \(\to q\) 转移变为了 \(\to clone\) 转移。设 \(q'=\delta(\mathrm{link}(p'),c)\),其中 \(c\) 是新加入的字符。容易证明原 \(\mathrm{link}(q)\) 即现 \(\mathrm{link}(clone)=q'\)。

设 \(d=\mathrm{depth}(p)-\mathrm{depth}(p')\),即从 \(p\) 开始跳 \(\mathrm{link}\) 的次数。根据这个引理,有 \(\mathrm{depth}(q')\le\mathrm{depth}(\mathrm{link}(p'))+1=\mathrm{depth}(p')=\mathrm{depth}(p)-d\le\mathrm{depth}(las)-1-d\)。

同时可知 \(\mathrm{link}(cur)=clone,\mathrm{link}(clone)=q'\),因此 \(\mathrm{depth}(cur)-2=\mathrm{depth}(q')\le\mathrm{depth}(las)-1-d\),即 \(d\le\mathrm{depth}(las)-\mathrm{depth}(cur)+1\)。这个 \(d\) 可以由 \(las\) 到 \(cur\) 的深度减小量来估计。

如果存在 \(las\to cur\) 的转移,因此根据引理有 \(\mathrm{depth}(las)+1\ge\mathrm{depth}(cur)\)。其他情况如上述。因此势能分析得到 \(O(\sum d)=O(n)\)。

时空复杂度

由上文可知,SAM 的时间复杂度和空间复杂度均为 \(O(n)\)。

应用

出现次数

考虑记录终止位置,即考虑在原串各个前缀对应的状态上打终止标记,在 \(\textrm{parent tree}\) 对子树标记数进行统计,这个数目在状态上的定义就是这个状态中的所有子串出现次数,也就是 \(|\mathrm{endpos}|\)。

当然,可以直接在自动机上 DP。

【模板】后缀自动机(SAM) 以及超多重题。

本质不同子串个数

就是 \(\sum_{i\in \mathrm{SAM},i\ne t_0}{\mathrm{len}(i)-\mathrm{len}(\mathrm{link}(i))}\)。显然,这个在线。

当然,可以直接在自动机上 DP。

[SDOI2016] 生成魔咒 以及超多重题。

最长公共子串

考虑实际上可以记录每个状态往前最多能匹配多少个。对于每个串扫进来之后进行匹配,如果成功就往下走,否则跳 \(\mathrm{link}\) 直到无法再跳或存在往下走的转移。然后在 \(\textrm{parent tree}\) 上从下往上更新。

记得更新长度。

LCS2 - Longest Common Substring II 以及超多重题。

值得一提的是,在限定区间内同样可以进行这样的匹配,但需要加限定条件,见 [NOI2018] 你的名字

出现位置

考虑在出现次数的做法下增加一个线段树合并,改成可持久化(?),大概这样。

inline int Merge (int p, int q, int l = 1, int r = n){
    if (! p || ! q)
        return p | q;
    int w = ++ cnt, mid = (l + r) >> 1;
    ls[w] = Merge (ls[p], ls[q], l, mid);
    rs[w] = Merge (rs[p], rs[q], mid + 1, r);
    return w;
}

[NOI2018] 你的名字

标签:子串,SAM,后缀,int,rk,SA,小记,sa,mathrm
From: https://www.cnblogs.com/imcaigou/p/17969001

相关文章

  • WhatsApp广播列表功能介绍及用法
    如果遇到想要发送一条信息给多个客户的时候,WhatsApp广播功能就能帮到你。WhatsApp的广播功能可以让你将同一条消息发送给多个联系人,而这些联系人不会知道你已向其他联系人发送了相同的消息。所以广播功能非常适合于一次向多个人发送通知或公告,例如线下活动通知、公司内部通知、最新......
  • 无涯教程-SQL - Transactions(事务)
    事务是将一个或多个更改打包在一起保存到数据库,事务对于确保数据完整性和处理数据库错误很重要。事务性质事务具有以下四个标准属性,通常以首字母缩写ACID表示。原子性-确保工作单元内的所有操作均成功完成,否则,事务将在失败时中止,并且所有先前的操作都将还原到以前的状态......
  • SAP 会计凭证字段可以更改
    会计凭证字段可以更改T-CODE:OB32 ......
  • .net core ECDsa
    ECDsa(EllipticCurveDigitalSignatureAlgorithm)是一种基于椭圆曲线密码学的数字签名算法。在.NETCore中,System.Security.Cryptography.ECDsa类提供了对ECDsa算法的支持。ECDsa算法用于生成和验证数字签名,其主要用途包括:数字签名:使用私钥对数据进行签名,生成数字签名。这个......
  • IaaS,PaaS,SaaS 的区别
    前言IaaS:基础设施即服务,Infrastructure-as-a-servicePaaS:平台即服务,Platform-as-a-serviceSaaS:软件即服务,Software-as-a-service以做披萨为例,你可以从头到尾,自己生产披萨,但是这样比较麻烦,需要准备的东西多,因此你决定外包一部分工作,采用他人的服务。你有三个方案。方案一:Iaa......
  • U-Boot Sandbox 基础
    U-BootSandbox是一个仿真平台,可以用来调试u-boot的非架构相关代码。平台:ubuntu22.04forx86_641.开发环境Ubuntu22.04forx86_64.$sudoaptupdate$sudoaptinstallbuild-essentialflexbisongawktexinfolibsdl2-devpython3-setuptoolspython3-devswig......
  • 事务Transactional失效的这10个场景,你一定得知道!
    @Transactional失效的场景都有哪些呢?如图所示!以上我们列举了10种场景,接下来我们针对不同的场景来具体的分析下。一、代理不生效导致1、同一个类中,方法内部调用事务失效同一个类中,addOrder()方法无事务,addOrder2()方法存在事务,addOrder()调用addOrder2()。我们通过外部方法调用addOr......
  • CodeForces 1266F Almost Same Distance
    洛谷传送门CF传送门好厉害。特判\(k=1\)。首先经过观察,我们可以按照\(k\)的奇偶性讨论:\(k\)为偶数,有一个中心点挂了若干条长度为\(\frac{k}{2}\)的链。\(k\)为偶数,有两个中心点,两边挂了若干条长度为\(\frac{k}{2}\)的链;\(k\)为奇数,有一个中心点挂了若干条长度......
  • AWS平台-SAP-DB-HANA-数据库-跨账号实现-Backint-恢复的配置
    生产环境账号:221234567891生产环境S3桶:project-backup-prd。生产环境S3桶的KMS:ccefc2e5-d396-5e23-915f-6eb70b40293d实现的目标:在另一个账号的EC2上(awtxxx05,awtxxx06),使用backint进行数据恢复 实现过程:1、到生产环境的S3桶上,添加权限如下策略,即允许另一个账号下的EC2的r......
  • 【vsan数据恢复】VSAN逻辑架构出现故障,部分虚拟机磁盘组件出现问题,磁盘文件丢失的数据
    VSAN数据恢复环境:一套有三台服务器节点的VSAN超融合基础架构,每台服务器节点上配置2块SSD硬盘和4块机械硬盘。每个服务器节点上配置有两个磁盘组,每个磁盘组使用1个SSD硬盘作为缓存盘,2个机械硬盘作为容量盘。三台服务器节点上共配置6个磁盘组,共同组成VSAN存储空间,存放虚拟机文件。......