忘光了,所以复习【STR】

字符串

本文字符串下标从 \(1\) 开始。

\(S[l,r]\) 表示字符串 \(S\)\(l\)\(r\) 的部分。

速通

哈希

没什么可说的,我也不喜欢用。

trie

顾名思义,就是一个像字典一样的树。

基础

不多说了。

01trie

在一些题目中,可以用 trie 维护 01 串。

最长异或路径

给定一棵 \(n\) 个点的带权树,求异或和最大的简单路径。

\(0\le n\le 10^5,0\le w<2^{31}\)

solution

考虑树上两点路径的异或和可以转化为根到两点的异或和异或起来。

于是转化为求寻找两点使两点的权值的异或和最大。

用 trie 维护:不断插入一个数,查询这个数与已插入的数的最大异或和是多少(只要尽量保证前缀是 1)。

自动机

定义

自动机是一个对信号序列进行判定的数学模型。

比如说,你在初始状态,可以往几条路走,通过这几条路可以走到其他状态。

一个确定有限状态自动机(DFA,deterministic finite automation)由以下五部分构成:

(另外有个东西叫做 NFA,以后可能会提到)

  • 字符集(\(\sum\)),该自动机只能输入这些字符。
  • 状态集合(\(Q\)),顾名思义。
  • 起始状态(\(st\)),同样顾名思义,\(st\in Q\)
  • 接受状态集合(\(F\)),\(F\subseteq Q\),或终止状态集合。
  • 转移函数(\(\delta(x,y)\)),\(x\) 是一个状态,\(y\) 是字符串,意思就是从 \(x\) 开始,走一个字符串 \(y\)

以上的字符串均为广义的。

想要更快的理解,可以把 DFA 看做一个有向图,但是 DFA 只是一个数学模型。

另外不难发现 trie 也是自动机,我称其为广义前缀自动机。下面就拿 01trie 举个例子。

v51RDP.png

  • \(\sum\):0/1。
  • \(Q\):每个节点。
  • \(st\):如图。
  • \(F\):如图黄点。
  • \(\delta\)\(\delta(st,101)\)

边可以看做状态转移。

序列自动机

对于字符串 \(S\) 的一个子串 \(s\)\(\delta(st,s)\) 表示 \(s\)\(S\) 中第一次出现时末位。

转移(\(u\) 是位置,\(c\) 是个字符):\(\delta(u,c)=\min(i|i>u,s_i=c)\)

可以通过记录下一个字符出现位置来实现。

给你两个由小写英文字母组成的串 \(A\)\(B\),求:

  • \(A\) 的一个最短的子串,它不是 \(B\) 的子序列。
  • \(A\) 的一个最短的子序列,它不是 \(B\) 的子序列。

\(n\le2000\)

solution

第一个相对简单。直接枚举起点跑就行。

\(f_{i,j}\) 表示在 \(A\) 中到第 \(i\) 位,在 \(B\) 中到第 \(j\) 位的答案。

那么 \(f_{i,j}=\min\{f_{\delta(i,c),\delta(j,c)}+1\}\)

KMP

\(\mathrm p_i\) 表示前 \(i\) 位最长相同真前后缀长度。

对于字符串 \(\texttt{abbaabb}\)

  1. \(\mathrm p_1=0\)\(\texttt{a}\)
  2. \(\mathrm p_2=0\)\(\texttt{ab}\)
  3. \(\mathrm p_3=0\)\(\texttt{abb}\)
  4. \(\mathrm p_4=1\)\(\underline{\texttt{a}}\texttt{bb}\underline{\texttt{a}}\)
  5. \(\mathrm p_5=1\)\(\underline{\texttt{a}}\texttt{bba}\underline{\texttt{a}}\)
  6. \(\mathrm p_6=2\)\(\underline{\texttt{ab}}\texttt{ba}\underline{\texttt{ab}}\)
  7. \(\mathrm p_7=3\)\(\underline{\texttt{abb}}\texttt{a}\underline{\texttt{abb}}\)

处理出 \(\mathrm p_i\) 有什么用呢?

模式串在匹配文本串的时候,如果是以下这个状态:

v5HMNR.png

发现到第五位时失配了,我们接下来肯定是想让它从蓝色这个状态继续匹配。

