字符串

小时候写的,应该含有大量错误,可以帮忙查查。

回文

manacher

manacher 是以每个点为中心暴力扩展回文串的巧妙优化。

\(p_i\) 表示以 \(i\) 为中心的最长回文串长度,\(r,mid\) 表示 \(\displaystyle\max_{j=1}^i\{j+p_j-1\}\) 和对应的 \(j\)

由回文串的对称性可知当 \(r\ge i\)\(p_i\ge\min(p_{2mid-i},r-i+1)\),所以将 \(p_i\) 置为该值再暴力扩展。

注意到暴力扩展每成功一次都会使 \(r\gets r+1\),故线性。

代码

s[++n] = '#';
for (rll i = 1; i <= n1; i++)
    s[++n] = s1[i], s[++n] = '#';
s[++n] = '$';
for (rll i = 1, mid = 1, r = 1; i <= n; i++) {
    if (i <= r)
        p[i] = min(p[mid * 2 - i], r - i + 1);
    while (s[i - p[i]] == s[i + p[i]])
        p[i]++;
    if (i + p[i] - 1 > r)
        mid = i, r = i + p[i] - 1;
}

习题

[POI 2010] ANT-Antisymmetry

[国家集训队] 最长双回文串

回文自动机

回文自动机,简称 PAM(Palindrome Automaton),又称回文树。

思路

建立两颗字典树,分别表示所有回文半径为奇数和偶数的回文串。

具体表示方法:每个节点向上爬到根节点,再从根节点返回这个节点,所经过的路径就表示了一个回文串。在奇回文树上靠近根节点的字符只计算一次,而偶回文树上要计算两次。

PAM 的构造是一个增量过程。

因为 \(s\) 的所有回文后缀都是其最长回文后缀的后缀,所以从以 \(i\) 结尾的最长回文串开始,每次找到它的最长回文真后缀,就能遍历以 \(i\) 结尾的所有回文串。记 \(fail_p\) 为节点 \(p\) 的最长回文真后缀在 PAM 上的节点。

对于新加入的一个字符,考虑在 \(i-1\) 的回文后缀中跳到一个最短的能通过 \(s_i\) 左右扩展的节点 \(p\),把新点加在它下面(这个新点可能已经存在,如果存在就不用新建),然后 \(fail_p\) 向上跳即可找到新点的 \(fail\)

显然,本质不同的回文串数就是 PAM 除奇偶根外的点数,PAM 的构造过程其实证明了本质不同的回文串只有 \(O(n)\) 种。

代码

ll newp(ll l) {
    len[++idt] = l;
    memset(t[idt], 0, lenpeof t[idt]);
    return idt;
}
void init() {
    s[0] = '$';
    idt = -1, last = 0;
    newp(0), newp(-1);
    fail[0] = 1;
}
ll get(ll p) {
    while (s[now - 1 - len[p]] != s[now])
        p = fail[p];
    return p;
}
void insert() {
    ll c = s[now] - 'a', p = get(last);
    if (!t[p][c]) {
        ll q = newp(len[p] + 2);
        fail[q] = t[get(fail[p])][c];
        t[p][c] = q;
    }
    last = t[p][c];
}

性质

  • \(s_{[1,i]}\) 在 PAM 上的状态为 \(p\),那么它和它所有 fail 树上的祖先就是所有以 \(i\) 结尾的回文串。

  • 若状态 \(p\) 出现了一次,那么 fail 树上它所有的祖先都出现了一次。

习题

秩序魔咒

***P哥旋转

应该是扯蛋,懒得改了,请跳过。本题数据过水。


\(s\) 复制一遍,就相当于求长度为 \(n\) 的区间的最大答案。

考虑把这题当作离线区间本质不同回文串个数模板题来做。

先把完整的 PAM 建出来,然后扫描线,假设现在扫到 \(i\),BIT 上 \(j(j<i)\)\([j,i]\) 的答案。

\(pos_i\) 为 PAM 插入到 \(i\) 后的节点,那么现在需要更新 \(pos_i\) 在 fail 树上到根的路径上所有点的贡献(除奇偶根)。

\(p\in\{pos_i\to 1\},p>1\),它上一次出现的位置为 \(lst_p\),那么需要对区间 \([lst_p+1,i-len_p+1]\) 加一。

接下来有一个关键结论:\(lst_{fail_p}=i-len_p+1\),即 \(p\) 的最长回文后缀上一次出现的位置为 \(p\) 当前出现的位置。

证明:首先 \(fail_p\) 一定在这个位置出现过一次,然后不可能 \(lst_{fail_p}>i-len_p+1\),否则 \([lst_{fail_p},i]\)\([i-len_p+1,i]\) 一个比 \([i-len_{fail_p}+1,i]\) 更长的回文后缀,与 \(fail\) 定义矛盾。

于是,从 \(p\) 到根的链上所有修改区间可以并成一个连续区间,即 \([lst_{pos_i}+1,i-len_{top_p}+1]\),其中 \(top_p\) 表示 \(p\) 的最浅的 \(>1\) 的祖先。

\(lst_{pos_i}\) 是 fail 树上 \(pos_i\) 子树内所有点当前最晚结束位置 \(-len_{pos_i}+1\),用线段树维护子树。

**[CERC2014] Virus synthesis

注意到几个东西:

  1. 一段序列只能由一个回文串暴力扩展而来。

  2. 只有偶回文串有用。

考虑在 PAM 上对回文串 DP,记 \(f_p\) 表示以点 \(p\) 代表回文串的答案,\(ft_p\) 表示 \(p\) 在 trie 上的父亲,\(q\) 是 fail 树上 \(p\) 的一个祖先:

\[f_p\gets f_{ft_p}+2-[len_{ft_p}>0] \]

\[f_p\gets\begin{cases} f_q+len_p-len_q,&len_q>\frac{len_p}{2}\\ f_q+\frac{len_p}{2}-len_q+1,&len_q\le\frac{len_p}{2} \end{cases}\]

