目录

  • 【模板】后缀自动机 (SAM)
    • 题目链接:[luogu P3804](https://www.luogu.com.cn/problem/P3804)
    • 题目大意
    • 思路
      • SAM
        • 前文
        • 一些定义&结论
        • 构造
        • 关于复杂度
      • SAM 与 SA
      • 一些例题&常见用法(给出链接&题解&代码)
        • 判断子串
        • 不同子串数
        • K小子串 / 弦论
        • 最长公共子串 / LCS - Longest Common Substring
        • LCS2 - Longest Common Substring II
        • 子串差异 / 差异
      • 这道题
    • 代码(这道题的)

【模板】后缀自动机 (SAM)

题目链接:luogu P3804

题目大意

给你一个字符串,求出它出现次数超过 1 的子串乘它长度的最大值。

思路

SAM

前文

有一些题目,它要你用 DAG 表示一个字符串的所有子串,要怎么搞。
容易想到可以用 Trie,把每个后缀都插进去。

但是它建图的时间和建的点数是 n2n^2n2 的,nnn 跑到 10410^4104 或更大的时候就爆了。

那要怎么搞呢?
我们考虑将一些节点合并在一起,以减小用的点数,然后通过一些东西,让构造它的时间也短。

于是,就有了这个叫做后缀自动机(SAM)的东西。

一些定义&结论

一个子串,它可能出现在原来串中的几个位置。
我们把这些位置的右端点组成一个集合,就是这个集合的 endpos\text{endpos}endpos。
比如原来的串是 abcabbc,那 endpos(ab)={2,5}\text{endpos}(ab) =\{2,5\}endpos(ab)={2,5}

接着要证明三个东西:

  1. 如果两个子串的 endpos\text{endpos}endpos 相同,那其中一个必然是另一个的后缀。

这个其实挺显然的。
你设长的长度是 xxx,短的长度是 yyy。
如果不是后缀,那它们在后面 yyy 个字符应该有不同。
但是他们的 endpos\text{endpos}endpos 相同,那就说明在后面 yyy 个字符应该相同。
所以就矛盾了,所以就一定短的是长的的后缀。

  1. 设有两个子串 a,ba,ba,b,aaa 的长度大于等于 bbb 的长度。那么要么 endpos(a)∈endpos(b)\text{endpos}(a)\in\text{endpos}(b)endpos(a)∈endpos(b),要么 endpos(a)endpos(b)=∅\text{endpos}(a)\text{endpos}(b)=\emptysetendpos(a)endpos(b)=∅

这个我们分两种情况,bbb 是 aaa 后缀和 bbb 不是 aaa 后缀。
如果是后缀,那它们如果长的长度是 xxx,短的长度是 yyy,那它们后 yyy 个字符就是相同的,那其中一个又只有 yyy 个字符,那它的 endpos\text{endpos}endpos 必然会包含另一个的 endpos\text{endpos}endpos。
如果不是后缀,那它们后 yyy 个字符是有不相同的地方的,那它们的 endpos\text{endpos}endpos 必然是一个有了另一个就一定没有。
(其实就是第一个证明的东西的逆命题)

  1. 我们把 endpos\text{endpos}endpos 相同的子串归为一个 endpos\text{endpos}endpos 等价类。对于每个这样的等价类,我们把里面的子串按长度从大到小排个序,你会发现每个子串的长度是上个子串的长度 −1-1−1(即长度连续),且后面是前面的后缀。

很容易看出,它一个等价类要覆盖就是覆盖一个右端点固定,左端点是一个连续的没有间隔的区间中的数。
而且从我们第二个证明的东西可以看出不同的等价类不会包含同一个子串。

  1. endpos\text{endpos}endpos 等价类的个数级别为 O(n)O(n)O(n)

对于一个等价类,我们找到长度最大的,根据我们第三个证明的定理,我们在它前面任意加一个字符,得到的新串都不是这个类的。
那我们考虑其实就是在原来的集合中进行分割,得到新的集合。
那新的集合也会分,那总的集合个数按线段树的分发是最多的,但是不超过 2∗n2*n2∗n 个。

所以级别个数是 O(n)O(n)O(n)

诶,你就会发现啊这个不断分集合的形式,其实可以弄出一个树。
点表示和,那连儿子就是它可以拆出的集合。

而这个树就叫做 parent tree。

我们的后缀自动机用的节点就是 parent tree 上的点,只是边不同。
因为这样点数就是 O(n)O(n)O(n) 级别,而且我们后面也会发现它的边数也是 O(n)O(n)O(n) 级别的。

  1. 一个类中有最长的子串也有最短的,对于 aaa 这个类,最长的的长度是 lenalen_alena​,最短的是 minlenaminlen_aminlena​,那在 parent tree 上有一些类之间有父子关系,设 aaa 的父亲是 faifa_ifai​,那么 lenfaa+1=minlenalen_{fa_a}+1=minlen_alenfaa​​+1=minlena​

这个其实从第四个结论就可以看出。就是在这个类的最长字符串前加一个子串,那这个新的字符串所属的类就是它儿子的,而且是这个儿子的类中最短的那个。

那我们其实就只用保存 lenilen_ileni​,minleniminlen_iminleni​ 我们可以推出来。

那我们会发现,沿着 parent tree 走就是在字符串前面加字符,而沿着后缀自动机走就是在字符串后面加字符。

  1. 后缀自动机边数是 O(n)O(n)O(n) 级别

我们可以先构出一个后缀树,把其他边舍去,然后对于每个终止节点,我们把它们的后缀根据后缀自动机到它的唯一路径跑。

那如果可以跑,就直接处理下一个,如果跑不了了,就连上要跑的边,沿着它跑。
那你这样搞可能会把后面要跑的子串给跑了,那我们就不用跑了。
那就其实相当于我们跑一个每加上的边都会让一个后缀可以跑。

那就会加上不超过 nnn 条边,那加上原来的 n−1n-1n−1,边的数量级还是 O(n)O(n)O(n)

构造

首先,我们要知道它的构造是在线的,也就是说你可以随时把一个点放到你现在放进去的字符串的后面形成新的字符串,然后得到这个新字符串的后缀自动机。

接着,我们来根据代码,讲讲它构造的过程:

这个是把新形成的串的 endpos\text{endpos}endpos 对于的点弄出来。
(因为它新加入了一个字符,有最新的长度 nnn 在它的 endpos\text{endpos}endpos 里,它整个新的字符串就是在一个新的 endpos\text{endpos}endpos 等价类中)

那它的最长长度就是之前的字符串所在的 endpos\text{endpos}endpos 的最长长度(就是这个之前的字符串)加一(加上你新放进去的字符串)。
那现在的字符串所在的 endpos\text{endpos}endpos 自然就是新的这个点了。


它的意思就是不断地找 ppp 对于的串的后缀,直到找到一个后缀它后面加 ccc 这个字符在字符串中出现过。
那就相当于把新的,没有在后缀自动机上的子串搞出来。

那之前出现过就可以退出是因为后面的后缀一定是现在这个串的子串,它加上字符 ccc 也还是这个串加 ccc 的子串,那就还是出现过,所以就不用搞了。

现在我们求出了 lenlenlen 和后缀自动机上的边,接着就剩 parent tree 上的了。


那如果 p 一直跳到了没有,就说明这个字符没有出现过在之前的串中,那它的祖先出了节点 111(它代表空串),都没有别的,那 111 就是它父亲。


那它其实就是找到了第一个后缀加这个子串有在原来串中出现过的子串,那它如果满足上面的条件,又有什么性质呢?
那 ppp 集合中最长的子串加 ccc 形成了 qqq 形成的子串是新串后缀。到达了 qqq 的所有串都是新串后缀,而它们的 endpos\text{endpos}endpos 相比之前就都多了 nnn,而这时所有到 qqq 的所有串 endpos\text{endpos}endpos 原来一样,都加上一个 nnn 之后还是一样,就还满足后缀自动机性质。
那 qqq 是我们找到的第一个跟 npnpnp 不同而且有后缀关系的点,那 fanpfa_{np}fanp​ 就是 ppp 了。


那刚刚说的是满足条件的,那如果不满足条件呢?
我们容易想到 lenq≥lenp+1len_q\geq len_p+1lenq​≥lenp​+1,因为 ppp 可以到 qqq,但是你又说 lenq≠lenp+1len_q\neq len_p+1lenq​​=lenp​+1,那就只有 lenq>lenp+1len_q>len_p+1lenq​>lenp​+1 了。

那这又说明了什么吗?
说明还有至少一个比 ppp 这一类中最长的子串后面加 ccc 字符得到的字符串还要长的串是属于 qqq 的。
但这个更加长的串就不是新串的后缀了(不然它就会被跳到),所以你就发现问题了。
属于 qqq 的串中,长度 ≤lenp+1\leq len_p+1≤lenp​+1 的是新串后缀,但是 >lenp+1>len_p+1>lenp​+1 的却不是,那到 ppp 个这个节点的字符串就不同属于一个类,就无法定义 qqq 的 endpos\text{endpos}endpos 了。

那怎么办呢?
它说分成了两类,是新串后缀和不是的,那我们考虑把 qqq 拆成两个点,分别表示是新串后缀的和不是新串后缀的,然后再维护各项值,就可以了呗。

那接着我们来看如何维护值。
我们考虑把是新串后缀的转移出来,转到 nqnqnq 上(nqnqnq 新建一个节点来弄),那它们的 endpos\text{endpos}endpos 相比没转移出来的就多了个 nnn。

我们先考虑 lenlenlen 值。
这个很好想,那要转移出来的是新串后缀,那新串后缀又满足 lenq=lenp+1len_q= len_p+1lenq​=lenp​+1,那 lennq=lenp+1len_{nq}=len_p+1lennq​=lenp​+1。

接着我们考虑连边,连后缀自动机上的。
我们考虑直接用 qqq 的边,而且这样是可以的。
因为我们拆点是因为 endpos\text{endpos}endpos 一样,但在后面加同样的字符,得到的字符串还是在同一个类的。
(因为它们在旧串中是属于同一个类,而且类中不是新串后缀,不然的话就会先跳到它停下,那它就不会受到新加入的字符的影响,就还是在同一个类中)

最后我们考虑 faifa_ifai​,也就是 parent tree 上的边。
原来的 qqq 被拆成了 qqq 和 nqnqnq,而且 lenfanq<lennq<lenqlen_{fa_{nq}}<len_{nq}<len_qlenfanq​​<lennq​<lenq​
而且在旧串中 qqq 和 nqnqnq 是相同的,那 fanq=faqfa_{nq}=fa_{q}fanq​=faq​(旧串中)
那其实就类似于 nqnqnq 插入到 qqq 和 faq(fanq)fa_q(fa_{nq})faq​(fanq​) 的父子关系中。
那就让 fanq=faqfa_{nq} = fa_qfanq​=faq​,再让 faq=nqfa_q=nqfaq​=nq。(类似链表的感觉)

接着我们还要考虑 npnpnp 的 fafafa(因为我们一开始就是因为求 fanpfa_{np}fanp​ 求不了才拆点的)
那它要么是 qqq,要是 nqnqnq。qqq 不行,因为其 endpos\text{endpos}endpos 没有 nnn 而 endpos(np)\text{endpos}(np)endpos(np) 中有 nnn,所以只能是 nqnqnq。

接着呢,我们从 ppp 不断的找 ccc 字符对于的边连向 qqq 点,然后 ppp 不断跳父亲(像前面一样),知道跳到不是连向 qqq 点的。
那这个说明什么呢?就说明这些点对应的 endpos\text{endpos}endpos 都是有 nnn 的,那就不能连向 qqq,而是要连向 nqnqnq。

那不是就可以退出是因为此时连向的就是 qqq 的祖先,那 qqq 父亲是 nqnqnq,那也就是说连向祖先的时候 endpos\text{endpos}endpos 就都有 nnn 这个位置了,就不会再出现错误了。

关于复杂度

其实是 O(n)O(n)O(n) 的。
因为两个循环均摊下来其实是 O(n)O(n)O(n) 的。

第一个循环,其实就是在加边,那边的个数是 O(n)O(n)O(n) 级别的,那它所有执行的次数也是 O(n)O(n)O(n) 级别的。
第二个循环,也就是第三种情况里面的那个,它就是在看一个带你的深度要跳 parent tree 要跳多少次才能到根。laslaslas 深度每插入一次最多加 222(新放入的两个点),而跳 fafafa 的这个循环跳多少次就减少多少层的深度,所以总体来讲还是 O(n)O(n)O(n) 级别的执行次数。

SAM 与 SA

SAM 和 SA 都能处理一些相同的问题,SAM 有 SA 不能处理的,而且其复杂度是 O(n)O(n)O(n),后缀数组是 O(nlogn)O(nlogn)O(nlogn)
(虽然 SAM 常数大,但还是比 SA 快)

但是 SA 有着 O(1)O(1)O(1) 求两个后缀的 LCP 的神仙操作。(加 ST 表)

一些例题&常见用法(给出链接&题解&代码)

判断子串

给你两个字符串,问你给出的第二个字符串是否是第一个字符串的子串。
——>点我跳转<——

直接拿一个建,然后拿另外一个跑。
如果跑到最后跑到的不是 NULL\text{NULL}NULL 就说明是它的子串,否则就不是子串。

不同子串数

给你一个字符串,问你它有多少个不同的子串。
——>点我跳转<——

我们考虑可以直接用每个 leni−lenfailen_i-len_{fa_i}leni​−lenfai​​ 相加。
也可以选择 DP,转移出从 iii 出发的不同子串个数(含空串或不含空串),转移用后缀自动机的边。
然后记得如果是含空串输出的是 f1−1f_1-1f1​−1,不含空串就是输出 f−1f-1f−1。

K小子串 / 弦论

给你一个字符串,要你求字典序第 k 小的子串。
(相同的子串可能算一个,也可能算多个,数据以读入 0/1 来判断)
——>点我跳转<——

我们考虑按照题目分两种情况来做。
相同算一个我们就用上一题做法做出的 fif_ifi​ 来搞,用一种类似平衡树找第 k 大的方式。
按字典序从小到大枚举下一个字符,然后能用就用,减去这个当前位置对于串的个数(因为多个算一个所以这里是减一),不然就 k 减去走那边的个数。
k 减到 0 就退出。

不同我们就求 sizeisize_isizei​ 表示 iii 对于的子串个数,然后通过 fafafa 边来转移。
(sizeisize_isizei​ 的初始化新建的点是 111,复制的点是 000)
然后 size_sisize\_s_isize_si​ 表示 iii 出发的子串个数,然后像求 fif_ifi​ 一样转移。
(其实就是 fif_ifi​ 最后的加一变成这里最后的加子串个数,或者说相同其实就是所有的 sizeisize_isizei​ 都是 111,然后像这样跑)
然后又用平衡树找第 k 小的方式来搞。

最长公共子串 / LCS - Longest Common Substring

给你两个字符串,求它们的最长公共子串。
——>点我跳转<——

你考虑用一个子串去构 SAM,然后拿另一个上去跑。
能跑就继续匹配下去,然后长度加一。
不能匹配就跳 fafafa 边,跳到可以匹配位置,此时的匹配长度就是你跳到可以跳的位置 nownownow 的 lenlenlen 值加一(加一是你新加上的字符),即 lennow+1len_{now}+1lennow​+1。

然后中途一直维护,求出长度的最大值就可以了。

LCS2 - Longest Common Substring II

给你多个字符串,要你求它们的最长公共子串。
——>点我跳转<——

这道题其实就是上一道题的升级版。
你考虑还是用一个子串去构 SAM,然后那剩下的去跑。
然后由于它有的话,它 fa 连向的点也会有。(不过要判断一下长度是否超过 fa 点的最长串大小,如果超时就取最长串)

那就用逆拓扑序 DP 一下取 max⁡\maxmax 就好了。

然后每一次到与每个串的匹配长度取最小值,然后再每个串的这个值取最大值就是答案了。

子串差异 / 差异

给你一个字符串,要你求一个式子:

(Ti 是字符串从第 i 个字符开始的后缀,len(a) 是字符串 a 的长度,lcp(a,b) 是字符串 a,b 的最长公共前缀)
——>点我跳转<——

这题主要是看如何快速求后缀两两间 LCP 的和。
翻转子串变成要快速求前缀两两之间最长后缀长度的和。

然后考虑两个前缀求最长后缀长度就是跳 fa 边,跳到 parent tree 上它们的 LCA。

就考虑枚举 LCA 的一个直属儿子,然后 DP 预处理 parent tree 某个点为根的串个数,然后就搞搞。

这道题

没啥好说的,直接拿字符串构一个 SAM,然后 DP 求出每个子串的出现次数。
然后枚举 SAM 上的点 iii,如果它对应的子串出现次数大于 111,就拿去算值和最大值比较。(lenilen_ileni​ 就是长度)

最后输出比较出的最大值就可以了。

代码(这道题的)

#include<cstdio>
#include<cstring>
#include<iostream>
#define ll long longusing namespace std;struct node {int len, fa;ll size;int son[26];node() {len = fa = 0;size = 0ll;memset(son, 0, sizeof(son));}
}d[2000001];
char s[1000001];
int n, tot, lst;
ll ans;void SAM_build(int now) {int p = lst;int np = ++tot;lst = np;d[np].size = 1;d[np].len = d[p].len + 1;for (; p && !d[p].son[now]; p = d[p].fa)d[p].son[now] = np;if (!p) d[np].fa = 1;else {int q = d[p].son[now];if (d[q].len == d[p].len + 1) d[np].fa = q;else {int nq = ++tot;d[nq] = d[q];d[nq].size = 0;d[nq].len = d[p].len + 1;d[q].fa = nq;d[np].fa = nq;for (; p && d[p].son[now] == q; p = d[p].fa)d[p].son[now] = nq; }}
}int tmp[1000001], tp[2000001];void get_tp() {for (int i = 0; i <= n; i++)tmp[i] = 0;for (int i = 1; i <= tot; i++)tmp[d[i].len]++;for (int i = 1; i <= n; i++)tmp[i] += tmp[i - 1];for (int i = 1; i <= tot; i++)tp[tmp[d[i].len]--] = i;
}void DP() {for (int i = tot; i >= 1; i--) {int now = tp[i];d[d[now].fa].size += d[now].size;}for (int i = 1; i <= tot; i++)if (d[i].size > 1)ans = max(ans, d[i].size * d[i].len);
}int main() {scanf("%s", s + 1);n = strlen(s + 1);tot = lst = 1;for (int i = 1; i <= n; i++)SAM_build(s[i] - 'a');get_tp();DP();printf("%lld", ans);return 0;
}