可以发现,跳到 \(\mathrm p_i\) 一定不劣。即比如在第五位失配了,就跳到 \(\mathrm p_4=1\) 位,如果跳到这里发现可以匹配下去,由于前后缀相同,所以可以继续匹配下去。如果不能匹配下去,那就继续往前跳。

由于每次只往后移动一格,往前跳的次数一定小于等于往后走的次数,复杂度 \(O(n)\)

怎么求 \(\mathrm p\) 呢。

v5bZGt.png

假设当前位是 \(i\)\(\mathrm p_{i-1}=j\)。如果 \(\mathrm p_i>\mathrm p_j\),那么 \(\mathrm p_i=\mathrm p_j+1\)\(S_{j+1}=S_i\)

否则,贪心地令 \(j=\mathrm p_j\),即上图绿框,可以发现如果此时 \(S_{j+1}=S_i\) 还是可以下去且最优。

最后,不难发现 kmp 的过程与求 \(\mathrm p\) 的过程类似,代码:

int j=0;
for(int i=2;i<=m;i++){
    while(j&&b[i]!=b[j+1])j=pre[j];
    j+=(b[i]==b[j+1]);pre[i]=j;
}
j=0;
for(int i=1;i<=n;i++){
    while(j&&a[i]!=b[j+1])j=pre[j];
    j+=(a[i]==b[j+1]);
    if(j==m)write(i-m+1,'\n'),j=pre[j];
}

KMP自动机

用 KMP 建出的自动机,转移:

\[\delta(x,c)=\begin{cases} x+1&S_{x+1}=c\\ 0&i=0\land S_1\neq c\\ \delta(\mathrm p_x,c)&i>0\land S_{x+1}\neq c\\ \end{cases} \]

[HNOI2008]GT考试

求有多少个 \(N\) 位数字文本串满足:没有一个子串为给定模式串。

模式串长度为 \(M\),对 \(K\) 取模。

\(N\leq10^9,M\leq20,K\leq1000\)

solution

dp,设 \(f_{i,j}\) 表示文本串中匹配到第 \(i\) 位,模式串中匹配到第 \(j\) 位的方案数。

那么:

\[\begin{aligned} f_{i,j}=&\sum _c\sum_{k,\delta(k,c)=j}f_{i-1,k}\\ =&\sum_{k}f_{i-1,k}\sum _c[\delta(k,c)=j]\\ \end{aligned} \]

设矩阵 \(g\) 有: \(\displaystyle g_{k,j}=\sum _c[\delta(k,c)=j]\),就可以把转移看做向量乘上矩阵。

又发现 \(g\)\(i\) 无关,于是矩阵快速幂。

失配树

border

任意长度相同前后缀。

失配树

考虑一个问题:

【模板】失配树

给定 \(S\)\(T\) 次询问 \(p,q\),求 \(p\) 前缀和 \(q\) 前缀的最长公共 border。

首先,根据 KMP 中 \(\mathrm p_i\) 的定义,从一个点开始不断往上跳 \(\mathrm p_i\),跳到的就是它的所有 border。

而仔细思考后发现一个点向它的 \(\mathrm p_i\) 连边,连出来的就是一个树形结构,我们称其为失配树。

而两段前缀的最长公共 border 转化为了失配树上的 LCA。

[NOI2014] 动物园

求出对于 \(S\) 每个前缀的不相交 border 个数。

对于一个前缀 \(S[1,i]\) 求不相交 border 可以转化为长度 \(\le \frac{i}2\),所以我们就可以在失配树上往上跳,跳到符合条件,深度就是答案。

可以不用显式建树。

Z 函数

\(\mathrm z_i\) 表示一个字符串 \(s\)\(s[i,n]\) 的 LCP,特别的,\(\mathrm z_1=0\)

对于字符串 \(\texttt{aaabaab}\)

  1. \(\mathrm z_1=0\)
  2. \(\mathrm z_2=2\)\(\underline {\texttt{a}\underline{\texttt{a}}}\underline{\texttt{a}}\)
  3. \(\mathrm z_3=1\)\(\underline {\texttt{a}}\texttt{a}\underline{\texttt{a}}\)
  4. \(\mathrm z_4=0\)
  5. \(\mathrm z_5=2\)\(\underline{\texttt{aa}}\texttt{ab}\underline{\texttt{aa}}\)
  6. \(\mathrm z_6=1\)\(\underline{\texttt{a}}\texttt{aaba}\underline{\texttt{a}}\)
  7. \(\mathrm z_7=0\)