答案即为 \(\min\{n-len_p+f_p\}\)

考虑维护 \(top_p\) 表示最深的 \(len_q\le\frac{len_p}{2}\)\(q\) 的位置,即可单点修改,树剖维护链上 \(\min\),因为只会在下面加点所以可以维护每条链上的前缀 \(\min\),两端零散链用线段树维护,复杂度单 \(\log\)

但其实还可进一步优化。

性质 \(1\):如果钦定当前这一步不复制,最优方案是从最长的偶回文后缀 \(lst_p\) 转移。

证明:对于任意两个转移点 \(q_1,q_2(len_{q_1}>len_{q_2})\)\(f_{q_1}\le f_{q_2}+len_{q_1}-len_{q_2}\),故 \(f_{q_1}+len_p-len_{q_1}\le(f_{q_2}+len_{q_1}-len_{q_2})+len_p-len_{q_1}=f_{q_2}+len_p-len_{q_2}\),所以 \(q_1\) 一定不劣。

性质 \(2\):对于一个偶回文串,最优操作的最后一步一定是复制。

证明:考虑归纳,首先 \(len_p=2\) 时结论正确。令 \(lst_p\) 的最优转移点为 \(l(len_l\le\frac{len_{lst_p}}{2})\),那么 \(lst_p\to p\) 的代价是 \(f_{lst_p}+len_p-len_{lst_p}=(f_l+\frac{len_{lst_p}}{2}-len_l+1)+len_p-len_{lst_p}=f_l+len_p-\frac{len_{lst_p}}{2}-len_l+1\),而 \(l\to p\) 的代价是 \(f_l+\frac{len_p}{2}-len_l+1\)。因为 \(len_p-\frac{len_{lst_p}}{2}>\frac{len_p}{2}\),所以从 \(f_l\) 转移更优。

因此 \(f_{top_p}\) 不劣于 \(f_{lst_p}\),故有用的转移只有 \(f_{ft_p},f_{top_p}\),线性。

注意,DP 必须写成 bfs。

***【模板】回文划分计数

一个朴素的 DP 是 \(\displaystyle f_i=\sum_{p\in\{pos_i\to 1\}}f_{i-len_p}\)

考虑 P 哥旋转的结论:\(lst_{fail_p}=i-len_p+1\),可以得出以 \(i\) 结尾的一些长度成等差序列回文串 \([l,r]\)(它们在 fail 树上的链是极长的)在 \(i\) 之前第一次出现的位置 \([l',r']\)

\(\begin{aligned} \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ [l_1\ \ \ \ \ r_1]&\\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ [l'_1\ \ \ \ \ r'_1]\ \ \ \ \ \ &\\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ [l_2\ \ \ \ \ \ \ \ \ \ \ r_2]&\\ \ \ \ \ \ \ \ \ [l'_2\ \ \ \ \ \ \ \ \ \ \ r'_2]\ \ \ \ \ \ \\ \ \ \ \ \ \ \ \ [l_3\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ r_3]&\\ \ \ [l'_3\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ r'_3]\ \ \ \ \ \ \\ \ \ [l_4\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ r_4]&\\ \end{aligned}\)

扫描线扫到 \(i\) 之前,\(pos_i\) 根链所贡献的转移点集为 \(\{lst_i-len_p+1\}\),而扫到 \(i\) 后,转移点集为 \(\{i-len_p+1\}\)。而观察上图可知,这两个点集的差集为 \(\{l_1\}\),因此可以 \(O(1)\) 更新和统计一个极长的等差祖孙链贡献。

其实等差序列的数量只有 \(O(\log n)\) 个。因为回文后缀是回文串的 border,而 border 构成的等差序列只有 \(O(\log)\) 个。

***Palindrome Partition

考虑令 \(s\gets s_1 s_{|s|} s_2 s_{|s|-1}\dots s_{\frac{|s|}{2}} s_{\frac{|s|}{2}+1}\),那么就转化为了求用偶回文串划分的方案数,只在偶数位转移 \(f\),其它和上一题相同。

前缀信息

KMP

思路

考虑求出 \(s\) 每个前缀的最长 border(公共前后缀),前缀 \(i\) 的最长 border 记为 \(fail_i\)

假设现在已经求出 \(fail_{[1,i]}\),考虑求 \(fail_{i+1}\)

令前缀 \(i\) 的某个 border 为 \(j\),若满足 \(p_{j+1}=p_{i+1}\),就说明 \(j\) 也是前缀 \(i+1\) 的 border。否则可以从大到小遍历前缀 \(i\) 的 border(\(j\gets fail_j\)),相当于将前缀 \(i\) 的最长相同前后缀分别同时向两边缩小,不断放宽条件,直至其能扩展为 \(i+1\) 的 border,如果 \(j=0\) 还不合法说明没有合法 border。

现在考虑匹配 \(p\),每次强制匹配 \(s_{[1,i]}\) 的一段后缀并维护它能匹配的 \(p\) 的最长前缀,容易证明这样能考虑到所有匹配。

现在已知 \(s_{[i-j,i-1]}=p_{[1,j]}\),需要求出最大的 \(j'\ (0\le j'\le j+1)\) 使得 \(s_{[i-j'+1,i]}=p_{[1,j']}\)。因为 \(p\) 当前能匹配 \(s_{[1,i]}\) 一段后缀的所有前缀是 \(j\) 不断跳 border 能跳到的 \(p\) 的前缀,所以暴力跳 \(fail\) 时跳到一个能扩展到 \(i\) 的前缀就得到了 \(j'\)。如果没有一个这样的前缀,那么 \(j'=0\)

分析复杂度:\(i\) 往后移一位 \(j\) 最多增加 \(1\)\(j\) 每次跳 \(j\gets fail_j\) 至少减 \(1\),因此最多跳 \(n+m\) 次,时间复杂度 \(O(n+m)\)