【luogu P3804】【模板】后缀自动机 (SAM)相关推荐

  1. 后缀自动机(SAM)讲解 + Luogu p3804【模板】后缀自动机 (SAM)

    本文求节点子串长度最小值有点问题,现已修改. SAM 后缀自动机可以存储某一个字符串的所有子串. 一.概念 下图是一个 字符串 "aababa" 的 后缀自动机. 上图中的 黑色边 ...

  2. JZOJ4025. 【佛山市选2015】找回密码(后缀自动机SAM)

    题目描述 Description Kevin是一个热爱字符串的小孩.有一天,他把自己的微信登录密码给忘记了,万般无奈之下只好点"找回密码". 这时候,网页上出现了当初设定的密保问题 ...

  3. 后缀自动机SAM详解

    用一个DFA来识别一个串(比如aabab)的所有后缀,要怎么做呢 最简单的办法,把所有后缀看作要保存的单词,画一棵 trie树,像这样: 点很多很麻烦复杂度也很高 我们给这个DFA按我们的需求合并化简 ...

  4. 多校冲刺NOIP模拟6 - 游戏——矩阵乘法、后缀自动机SAM

    此题不提供链接 题目描述 前言 好久没用SAM了.我记得上次用SAM做题还是在上次. 题解 每一种长度的总方案是确定的,所以我们只需要求出赢的方案数.平局方案数即可. 做法其实和官方正解区别不大,官方 ...

  5. [hdu4416 Good Article Good sentence]后缀自动机SAM

    题意:给出串A和串集合B={B1,B2,...,Bn},求串A的所有不同子串中不是B中任一串的子串的数目. 思路:把A和B中所有字符串依次拼接在一起,然后构造后缀自动机,计算每个状态的R集合元素的最大 ...

  6. P3804 【模板】后缀自动机 (SAM)

    传送门 文章目录 题意: 思路: 题意: 给你一个字符串sss,让你求sss中出现次数不为111的子串出现次数乘上该字串长度最大值. ∣s∣≤1e6|s|\le 1e6∣s∣≤1e6 思路: 没学明白 ...

  7. 【算法竞赛学习笔记】后缀自动机SAM-超经典的字符串问题详解

    title : 后缀自动机 date : 2021-11-11 tags : ACM,字符串 author : Linno 前置知识 KMP,Trie,AC自动机等字符串基础 DFA(有限状态自动机) ...

  8. 后缀自动机 AC自动机

    trie树可遍历出所有无重复后缀,通过后缀遍历前缀可得到所有子串:后缀链接把所有后缀相同的状态(以当前节点为endpos的子串)连接起来,便有了类似KMP的next数组的性质.             ...

  9. 【数据结构】自动机全家桶(AC、回文、后缀自动机)

    自动机全家桶 前言 一.AC自动机 1.优秀博客链接 2.问题模板 3.使用 4.本质 5.运用 6.代码模板 二.回文自动机(回文树) 1.优秀博客链接 2.问题模板 3.使用 4.本质 5.运用 ...

  10. 【牛客 - 551C】CSL 的密码(后缀数组,后缀自动机,随机算法)

    题干: 链接:https://ac.nowcoder.com/acm/contest/551/C 来源:牛客网 为了改变这一点,他决定重新设定一个密码.于是他随机生成了一个很长很长的字符串,并打算选择 ...