Z 函数其实也很好求:

从某个位置 \(i\) 开始,与整串前缀相同的前缀(称为 Z-box)为 \(s[i,i+\mathrm z_i-1]\)

我们维护当前 \(i+\mathrm z_i-1\) 的最大值 \(r\),以及其对应的 \(i\) ,令其为 \(l\)

假设我们已经求出 \(1\sim i-1\) 的 Z 函数,接下来要求 \(\mathrm z_i\)

根据定义,有 \(s[i,r]=s[i-l,r-l]\),所以,\(\mathrm z_i\ge\min({\mathrm z_{i-l}},r-i+1)\)

如果还有机会继续拓展(\(r-i+1\le \mathrm z_{i-l}\)),那就右移 \(r\),否则就 G 了。

l=1,r=0;
for(int i=2;i<=m;i++){
    if(i<r)z[i]=min(z[i-l+1],r-i+1);
    while(i+z[i]<=m&&b[i+z[i]]==b[z[i]+1])++z[i];
    if(i+z[i]-1>=r)r=i+z[i]-1,l=i;
}

匹配另一个串

拼起来再做一遍即可。

但是注意拼起来时中间放一个随便什么引荐字符,避免匹配到后面的串。

[NOIP2020] 字符串匹配

给字符串 \(S\),求 \(S = {(AB)}^iC\) 的方案数,设 \(F(S)\) 表示 \(S\) 中出现奇数次的字符数量,有 \(F(A)\le F(C)\)。定义乘法为前后拼接。

solution

枚举循环节长度:

如图为长度为 \(i-1\) 的循环节,不难发现有颜色的段是完全相同的,而且不能再往后延伸一段。

可以得出,最大循环节段数为 \(t_i=\displaystyle\left\lfloor\frac{\mathrm z_i}{i-1}\right\rfloor+1\)。注意:为了最后留至少一个字符给 \(C\),所以如果循环节把整串排满了,要 \(-1\)

\(pre_i\) 表示 \(S[1,i]\) 中出现奇数次的字符数量,\(suf_i\) 表示 \(S[i,n]\) 中出现奇数次的字符数量。

对循环节段数 \(k\) 奇偶分类讨论:

  • \(k\equiv1\pmod 2\)

    不难发现,段数为奇数时,\(F(C)\) 保持不变(两个不同的奇数 \(k\) 对应的串 \(C\) 只差偶数段循环节,正好抵消了)。

    所以,我们只要找到 \(S[1,j](j<i)\) 使得 \(F(A)=pre_j\le F(C)=suf_i\)(算 \(k=1\) 时的 \(F(C)\))。

    这部分的贡献为:满足条件的 \(j\) 的个数 \(\times\) 满足条件的 \(k\) 的个数。

  • \(k\equiv0\pmod 2\)

    段数为偶数时 \(F(C)\) 也保持不变(原因同上),且等于 \(suf_1\)

    所以,我们只要找到 \(S[1,j](j<i)\) 使得 \(F(A)=pre_j\le F(C)=suf_1\)

    这部分的贡献为:满足条件的 \(j\) 的个数 \(\times\) 满足条件的 \(k\) 的个数。

然后发现做完了。

🐴拉🚗

【模板】manacher 算法

\(S\) 中最长回文串长度。

先把字符串变成 \(s_1|s_2|s_3|s_4|s_5\) 这个样子,这样一定有一个中心。

模仿 Z 函数,设 \(\operatorname{manacher}_i\) 表示以 \(i\) 为中心最长回文串半边长(包括中间)。

我们维护当前 \(i+\operatorname{manacher}_i-1\) 的最大值 \(r\),以及其对应的 \(i\) ,令其为 \(mid\)

可以考虑把 \(i\)\(mid\) 为中心翻折,得到 \(\operatorname{manacher}_i\ge\min(\operatorname{manacher}_{2mid-i},r-i+1)\)

然后跟 Z 函数类似地右移 \(r\) 即可。