代码

#include <bits/stdc++.h>
using namespace std;
int n, m, fail[1000005];
char p[1000005], s[1000005];
int main() {
    scanf("%s", s + 1), n = strlen(s + 1);
    scanf("%s", p + 1), m = strlen(p + 1);
    for (int i = 2, j = 0; i <= m; i++) {
        while (j && p[i] != p[j + 1]) j = fail[j];
        if (p[i] == p[j + 1]) j++;
        fail[i] = j;
    }
    for (int i = 1, j = 0; i <= n; i++) {
        while (j && s[i] != p[j + 1]) j = fail[j];
        if (s[i] == p[j + 1]) j++;
        if (j == m)
            cout << i - m + 1 << '\n', j = fail[j];
    }
    for (int i = 1; i <= m; i ++)
        cout << fail[i] << ' ';
    return 0;
}

KMP 自动机

注意到,KMP 看似是用 \(p\) 去尝试匹配 \(s\),实际上当对于每个当前匹配长度和当前 \(s_i\) 的组合,\(p\) 存在一个固定的转移关系。

于是记 \(t_{i,c}\) 表示当匹配了 \(i\) 位时加入字符 \(c\) 匹配的位数,可以得到一张有向图,就是 KMP 自动机。

\(p\) 匹配 \(s\) 相当于 \(s\) 在 KMP 自动机上每次走 \(p\to t_{p,s_i}\) 这条边,走到 \(p=m\) 就意味着匹配了一次。

这种每条路径对应了一个字符串的图叫字典图,自动机,就是将匹配的本质信息提取出来,使匹配变为在字典图上走一条路径,图上代表了若干路径的点叫做状态,匹配抽象而成的边叫做转移。

周期与循环节

\(1\le p\le|s|,\forall 1\le i\le|s|-p,s_i=s_{i+p}\),则称 \(p\)\(s\) 的周期,当 \(p|(|s|)\)\(s_{1\sim p}\)\(s\) 的循环节。

\(t\)\(s\) 的一个前缀时,\(t\)\(s\) 的 border 与 \(|s|-|t|\)\(s\) 的周期条件等价,由二者定义易证。

性质

  • 所有循环节都是最小循环节的倍长。

  • \(k\)\(s\) 的最长 border,则当 \((n-k)\mid n\) 时最小循环节为 \(n-k\),否则为 \(n\)

  • 所有边 \((fail_i,i)\) 构成一棵树(失配树),两个前缀 \(i,j\) 的最长公共 border 是前缀 \(\operatorname{LCA}(i,j)\)

  • \(s\) 在自动机上经过了前缀 \(i\),那么 \(i\) 在失配树上的所有祖先都在 \(s\) 中出现了一次。

习题

[NOIP2020] 字符串匹配

[HNOI2008] GT考试

*[POI 2005] SZA-Template

注意到一个字符串只能用其 border 覆盖,于是枚举 \(n\) 的每个 border。

对于一个 \(s\) 的 border,若要合法那么它在 \(s\) 中每次出现的位置(不妨设为它的末尾所在的位置,钦定它在 \(n+1\) 出现了一次)间隔不超过它的长度。

显然,若一个子串 \(t\) 出现在某一位置那么它的 border 也会在该位置出现,所以前缀 \(x\) 的所有出现位置为失配树上 \(x\) 的子树内所有点的编号以及 \(x\)

这是一个简单树上 dsu,用两个 set 分别维护出现位置和最大间隔。

但题解存在更聪明的做法:注意到仅需考虑 \(0\to n\) 路径上的点,于是可以一边往下走一边删不在链上的点,删点时 \(O(1)\) 维护链表是求出前后缀,最大间隔也单调不降,仅需用一个变量维护。

AC 自动机

思路

AC 自动机的思路本质与 KMP 自动机相同,不同之处在于 AC 自动机是将所有模式串建成一张有向图,母串在上面走一条固定路径时匹配了所有的模式串。AC 自动机由所有模式串的 trie 和失配指针构成,实现时它们都作为自动机的转移边。

\(t_0\) 为自动机的初始状态,意义为空串,\(fail_p\) 表示对应的在 trie 上最深的点,使 \(t_0\to fail_p\) 路径对应的字符串是 \(t_0\to p\) 路径对应的字符串的后缀。

类似 KMP,还是强制匹配 \(s\) 地一段后缀,\(s\) 在自动机上走它的固定路径时,到达一个模式串的终止状态就意味着匹配了它一次。

考虑先建出 trie,然后把它补满成字典图,假设当前处理到 \(x\),枚举转移字符 \(i\),分类讨论:

  • \(t_{x,i}\) 不存在,则说明这里失配了,那么 \(s\) 走到这里时为了匹配一段后缀应走到 \(t_{fail_x,i}\)

  • 否则,无需改变 \(t_{x,i}\),只需求出其失配指针,直接在 \(x\) 对应指针上扩展即可:\(fail_{t_{x,i}}=t_{fail_x,i}\)

    这是原字典树上的边,因此需要补全它的状态。

一般用 bfs 实现。

代码

queue<ll> q;
for (ll i = 0; i < 26; i++)
    if (t[0][i])
        q.push(t[0][i]);
while (!q.empty()) {
    ll x = q.front();
    q.pop();
    for (ll i = 0; i < 26; i++) {
        ll y = t[x][i];
        if (y) {
            fail[y] = t[fail[x]][i];
            q.push(y);
        } else
            t[x][i] = t[fail[x]][i];
    }
}

性质

  • \(p\) 出现过一次 \(fail_p\) 也会出现一次。

  • \(t\)\(s\) 中出现的次数(\(t\) 在自动机上,\(s\) 不一定在)为 \(s\) 经过的有向图上的点(可重)总共在 fail 树上 \(t\) 的子树中出现的次数。

  • \(s\) 在被匹配时 \(s_i\) 在点 \(p\),那么 fail 树上 \(p\to 0\) 路径上的所有点代表的字符串都在 \(i\) 处出现了一次。