最新文章

  1. 一手好 SQL 是如何炼成的?
  2. Java内存模型、volatile、原子性、可见性、有序性、happens-before原则
  3. SourceInsight 常用快捷键
  4. 博客园文章方块背景格式
  5. Typora Mermaid 使用指南
  6. echarts源码打包_Echarts源码阅读指南
  7. 将你的blog变成英文版
  8. KB2999226安装提示 此更新不适用你的计算机
  9. FPS游戏通用自瞄实现
  10. otn与stn网络_otn与stn网络_光通信网络
  11. 关于对《上海市人民政府办公厅关于执行〈上海市非营业性客车额度拍卖管理规定〉若干要求的通知》的政策解读...
  12. 联想拯救者wif开不了_联想拯救者wifi开关
  13. “天翼阅读”APP用户体验
  14. 阿里云服务器ftp连接后21端口无法使用的问题
  15. 最近非常火的电子木鱼流量主小程序源码
  16. python中英文古风排版_ET(CAD)-中国风复古女唐装制版教程04
  17. 量化金融分析AQF(1):股票概述
  18. Tracer 记录 Controller 日志
  19. sandstone hypercube超融合一体机知识
  20. 浦东计算机学校排名2015,浦东重点初中排名:四大名校、八大金刚。。(2015新编)...

热门文章

  1. ffmepg安装yasm之后还是出现nasm/yasm not found or too old. Use --disable-x86asm for
  2. html5 移动端上传图片插件,H5文件上传插件easyUpload.js
  3. PHP网站接入QQ互联实现QQ登录获取用户信息功能,超级简单,三个文件就搞定,无需费力地去了解官方提供的一大堆Demo文件
  4. Mysql数据库实现分页查询
  5. 一篇文章带你深入了解Dart语言
  6. zte中兴客户端掉线的一种解决办法
  7. comsol显示电场计算结果_comsol电磁场仿真案例
  8. Day 11 - 视频转换成图片
  9. mq相关的面试突击笔记 大神公众号“石杉的架构笔记
  10. gitlab接入公司内部单点登录