int mid=0,r=0;
for(int i=1;i<n;i++){
    if(i<=r)manacher[i]=min(manacher[(mid<<1)-i],r-i+1);
    while(s[i-manacher[i]]==s[i+manacher[i]])++manacher[i];
    if(manacher[i]+i-1>=r)r=manacher[i]+i-1,mid=i;
}

AC 自动机

多模式串 \(s_i\) 配单文本串 \(S\)

先将模式串 \(s_i\) 都放到 trie 里面。

\(\operatorname{fail}_i\) 表示 trie 上的一个点 \(i\) 的最长能在 trie 上查询的真后缀的对应节点。

vTtfkn.png

  1. \(\operatorname{fail}_1=0\):最长真后缀为 \(\varnothing\)
  2. \(\operatorname{fail}_2=1\):最长真后缀为 \(\texttt{a}\)
  3. \(\operatorname{fail}_3=5\):最长真后缀为 \(\texttt{b}\)
  4. \(\operatorname{fail}_4=9\):最长真后缀为 \(\texttt{bb}\)
  5. \(\operatorname{fail}_5=0\):最长真后缀为 \(\varnothing\)
  6. \(\operatorname{fail}_6=1\):最长真后缀为 \(\texttt{a}\)
  7. \(\operatorname{fail}_7=2\):最长真后缀为 \(\texttt{aa}\)
  8. \(\operatorname{fail}_8=3\):最长真后缀为 \(\texttt{aab}\)
  9. \(\operatorname{fail}_9=5\):最长真后缀为 \(\texttt{b}\)

构建方法:设当前求 \(i\) 点的 \(\operatorname{fail}\),那就从 \(i\) 在 trie 上的父亲 \(fa_i\) 开始,往上跳 \(\operatorname{fail}\) 直到可以往下走为止。

具体的,设当前在点 \(u\)

  • \(\operatorname{trie}_{u,c}\) 存在,那么 \(\operatorname{fail}_{\operatorname{trie}_{u,c}}=\operatorname{trie}_{\operatorname{fail}_u,c}\)
  • 否则,令 \(\operatorname{trie}_{u,c}=\operatorname{trie}_{\operatorname{fail}_u,c}\)
inline void init(){
    L=1,R=0;
    for(int i=0;i<26;i++)
        if(trie[0][i])q[++R]=trie[0][i];
    while(L<=R){
        int u=q[L++],t=fail[u];
        for(int i=0;i<26;i++){
            int v=trie[u][i];
            if(v)fail[v]=trie[t][i],q[++R]=v;
            else trie[u][i]=trie[t][i];
        }
    }
}

第二个操作可以使不断往上跳 \(\operatorname{fail}\) 的过程一步到位。

匹配方法:如果文本串在树上匹配到了一个串,那么一定能匹配到跳 \(\operatorname{fail}\) 能跳到的所有点。

【模板】AC 自动机(简单版)

给定 \(n\) 个模式串 \(s_i\) 和一个文本串 \(t\),求有多少个不同的模式串在文本串里出现过。

\(1 \leq |t| \leq 10^6\)\(1 \leq \sum\limits_{} |s_i| \leq 10^6\)

如果一个串出现过了,那么它的所有 \(\operatorname{fail}\) 都出现过,所以我们只要在节点上打个 tag,如果访问过了,就不继续。

复杂度 \(O(|t|+26\sum|s_i|)\)

fail 树

不难发现,AC 自动机的 \(\operatorname{fail}\) 和 KMP 的 \(\operatorname{p}\) 一样可以连成一棵树。

【模板】AC 自动机(加强版)

\(N\) 个由小写字母组成的模式串 \(s_i\) 以及一个文本串 \(T\)。你需要找出哪些模式串在文本串 \(T\) 中出现的次数最多。

\(N\le150,|s_i|\le70,|T|\le10^6\)

这题暴力可过,但是同样可以在 fail 树上树形 dp。

【模板】AC 自动机(二次加强版) 要树形 dp 才能过。

复杂度 \(O(|t|+26\sum|s_i|)\)

非速通

SAM

一个字符串 \(S\) 的 SAM 是一个接受 \(S\) 的所有后缀的最小 DFA。

从初始状态出发,转移到了一个终止状态,则路径上所有转移连起来一定是 \(S\) 的一个后缀。

\(S\) 的每个子串均可用一条从初始状态到某个状态的路径构成。

画 SAM 工具

结束位置(endpos)