习题

[NOI2011] 阿狸的打字机

[HNOI2004] L 语言

[USACO12JAN] Video Game G

Mike and Friends

[SDOI2014] 数数

[USACO15FEB] Censoring G

[COCI 2015] Divljak

*[JSOI2009] 有趣的游戏

把机器产生的所有可能序列画出来,会得到一棵 dfs 树,每个点上都有一个字符串。

注意到当这个字符串匹配到了某个人的字母序列后它的子树就都归为这个人,并且后面的人都不能取这颗子树。

一棵子树被取的概率是能算出来的,可以模拟机器一个个加字符,在自动机上转移概率,如果一个能被人取的点有了概率,则这个人取走该子树,将概率清零。

这个过程形如概率在有向图上的流动,流动到一些点时被取走。

但字符串长度是无限的,需要卡时使答案更精确。

最后还会有一些概率没有被取走,那就把所有概率除一个被取走的总概率。

注意特判所有概率均为 \(0\)

*String Set Queries

当操作存在插入和删除,且具有可加性时,分别维护所有插入的贡献和所有删除的贡献。

考虑对操作二进制分组,每加入一次操作就暴力将相同的位合并重构 ACAM,复杂度单 \(\log\)

*[JRKSJ R4] Salieri

\(s\) 经过 AC 自动机上的点以及 \(t_0\) 建虚树,于是所有在 \(s\) 中出现过的串的点都在虚树上。考虑二分答案,在虚树上统计 \(cnt_i\times w_i\ge mid\) 的点数。

注意到虚树上一条边 \((x,y)\) 上所有点的 \(cnt\) 都是 \(y\) 的子树大小 \(b_y\),因此只需统计原树这条路径上 \(w_i\ge\lceil\frac{mid}{b_y}\rceil\)\(w_i\) 数量,直接用主席树维护,复杂度总共 \(\log^2\)

后缀信息

后缀数组

后缀数组,简称 SA(Suffix Array),用于表示 \(s\) 的所有后缀的按字典序排序后的编号序列,\(sa_i=x\) 表示字典序排名为 \(i\) 的后缀为 \(s_{[x\sim n]}\)

思路

后缀数组的本质目的是将相同前缀最大的后缀放在一起以得到一些良好性质。而两个后缀相同前缀最大等价于它们的字典序最接近,从而转化为后缀排序算法。

考虑倍增,假设当前已得到所有长为 \(2^x\) 的子串的排名,要求所有长为 \(2^{x+1}\) 的子串的排名。可以将 \(rk_i,rk_{i+2^k}\) 分别为第一二关键字进行排序。

直接 sort 复杂度 \(O(n\log^2n)\)。但注意到只有两个关键字,可以使用计数排序先后对第二、第一关键字排序,就少了一个 \(\log\)

一般实现中对第二关键字的排序可以这样实现,能很大地减小常数:

  1. 令当前排序的子串长度为 \(len\),后缀 \(n-len+1\sim n\) 没有第二关键字,直接放在最前面。

  2. 后缀 \(i\ (i>len)\) 可作为后缀 \(i-len\) 的第二关键字,因为是按字典序枚举的,应将后缀 \(i-len\) 加入第二关键字排序序列末尾。

#include <bits/stdc++.h>
using namespace std;
int n, len, lim, sa[2000005], rk[2000005], tmp[2000005], cnt[2000005];
char s[2000005];
void cntsort() {
    for (int i = 1; i <= lim; i++)
        cnt[i] = 0;
    for (int i = 1; i <= n; i++)
        cnt[rk[i]]++;
    for (int i = 1; i <= lim; i++)
        cnt[i] += cnt[i - 1];
    for (int i = n; i >= 1; i--)
        sa[cnt[rk[tmp[i]]]--] = tmp[i];
}
int main() {
    cin >> s + 1;
    n = strlen(s + 1);
    for (int i = 1; i <= n; i++) {
        rk[i] = s[i];
        sa[i] = tmp[i] = i;
        lim = max(lim, (int)s[i]);
    }
    cntsort();
    for (len = 1; len <= n; len <<= 1) {
        int now = 0;
        for (int i = n - len + 1; i <= n; i++)
            tmp[++now] = i;
        for (int i = 1; i <= n; i++)
            if (sa[i] > len)
                tmp[++now] = sa[i] - len;
        cntsort();
        for (int i = 1; i <= n; i++)
            tmp[i] = rk[i];
        lim = 0;
        for (int i = 1; i <= n; i++) {
            if (tmp[sa[i]] != tmp[sa[i - 1]] || tmp[sa[i] + len] != tmp[sa[i - 1] + len])
                lim++;
            rk[sa[i]] = lim;
        }
        if (lim == n)
            break;
    }
    for (int i = 1; i <= n; i++)
        cout << sa[i] << ' ';
    return 0;
}

最长公共前缀

\(\operatorname{LCP}(i,j)\) 表示后缀 \(i\) 和后缀 \(j\) 的最长公共前缀。

引理 \(1\)\(\displaystyle\max_{j\ne i}\{\operatorname{LCP}(i,j)\}=\max\big(\operatorname{LCP}(i,sa_{rk_{i-1}}),\operatorname{LCP}(i,sa_{rk_{i+1}})\big)\)。(易证)

\(h_i=\operatorname{LCP}(sa_i,sa_{i-1})\)

引理 \(2\)\(h_{rk_i}\ge h_{rk_{i-1}}-1\)

画个图就很清晰了,\(h_{rk_{i-1}}=|cA|,h_{rk_i}\ge|A|\)

\(\begin{aligned} j:\ &cAC\\ i-1:\ &cAB\\ i:\ &\ \ AB\\ \end{aligned}\)

有了结论 \(2\),便可按后缀长度从大到小递推 \(h_i\)

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

两个后缀的 \(\operatorname{LCP}\)

不妨设 \(rk_i<rk_j\)(若相等为特殊情况),则:

\[\operatorname{LCP}(i,j)=\min_{k=rk_i+1}^{rk_j} ht_k \]

首先容易证明答案至多为这个值,而最小的 \(ht_k\) 可以在排序后的后缀上向前后扩展直至 \(i,j\)

习题

[AHOI2013] 差异

[HAOI2016] 找相同字符

[SDOI2008] Sandy 的卡片

[NOI2015] 品酒大会

**[NOI2016] 优秀的拆分

定义形如 \(\operatorname{AA}\) 的串为双倍串。

考虑枚举 \(\operatorname{AA}\)\(\operatorname{BB}\) 的分界点 \(i\),然后分别统计前缀 \(i\) 的双倍后缀数量和后缀 \(i+1\) 的双倍前缀数量。

有一个重要性质:对于任何长度为 \(2l\) 的双倍串 \(\operatorname{S}\),都可以从任意一对在它之内的距离为 \(l\) 的点对 \((i,i+l)\) 的前后缀信息中得到。

考虑求出一对最长的分别包含 \(i,i+l\) 且起点距离为 \(l\) 的相等串,令 \(\operatorname{B}=\operatorname{LCS}(i-1,i+l-1),\operatorname{A}=\operatorname{LCP}(i,i+l)\),那么这满足条件的最长串为 \(A+B\)。当这两个最长相等串重叠时,它的一些长度为 \(2l\) 子串就是双倍串,即 \(S\)。注意到这些双倍串的起点是连续的,可以用差分维护。

\(\begin{aligned} &\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ [i\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ ][i+l\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ ]\\ &[\ \ \ \ \ \ \ \ \ \operatorname{B}\ \ \ \ \ \ \ \ \ ][\ \ \ \ \ \ \operatorname{A}\ \ \ \ \ \ \ ]\\ &\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ [\ \ \ \ \ \ \ \ \ \operatorname{B}\ \ \ \ \ \ \ \ \ ][\ \ \ \ \ \ \operatorname{A}\ \ \ \ \ \ \ ]\\ &[\ \operatorname{C}\ ]\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ [\ \operatorname{C}\ ]\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ [\ \operatorname{C}\ ]\\ &[\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \operatorname{S}\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ ]\\ &\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \cdot\cdot\cdot\cdot\cdot\cdot\\ &\ \ \ \ \ \ \ [\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \operatorname{S}\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ ]\\ \end{aligned}\)

考虑枚举 \(l\),然后钦定 \(|\operatorname{S}|=2l\) 必须从 \(i\equiv 1\pmod l\) 的点对 \((i,i+l)\) 得到,然后暴力枚举 \(i\) 更新贡献,复杂度调和级数。

**REPEATS - Repeats

使用上一题的 trick,枚举 \(l\),枚举关键点 \((i,i+l)\),令 \(lcp=\operatorname{LCP}(i,i+l),lcs=\operatorname{LCS}(i-1,i+l-1)\) 那么 \([i-lcs,i+l+lcp-1]\) 就是一段存在周期为 \(l\) 的极长子串,\(\lfloor\frac{lcs+lcp+l}{l}\rfloor\) 可以作为答案。

*Liar

注意到 \(t\) 的子串在 \(s\) 中的任意匹配都是一段极长匹配,因为当分段数和 \(t\) 的匹配长度都相同时,在 \(s\) 中的匹配末尾越早越优。

同理,对于 \(s\) 的一段前缀,它应尽可能地匹配到最长的 \(t\) 的前缀。

\(f_{i,j}\) 表示 \(s_{[1,i]}\) 能匹配到 \(t\) 中的最晚结尾位置,考虑当前位置是否开始匹配,有:

\[f_{i+1,j}\gets f_{i,j} \]

\[f_{i+l,j+1}\gets f_{i,j}+l \]

其中,\(l\)\(s_{[i+1,n]}\)\(t_{[f_{i,j}+1,m]}\) 的最长公共前缀,用 SA 维护。

*[HEOI2016/TJOI2016] 字符串

相当于求 \(\displaystyle\min\Big(\max_{i=a}^b\{\min(\operatorname{LCP}(i,c),b-i+1)\},d-c+1\Big)\)

发现上式难以直接维护,考虑二分答案。对于一个 \(mid\),需要判断是否存在后缀 \(i\in[a,b-mid+1]\),使得 \(\operatorname{LCP}(i,c)\ge mid\)

可以在 hight 数组上二分出最大的包含 \(rk_c\) 的区间 \([L,R]\),主席树查询否存在 \(i\in[a,b-mid+1],rk_i\in[L,R]\),复杂度 \(\log^2\)

*「CGOI-1」大师选徒

注意到要找的区间与原区间差分数组为相反数,考虑将在差分数组后面拼上差分数组的相反数,求 SA。