对于 \(S\) 的一个非空子串 \(s\),记 \(\operatorname{endpos}(s)\)\(S\) 中所有 \(s\) 的结束位置集合,令 \(\operatorname{endpos}(st)=\{x|x\in\mathbb{N},x\le |S|\}\)(注意包含 \(0\))。

对于字符串 \(\texttt{abababbb}\)\(\operatorname{endpos}(\texttt{ab})=\{2,4,6\}\)

可能存在两个 \(S\) 的非空子串 \(s_1,s_2\),满足 \(\operatorname {endpos}(s1)=\operatorname {endpos}(s2)\),这样所有 \(S\) 的非空子串可以依据 \(\operatorname {endpos}\) 分为若干个等价类。

SAM 中的每个状态对应一个或多个 \(\operatorname {endpos}\) 相同的子串。

所以 SAM 中的状态数等于所有子串的等价类的个数,再加上初始状态。

\(\operatorname {endpos}\) 我们可以得到一些重要结论:

  • 字符串 \(S\) 的两个不同非空子串的 \(\operatorname {endpos}\) 相同,当且仅当其中一个每一次出现都是以另一个的真后缀的形式存在的。

    证明:如果 \(\operatorname {endpos}\) 相同,所以较短者必为较长者的真后缀,所以在较长者每一次出现时,较短者必出现。

  • 对于字符串 \(S\) 的两个非空子串 \(s_1,s_2\)\(|s_1|\le|s_2|\)),有 \(\operatorname {endpos}(s_1)\subseteq\operatorname {endpos}(s_2)\),或 \(\operatorname {endpos}(s_1)\cap\operatorname {endpos}(s_2)=\varnothing\)。取决于 \(s_1\) 是否为 \(s_2\) 的后缀。

    证明:如果 \(\operatorname {endpos}(s_1)\cap\operatorname {endpos}(s_2)\neq\varnothing\),那么在一个位置定有 \(s_1\)\(s_2\) 的后缀的形式出现,那么在每次 \(s_2\) 出现时 \(s_1\) 必以其后缀形式出现,即 \(\operatorname {endpos}(s_1)\subseteq\operatorname {endpos}(s_2)\)

  • 一个 \(\operatorname {endpos}\) 等价类中的所有子串长度恰好覆盖一个区间。

    证明:

    令等价类中长度最小的字符串为 \(s_l\),最大的为 \(s_r\),那么 \(s_l\)\(s_r\) 的后缀。

    对于 \(s_r\) 一个后缀 \(s(|s|\ge|s_l|)\),由第二个结论:\(\operatorname {endpos}(s_l)\subseteq\operatorname {endpos}(s),\operatorname {endpos}(s)\subseteq\operatorname {endpos}(s_r)\)

    又有 \(\operatorname {endpos}(s_l)=\operatorname {endpos}(s_r)\),所以 \(\operatorname {endpos}(s_l)=\operatorname {endpos}(s_r)=\operatorname {endpos}(s)\)

    即满足条件的 \(s\) 均会出现在等价类中,所以恰好覆盖一个区间。

\(\operatorname {endpos}(x)\) 等价类中长度最大的为 \(t_x\)

一个后缀链接 \(\operatorname{link}(x)\) 中, \(x\) 代表一个状态,设 \(t_x\) 后缀中最长且不属于 \(\operatorname {endpos}(x)\) 的后缀为 \(g\),那么 \(\operatorname{link}(x)\) 指向 \(g\) 所在的状态。

不难发现,\(\operatorname{link}\) 也能类似于 \(\operatorname{fail}\) 变成一棵以 \(st\) 为根的树,我们叫它后缀链接树或 parent 树。

一些性质(对于一个字符串 \(S\)):

  • 共有 \(|S|\) 个叶子结点,代表 \(S\)\(|S|\) 个前缀所属状态。

    证明:没有点会链接到前缀对应的等价类,非前缀不是属于前缀所在状态,就是能通过 link 。

  • 后缀链接树(SAM)的节点个数最多为 \(2n-1\),后面会证。

  • 任意串的后缀全部位于该串所在状态的后缀链接路径上。

  • 一个状态的 \(\operatorname{endpos}\) 是后缀链接树上子树内所有叶子的 \(\operatorname{endpos}\) 的并。

    后缀链接树上的边可以看做 \(\operatorname{endpos}\) 的偏序关系。

线性构造

后缀链接树不够用,建出 SAM 才行。(建 SAM 的算法叫 Blumer 算法)

\(\operatorname {endpos}(x)\) 等价类中长度最大的长度为 \(\operatorname{len}(x)\)

假设当前已经完成了 \([1,i-1]\) 的,当前加入字符 \(S_i=c\)

  • \([1,i-1]\) 这一前缀所在状态是 \(last\),那我们创建一个新节点 \(u\),表示后缀 \([1,i]\) 所在状态,由定义:\(\operatorname{len}(u)=\operatorname{len}(last)+1\)
  • \(last\) 开始跳 \(\operatorname{link}\),如果当前状态没有 \(c\) 的转移,那就创建一个 \(c\) 的转移,指向 \(u\)
  • 如果跳到了有 \(c\) 的转移的点,设其为 \(p\),设 \(q\)\(\delta(p,c)\),如果 \(\operatorname{len}(q)=\operatorname{len}(p)+1\),那就令 \(\operatorname{link}(u)=q\)
  • 否则,构造一个新点 \(v\)\(v\) 继承 \(q\) 出发的转移和 \(\operatorname{link}\),并且 \(\operatorname{len}(v)=\operatorname{len}(p)+1\),然后令 \(\operatorname{link}(u)=\operatorname{link}(q)=v\)
  • 最后,要从 \(p\) 继续跳 \(\operatorname{link}\),并把路径上原来指向 \(q\) 的边指向 \(v\)

复杂度证明:

不难发现,一次加点最多加两个,再加上前两个点不可能加点,一共 \(2n-2\),但是还有初始状态,所以是 \(2n-1\) 个。

发现转移数也是 \(3n+O(1)\) 的,具体可以看 这里

其它部分的复杂度显然,主要是两次跳 \(\operatorname{link}\) 的过程。

第一次比较显然,最多创建转移数条新边。

对于第二次:

不难发现,第二次跳 \(\operatorname{link}\) 的过程跳到第一个不指向 \(q\) 的位置就可以停了,再往上跳,就会指向 \(\operatorname{link}(q)\)

\(\text{depth}(x)\) 表示后缀链接树上 \(x\) 的深度。

  • 引理:若 \(x\to y\) 存在转移边,则 \(\text{depth}(x)+1\ge\text{depth}(y)\)

vqulh8.png