那么答案区间 \([b,b+r-l]\) 的充要条件是 \(\operatorname{LCP}(b',l)\ge r-l\)\(a_b=s-a_l\)

类似上一题地二分出条件一的区间,判断区间内是否有 \(s-a_l\) 即可。

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

出现次数 \(>1\) 的子串等价于任意两个不同后缀的 \(\operatorname{LCP}\),所以只会选某个 \(ht_i\)

对于一个后缀 \(i\),它与 \(ht_i\) 作为最小值的区间内所有后缀的 \(\operatorname{LCP}\) 都是长为 \(ht_i\) 的相同子串,区间大小就是出现次数。

可以对 \(ht_i\) 建笛卡尔树维护。

后缀自动机

前置知识

\(\operatorname{endpos}\) 集合

\(s\) 的某一个子串都会在 \(s\) 中出现 \(\ge1\) 次,这个子串在 \(s\) 中所有出现位置的末尾位置构成的集合,称为这个子串的 \(\operatorname{endpos}\) 集合。

例如,在 \(\tt abcbc\) 中,\(\operatorname{endpos}(\tt bc)=\{3,5\}\)

性质 \(1\):一个子串的 \(\operatorname{endpos}\) 为它的后缀的 \(\operatorname{endpos}\) 的子集。(易证)

性质 \(2\)\(\operatorname{endpos}\) 集合只有不交和包含关系。

证明:反证法:假设 \(\operatorname{endpos}(t_1)\nsubseteqq\operatorname{endpos}(t_2),\operatorname{endpos}(t_2)\nsubseteqq\operatorname{endpos}(t_1),\operatorname{endpos}(t_1)\cap\operatorname{endpos}(t_2)=S\ne\varnothing\),那么一定存在 \(t_0\) 使得 \(\operatorname{endpos}(t_0)\subset S\),由 \(1.1\)\(t_0\) 同时为 \(t_1,t_2\) 的后缀,所以 \(\operatorname{endpos}(t_1),\operatorname{endpos}(t_2)\) 是包含关系,矛盾。

等价类

所有 \(\operatorname{endpos}\) 集合相同的子串属于同一个等价类。

例如,在 \(\tt abcbc\) 中,\(\operatorname{endpos}({\tt c})=\operatorname{endpos}({\tt bc})=\{3,5\}\),于是称子串 \(\tt c\) 和子串 \(\tt bc\) 为一个等价类。

性质 \(3\):同一个等价类中的较短字串为较长子串的后缀。

性质 \(4\):同一个等价类中的所有子串长度依次增加 \(1\),它们是最长子串的长度大于等于最短子串的后缀。

性质 \(5\):任意两个不同的等价类不交。

证明:每个子串恰好属于一个等价类。

定义

  • SAM:一个字符串 \(s\) 的 SAM 是一个接受 \(s\) 所有后缀的最小的有限状态自动机。不同于 KMP 自动机和 AC 自动机,SAM 是一个 DAG。

  • 状态:SAM 的节点被称作状态,所有状态与所有等价类一一对应,每条边是一个字符,每条路径是一个字符串。

  • 初始状态:初始状态 \(t_0\) 表示空串,\(t_0\) 到任意一个状态的任意一条路径与 \(s\) 的每一个子串一一对应。

  • 终止状态:\(t_0\) 到终止状态的任意一条路径都对应了 \(s\) 的一个后缀。

  • \(\operatorname{longest},\operatorname{shortest},\operatorname{len},\operatorname{minlen}\):由定义可知状态 \(p\) 表示的所有子串一一对应 \(t_0\to p\) 的每一条路径。令 \(p\) 表示的子串集合为 \(\operatorname{sub(p)}\),其中的最长的子串为 \(\operatorname{longest(p)},\operatorname{len(p)}=|\operatorname{longest(p)}|\),最短子串为 \(\operatorname{shortest(p)},\operatorname{minlen(p)}=|\operatorname{shortest(p)}|\)

    性质 \(6\)\(\operatorname{sub}(p)\) 中的所有子串都是 \(\operatorname{longest}(p)\) 的后缀,确切的表述:\(\operatorname{sub(p)}=\{s'|s'\in\operatorname{suffix}(\operatorname{longest}(p)),\operatorname{shortest}(p)\in{\operatorname{suffix}(s')}\}\)

  • \(\operatorname{endpos}(p/s')\):状态 \(p\) 所在等价类的 \(\operatorname{endpos}\) 集合/子串 \(s'\) 所在等价类的 \(\operatorname{endpos}\) 集合。

  • \(\operatorname{link(p)}\):由等价类的性质可知它们的 \(\operatorname{endpos}\) 包含关系构成一棵树(\(\operatorname{link}\) 树)。\(\operatorname{link}(p)\) 是最小的 \(\operatorname{endpos}\) 包含状态 \(p\) 的状态。

    性质 \(7\)\(\operatorname{longest}(\operatorname{link}(p))\)\(\operatorname{shortest}(p)\) 的最长真后缀,\(\operatorname{len}(\operatorname{link}(p))+1=\operatorname{minlen}(p)\)

    如图,同一垂线上的字符相同:

    \[\begin{aligned} \operatorname{sub}(p)=\begin{cases} [\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \operatorname{longest(p)}\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ &]\\ \ \ \ [&]\\ \ \ \ \ \ \ [&]\\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \vdots\\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ [\ \ \ \ \ \ \ \ \ \ \operatorname{shortest(p)}&]\\ \end{cases}&\\ \operatorname{sub}(\operatorname{link(p)})\begin{cases} \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ [\ \ \ \ \ \ \ \ \operatorname{longset}(\operatorname{link}(p))&]\\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \vdots\\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ [\ \ \ \operatorname{shortest}(\operatorname{link(p)})&]\\ \end{cases}& \end{aligned}\]

  • \(\operatorname{t}(p,c)\):状态 \(p\) 通过字符 \(c\) 转移到的状态。

    转移的定义:\(\operatorname{sub}(\operatorname{t}(p,c))\) 所有 \(\{s'+c\mid s'\in\operatorname{sub}(p)\}\) 的并,\(\operatorname{endpos}(\operatorname{t}(p,c))\) 是所有 \(\{j+1\mid j\in\operatorname{endpos}(p)\}\) 的并再并上 \(\{i\}\)\(i\)\(c\) 在串中的位置)。

    性质 \(8\):对于一个状态 \(q\) 和所有能转移到它的 \(p\),所有 \(\operatorname{sub}(p)\) 不交,所有 \(\operatorname{sub}(p)\) 都是后缀包含关系。

    证明:因为转移是不重的,故所有 \(\operatorname{sub}(p)\) 不交。又因为 \(\operatorname{sub}(q)\)\(\operatorname{longest}(q)\) 的连续后缀且是所有 \(\operatorname{sub}(p)\) 的并,故所有 \(\operatorname{sub}(p)\) 都是后缀包含关系。

结论

结论 \(1\):对于任意状态 \(q\),有 \(\forall p_1\to q,p_2\to q,\operatorname{len}(p_1)\ne\operatorname{len}(p_2),\operatorname{minlen}(p_1)\ne\operatorname{minlen}(p_2)\)

证明:若存在 \(p_1=p_2\) 都能转移到 \(q\),那么由性质 \(3\) 可知 \(t_0\leadsto p_1\to q\)\(t_0\leadsto p_2\to q\) 的意义为两个长度相等的不同后缀,矛盾。\(\operatorname{minlen}\) 同理。

结论 \(2\):对于任意状态 \(q\),都存在唯一的 \(\operatorname{maxtrans}(q)\to q\) 使得 \(\operatorname{len}(\operatorname{maxtrans}(q))+1=\operatorname{len}(q)\) 和唯一的 \(\operatorname{mintrans}(q)\to q\) 使得 \(\operatorname{minlen}(\operatorname{mintrans}(q))+1=\operatorname{minlen}(q)\)

证明:因为 \(\operatorname{sub}(q)\) 是所有 \(\operatorname{sub}(p)\) 并集的子集,易证存在性和最大或最小性,由结论 \(1\) 可得唯一性。

结论 \(3\):对于任意状态 \(q\),所有 \(p\to q\)\(\operatorname{link}\) 树上在 \(\operatorname{maxtrans}(q)\to\operatorname{mintrans}(q)\) 这条深度递减的链上。

证明:由性质 \(8\) 可知所有 \(p\)\(\operatorname{link}\) 树上的一条链上。\(\operatorname{maxtrans}(q)\)\(\operatorname{len}\) 最大,所以最深;\(\operatorname{mintrans}\)\(\operatorname{minlen}\) 最小,所以 \(\operatorname{len}\) 最小,所以最浅。

构造

SAM 的构造使用增量法,不断地从 \(s_{[1,i-1]}\) 的 SAM 扩展为 \(s_{[1,i]}\) 的 SAM。

假设现在已知 \(s_{[1,i-1]}\) 的 SAM,考虑加入 \(s_i=c\) 后 SAM 的变化。

  1. \(s_{[1,i-1]}\) 在 SAM 上对应的状态为 \(last\),新建状态 \(cur\),令 \(\operatorname{len}(cur)\gets\operatorname{len}(last)+1\)

    状态 \(cur\) 代表 \(s_{[1,i]}\) 的 SAM 的终止状态。增量产生了一个 \(\operatorname{endpos}\) 的新元素 \(i\),因此至少产生了一个新等价类。\(\operatorname{len(cur)}\) 是显然的,因为 \(\operatorname{longest}(last)=s_{[1,i-1]},\operatorname{longest}(cur)=s_{[1,i]}\)

  2. 令变量 \(p=last\),不断跳 \(\operatorname{link}\) 树上的父亲,如果没有父亲(\(p=t_0\))或状态 \(\operatorname{t}(p,c)\) 已确定则停止。否则状态 \(\operatorname{t}(p,c)\) 还未确定,此时可以确定状态 \(\operatorname{t}(p,c)=cur\)

    由转移的定义可知,这是到状态 \(cur\) 的全部转移。

  3. 如果 \(p=t_0\) 说明 \(c\) 是一个新字符,所以 \(\operatorname{link}(p)=t_0\)。否则说明现在跳到了一个存在的状态 \(\operatorname{t}(p,c)\),显然不能直接令 \(\operatorname{t}(p,c)=cur\),否则会破坏之前构造的正确 SAM,需分类讨论,令 \(q=\operatorname{t}(p,c)\)

    • \(\operatorname{len}(p)+1=\operatorname{len}(q)\),则令 \(\operatorname{link}(cur)\gets q\)

      上面跳 \(\operatorname{link}\) 时至少已经新建了一个到 \(cur\) 的转移,令 \(p'\) 为最晚跳到的新建了到 \(cur\) 的转移的状态,那么 \(\operatorname{mintrans}(cur)=p'\),所以 \(\operatorname{minlen}(cur)=\operatorname{minlen}(p')+1=(\operatorname{len}(p)+1)+1=\operatorname{len}(q)+1\),即 \(\operatorname{len}(q)=\operatorname{minlen}(cur)-1\)。(1)

      因为 \(p\) 是从 \(last\) 开始跳 \(\operatorname{link}\) 得到,所以 \(\operatorname{longest}(p)\)\(s_{[1,i-1]}\) 的后缀,而 \(\operatorname{longest}(q)=\operatorname{longest}(p)+c\),所以 \(\operatorname{longest}(q)\)\(s_{[1,i]}\) 的后缀,即 \(\operatorname{longest}(cur)\) 的后缀。(2)

      由 (1)(2) 可知 \(\operatorname{longest}(q)\)\(\operatorname{shortest}(cur)\) 的最长真后缀,故 \(\operatorname{link}(cur)=q\)

    • \(\operatorname{len}(p)+1<\operatorname{len}(q)\),则新建状态 \(q_1\),令 \(\forall c',\operatorname{t}(q_1,c')\gets\operatorname{t}(q,c'),\operatorname{link}(q_1)\gets\operatorname{link}(q),\operatorname{len}(q_1)\gets\operatorname{len}(p)+1,\operatorname{link}(q)\gets q_1\)

      由上一种情况的 (1)(2) 可知此时 \(\operatorname{sub}(q)\) 只有长度为 \(\operatorname{len}(p)+1\) 的子串及其后缀的才能转移到 \(cur\),将这些子串作为一个新状态 \(q_1\)\(q\) 分裂出去。

      那么显然有 \(\operatorname{len}(q_1)=\operatorname{len}(p)+1,\operatorname{minlen}(q)=\operatorname{len}(q_1)+1\)

      由于 \(p\) 以及 \(p\)\(\operatorname{link}\) 树上的状态 \(p_1\)\(q\) 的转移都是长度 \(\le\operatorname{len}(p)+1\) 的串,所以它们的转移都要移到 \(q_1\) 上。

      至于原 \(q\) 向其他状态的转移则不受影响,复制一遍给 \(q_1\) 即可。

代码

void extend(int c) {
    int p = last, cur = ++idt;
    len[cur] = len[p] + 1, last = cur, sz[cur] = 1;
    while (!t[p][c])
        t[p][c] = cur, p = ft[p];
    if (!p)
        return ft[cur] = 1, void();
    int q = t[p][c];
    if (len[p] + 1 == len[q])
        return ft[cur] = q, void();
    int q1 = ++idt;
    len[q1] = len[p] + 1, ft[q1] = ft[q], ft[q] = ft[cur] = q1;
    while (t[p][c] == q)
        t[p][c] = q1, p = ft[p];
    memcpy(t[q1], t[q], sizeof t[q1]);
}

习题

[SDOI2016] 生成魔咒

[TJOI2019] 甲苯先生和大中锋的字符串

LCS - Longest Common Substring

LONGCS - Longest Common Substring

*[TJOI2015] 弦论

\(f_p\) 表示状态 \(p\) 出发能到达的非空字串数量,\(sz_p=\begin{cases}1&(t=0)\\|\operatorname{endpos}(p)|&(t=1)\end{cases}\),那么当 \(t=0\) 时:

\[f_p=\sum_{p\to q}(f_q+sz_q) \]

考虑求第 \(k\) 大,在 DAG 上遍历,当走到 \(p\) 时:

  1. \(p\) 表示当前第 \(1\sim sz_p\) 大的子串,如果 \(k\le sz_p\) 就停止。

  2. 对于每个 \(p\to q\),它能到达 \(f_q+sz_q\) 个子串,如果 \(k\) 在里面就递归进去,否则减去它再枚举其它 \(q\)

*Cyclical Quest

套路地将询问串复制一遍,循环同构即前缀 \(|x_i|+1\sim 2|x_i|\) 的长为 \(n\) 的后缀。

考虑每次扩展前缀,然后不断删去开头字符使长度 \(\ge n\)(如果删完后 \(l<n\) 就说明没出现过)。这一过程是不断将剩余子串缩为它的真后缀,等价类只会不变或跳 \(\operatorname{link}\)。令前缀 \(i(|x_i|+1\le i\le 2|x_i|)\) 剩余的后缀长度为 \(l\),这段后缀的等价类为 \(p\),有两种情况:

  1. \(l-1=\operatorname{len}(\operatorname{link}(p))\),则等价类变化,令 \(p\gets\operatorname{link}(p)\)

  2. 否则 \(l-1\in[\operatorname{len}(\operatorname{link}(p))+1,\operatorname{len}(p)]\),该子串仍在等价类 \(p\) 里,无需操作。

*[BJOI2020] 封印

求出 \(f_i\) 表示 \(s_{[1,i]}\) 最长的存在于 \(t\) 中的后缀长度。

对于一次询问 \(l,r\),所有 \(i\in[l,r],i-f_i+1\ge l\)\(i\) 贡献为 \(f_i\),其余 \(i\) 的贡献为 \(i-l+1\)

因为 \(i-1\to i\)\(f\) 最多扩展一位,所以 \(i-f_i+1\) 不降,故存在一个划分点 \(pos\),使得 \(i\in[l,pos-1]\) 的贡献都是 \(i-l+1\)\(i\in[pos,r]\) 的贡献都是 \(f_i\),可以二分出 \(pos\) 然后左右分别考虑。

bitset

subings in a String

思路

钦定母串 \(s1_{1\sim n}\)、匹配串 \(s2_{1\sim m}\),现在要求 \(s2\)\(s1_{l,r}\) 中出现了几次。

维护 bitset \(pos\)\(pos_i\in\{0,1\}\) 表示能否以 \(i\) 为起始位置在 \(s1\) 中匹配 \(s2\)

注意到在确定 \(j\) 后,对于每个 \(i%=\)\(c_{s2_j,1\sim n}\) 都存在一个固定偏移量 \(j-1\),可用 bitset 移位解决。

修改复杂度 \(O(1)\),查询复杂度 \(O(\frac{nm}{w})\)

注意,bitset 移位方向与普通位运算相反。

代码

#include <bits/stdc++.h>
using namespace std;
int n, m, q;
char s1[100005], s2[100005];
bitset<100005> pos, c[26];
int query(int l, int r) {
    int t1, t2;
    t1 = (pos >> l).count();
    t2 = (pos >> r + 1).count();
    return t1 - t2;
}
int main() {
    scanf("%s", s1 + 1);
    n = strlen(s1 + 1);
    for (int i = 1; i <= n; i++)
        c[s1[i] - 'a'][i] = 1;
    cin >> q;
    while (q--) {
        int op;
        cin >> op;
        if (op == 1) {
            int x;
            cin >> x;
            scanf("%s", s2 + 1);
            c[s1[x] - 'a'][x] = 0;
            s1[x] = s2[1];
            c[s1[x] - 'a'][x] = 1;
        } else {
            int l, r;
            cin >> l >> r;
            scanf("%s", s2 + 1);
            m = strlen(s2 + 1);
            if (m > r - l + 1) {
                puts("0");
                continue;
            }
            pos = c[s2[1] - 'a'];
            for (int i = 2; i <= m; i++)
                pos &= c[s2[i] - 'a'] >> i - 1;
            cout << query(l, r - m + 1) << '\n';
        }
    }
    return 0;
}
posted @ 2023-01-25 20:50  wangxuzhou  阅读(194)  评论(0)    收藏  举报