\(last'\) 表示在构建 \(last\) 时的 \(last\)\(v\) 在一些时候可能代表 \(q\)

在构建 \(last\) 时,第一次跳 \(\operatorname{link}\) 的过程为下面的绿色部分,第二次为紫色部分,令紫色部分最上方的点为 \(t''\)

构建 \(u\) 时,第一次跳 \(\operatorname{link}\) 的过程为下面的红色部分,第二次为棕色部分,令棕色部分最上方的点为 \(t'\)

不难发现,构建 \(last\) 时第二次跳的距离为 \(\text{depth}(v'')-\text{depth}(t'')+1\),构建 \(u\) 时为 \(\text{depth}(v')-\text{depth}(t')+1\)

引理 \(\text{depth}(v')\le\text{depth}(t'')+1\),换句话说就是 \(t''\) 最多往一层。

这说明,构建一个点时第二次开始跳的位置的深度,最多是构建上一个点时第二次结束的位置的深度 \(+1\)

不难发现深度最多 \(+|S|\),构建而跳一直是跳到深度更低的,所以势能分析一下,复杂度 \(O(|S|)\)


瓶颈在于复制。

朴素的构造是 \(O(|\sum||S|)\)(每次复制时 memcpy)或 \(O(|S|\log|\sum|)\)(使用 std::map)的。

当然也可以开一个 std::vector,在每次加转移时把字符压进去。这样可以均摊 \(O(n)\) 复制。

但是由于常数问题,\(|\sum|=26\) 时第一种最快。

代码:

inline void add(int c){
    int p=last,u=++cnt;last=u;
    clear(u);len[u]=len[p]+1;
    while(p&&!son[p][c])son[p][c]=u,p=link[p];
    if(!p){link[u]=1;return;}
    int q=son[p][c];
    if(len[q]==len[p]+1){link[u]=q;return;}
    int v=++cnt;copy(v,q);len[v]=len[p]+1;link[u]=link[q]=v;
    while(p&&son[p][c]==q)son[p][c]=v,p=link[p];
}

本文字符串下标从 \(1\) 开始。

\(S[l,r]\) 表示字符串 \(S\)\(l\)\(r\) 的部分。

应用

检查字符串是否出现

【模板】AC 自动机(简单版)

给定 \(n\) 个模式串 \(s_i\) 和一个文本串 \(t\),求有多少个不同的模式串在文本串里出现过。

\(1≤|t|≤10^6\)\(1≤∑|s_i|≤10^6\)

直接根据建出来的 SAM 转移即可。


计算字符串出现次数

显然,一个字符串 \(s\)\(S\) 中的出现次数为 \(\displaystyle \sum_{i=1}^{|S|}[s\text{ is a suffix of }S[1,i]]\)

而如果 \(s\) 是一个 \(S[1,i]\) 的后缀,那么在后缀链接树上 \(s\) 对应的节点定是 \(S[1,i]\) 对应的叶子结点的祖先。

于是树形 dp 即可。

【模板】AC 自动机(二次加强版)

有 N 个由小写字母组成的模式串 si 以及一个文本串 S,求每个模式串在文本串中的出现次数。

\(1≤n≤2×10^5\)\(∑|s_i|≤2×10^5\)\(|S|≤2×10^6\)

上面已经讲了,但是被卡空间了。【模板】后缀自动机 (SAM) 也类似。


本质不同的子串个数

一个子串就是一条从 \(st\) 开始的路径,而 SAM 又是一张 DAG,所以转换为求 DAG 上本质不同路径条数,可以拓扑排序+DP,转移方程 \(d_u\) 表示点 \(u\) 的出度:

\[\displaystyle f_u=d_u+\sum_{v,v\in \delta(u)}f_v \]

但是有更优美的做法,每个状态对应的 \(\operatorname{endpos}\) 等价类中的元素,与从 \(st\) 开始、以该状态结尾的路径构成双射。

所以只需求出每个点的等价类大小,即对于点 \(x\)\(\operatorname{len}(x)-\operatorname{len}(\operatorname{link}(x))\)

不同子串个数

给一个的字符串,求不同的子串的个数。

直接来即可。


[SDOI2016]生成魔咒

共进行 \(n\) 次操作,每次在数组 \(S\) 的末尾加入一个数 \(x_i\)。每次操作求出,\(S\) 的不同子串个数。

\(n\le10^5,x_i\le10^9\)

由于 \(x_i\) 比较大,用 std::map 即可。每次末尾加一个数,注意一下即可。

本质不同子串总长度

拓扑排序的方法同样行得通,转移方程(\(f_u\) 是上一题的):

\[g_u=f_u+\sum_{v,v\in \delta(u)}g_v \]

第二种同样行得通,因为 \(\operatorname{endpos}\) 等价类恰好形成一个区间,所以等差数列求和即可。

求字典序第 k 大子串

求出 \(f_i\) 之后,每一层减下去即可。

[TJOI2015]弦论

给定的长度为 \(n\) 的字符串,求出它的第 \(k\) 小子串是什么。

\(t\)\(0\) 则表示不同位置的相同子串算作一个,\(t\)\(1\) 则表示不同位置的相同子串算作多个。

\(1\leq n \leq 5 \times 10^5\)\(0\leq t \leq 1\)\(1\leq k \leq 10^9\)

\(t=1\)\(f_i\) 还需改动一下。

求第一次出现的位置

维护 \(\operatorname{firstpos}(i)\) 表示该状态对应的 \(\operatorname{endpos}\) 等价类的第一次出现的末尾位置。

当加入新点时 \(\operatorname{firstpos}(u)=\operatorname{len}(u)\)

复制一个点时 \(\operatorname{firstpos}(v)=\operatorname{firstpos}(q)\)

第一次出现位置即为查询的字符串对应状态的 \(\operatorname{firstpos}\) 减去查询的字符串的长度再 \(+1\)

求每一次出现的位置

每一次出现位置对应这后缀链接树上子树内所有叶子节点的位置。

暴力就可以了。

posted @ 2023-03-07 15:45  TOBapNw-cjn  阅读(31)  评论(0编辑  收藏  举报