后缀自动机
后缀自动机
字符串 \(S\) 的 SAM 是一个接受 \(S\) 的所有后缀的最小 DFA,其每个子串都对应了 SAM 上根开始的一条路径。
考虑 \(S\) 的任意非空子串 \(p\) ,记 \(\mathrm{endpos}(p)\) 为 \(p\) 在 \(S\) 中的所有结束位置,将 \(S\) 的所有非空子串按 \(endpos\) 集合划分为若干个等价类。
可以发现 \(\mathrm{endpos}\) 有以下性质:
- 对于一个子串 \(p\) ,不断去掉其首字母,\(\mathrm{endpos}(p)\) 要么不变,要么增加若干新位置。
- 若 \(\mathrm{endpos}(t) = \mathrm{endpos}(p)\) 且 \(|t| \le |p|\) ,则 \(t\) 为 \(p\) 的后缀。
- 若 \(|t| \le |p|\) ,那么 \(\mathrm{endpos}(p) \subseteq \mathrm{endpos}(t)\) 或 \(\mathrm{endpos}(t) \cap \mathrm{endpos}(p) = \emptyset\) 。
- 一个 \(\mathrm{endpos}\) 等价类里的子串长度连续。
- \(\mathrm{endpos}\) 等价类的个数级别为 \(O(n)\) ,具体上界为 \(2n - 1\) ,因此空间需要开两倍。
可以发现将 \(\mathrm{endpos}\) 按划分集合的关系可以构成树的结构,称之为 Parent Tree。
可以证明基于 Parent Tree 的 SAM 的边数是 \(O(n)\) 级别的,具体的一个上界为 \(3n - 4\) 。
在一个 \(\mathrm{endpos}\) 等价类 \(x\) 中,设最长子串长度为 \(\mathrm{len}(x)\) ,最短子串长度为 \(\mathrm{minlen}(x)\) ,则 \(\mathrm{len}(fa_x) + 1 = \mathrm{minlen}(x)\) ,故只需记录每个类的 \(\mathrm{len}\) 即可。
在线构建
考虑在线添加字符,同时维护每个节点的信息。
令 \(p\) 表示原来 SAM 的最末点(即添加字符前整个串表示的点),此时添加的字符为 \(c\) ,进行如下操作:
-
创建一个新节点 \(np\) 表示当前整个串。并令 \(\mathrm{len}(np) = \mathrm{len}(p) + 1\) 。
加入 \(c\) 之后的整个串所属的类肯定不在原来的 SAM 中,故新建一个节点,并将其 \(\mathrm{len}\) 定义为加入 \(c\) 后整个字符串的长度,即 \(\mathrm{len}(p) + 1\) 。
-
从 \(p\) 开始遍历其父亲,若当前点不存在字符 \(c\) 的边,则添加一个到 \(np\) 的字符 \(c\) 的边;否则将 \(p\) 赋值为这个点,遍历到的最后一个存在字符 \(c\) 的出点记作 \(q\) ,结束遍历。
跳父亲就相当于去掉该类中字符串的若干首字符,可视为从长到短遍历所有后缀。
连边就是将所有后面加 \(c\) 后不是旧串子串的旧串后缀向新串最长后缀所属节点连一条字符 \(c\) 的边。
-
接下来会遇到三种情况:
-
若始终都没有节点存在字符 \(c\) 的边,则将 \(np\) 的父亲设为根。
这说明 \(c\) 在原串中未出现过,其不可能存在除了根节点以外的祖先。
-
否则,若 \(\mathrm{len}(p) + 1 = \mathrm{len}(q)\) ,则将 \(np\) 的父亲设为 \(q\) 。
因为属于 \(p\) 的最长串在末尾加上 \(c\) 为新串的后缀,且 \(\mathrm{len}(q) = \mathrm{len}(p) + 1\) ,所以属于 \(q\) 的最长串即为属于 \(p\) 的最长串在末尾加上 \(c\) ,即属于 \(q\) 的最长串也是新串的后缀。
又因为 \(q\) 中所有字符串均为 \(q\) 的最长串的后缀,所以到达 \(q\) 的字符串均为新串后缀。
由于 \(q\) 是找到的从根开始第一个与 \(np\) 不同且具有后缀关系的节点,所以将 \(np\) 的父亲设为 \(q\) 。
-
否则新建一个点 \(nq\) ,将 \(q\) 的父亲与出边复制到 \(nq\) 上,并将 \(\mathrm{len}(nq)\) 设为 \(\mathrm{len}(p)+1\) ,再将 \(q\) 和 \(np\) 的父亲都设为 \(nq\) 。接着从 \(p\) 开始遍历其父亲,若存在字符 \(c\) 的边,则将其改连向 \(nq\) ,否则跳出循环。
易知 \(\mathrm{len}(q) > \mathrm{len}(p) + 1\) ,说明还有至少一个比 \(\mathrm{len}(p) + 1\) 更长且以 \(c\) 结尾的串属于 \(q\) 。显然这个更长的串不是新串的后缀(否则其去掉 \(c\) 之后的串一定是旧串的后缀,且长度大于 \(\mathrm{len}(p)\) ,应该先被跳到)。
此时可以发现属于 \(q\) 的长度 \(\le \mathrm{len}(p) + 1\) 的串是新串的后缀,但 \(> \mathrm{len}(q) + 1\) 的串却不是。此时到达 \(q\) 的串属于两个等价类,与定义矛盾。
考虑新建一个节点 \(nq\) ,让 \(\mathrm{endpos}\) 多出一个 \(n\) 的串转移到 \(nq\) ,考虑信息的维护:
- \(fa\) :由于 \(q\) 被拆为 \(q\) 和 \(nq\) ,且 \(\mathrm{len}(fa_{nq}) < \mathrm{len}(nq) < \mathrm{len}(q)\) ,因此可以看作 \(nq\) 插入这个父子关系中,所以令 \(fa_{nq} \gets fa_q, fa_q \gets nq\) 。
- \(\mathrm{len}\) :由上文可知,长度 \(> \mathrm{len}(p)+1\) 的字符串都不是新串的后缀,因此 \(\mathrm{len}(nq) \gets \mathrm{len}(p) + 1\) 。
- 出边:由于 \(nq\) 只是从 \(q\) 拆出来的一个点,因此可以直接用 \(q\) 的出边(正确性:因为把 \(nq\) 拆出来只是因为其 \(\mathrm{endpos}\) 不同,而在 \(nq\) 与 \(q\) 后面加上相同的字符,得到的字符串必然属于同一个等价类)。
最后考虑 \(np\) 的父亲,显然只能是 \(q\) 或 \(nq\) (因为它们最先被跳到)。因为 \(q\) 的 \(\mathrm{endpos}\) 没有 \(n\) ,所以肯定转移不到 \(np\) ,所以 \(np\) 的父亲只能是 \(nq\) 。
接着跳 \(p\) 的父亲,若存在字符 \(c\) 的边,则其 \(\mathrm{endpos}\) 必然包含 \(n\) 而无法转移到 \(q\) ,所以改连 \(nq\) 。
-
构建 SAM 的时间复杂度是 \(O(n |\sum|)\) 的,精细实现可以做到 \(O(n)\) ,注意开两倍空间。
inline void extend(int c) {
int p = las, np = las = ++tot;
len[np] = len[p] + 1;
for (; p && !ch[p][c]; p = fa[p])
ch[p][c] = np;
if (!p)
fa[np] = 1;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
fa[np] = q;
else {
int nq = ++tot;
fa[nq] = fa[q], fa[q] = fa[np] = nq, len[nq] = len[p] + 1;
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
}
}
}
如果字符集太大,可以用 map 存储转移边,空间复杂度是 \(O(n)\) 的。
广义 SAM
广义 SAM 用于解决多模式串上的子串相关问题,本质是对多模式串的 Trie 建立 SAM。
定义:
- 后缀:Trie 上一个点 \(x\) 走到子树内一个叶子 \(y\) 的路径,记为 \(S_{x, y}\) 。
- \(\mathrm{endpos}(t) = \{ y | y \in \mathrm{subtree}(x), S_{x, y} = t \}\) 。
一些常见的假做法:
- 用无关字符连接起来:需要保证无关字符互异,字符串数量很多时无法保证字符集为常数,故复杂度无法做到线性。
- 每次将 \(lst\) 赋为 \(1\) :可能会出现当前 \(lst\) 已经有了 \(c\) 的出边,此时会产生空节点(没有转移边转移到它,都转移到原来的出点了),一些信息统计的正确性无法保证。
下面以 P6139 【模板】广义后缀自动机(广义 SAM) 为例给出几种常见的构建方式。
离线构建
先建出 Trie,然后对 Trie 进行 bfs,每次将父亲作为 SAM extend 函数中的 \(lst\) 进行插入即可。
由于 bfs 的性质,不会出现原来 \(lst\) 已经有一个 \(c\) 的出边的情况,自然就不会出现空节点。
时间复杂度为 Trie 的大小,非常好写。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e6 + 7, S = 26;
char str[N];
int n;
namespace Trie {
int ch[N][S];
int tot = 1;
inline void insert(char *str, int n) {
int u = 1;
for (int i = 1; i <= n; ++i) {
int c = str[i] - 'a';
if (!ch[u][c])
ch[u][c] = ++tot;
u = ch[u][c];
}
}
} // namespace Trie
namespace SAM {
int ch[N][S], fa[N], len[N], pos[N];
int tot = 1;
inline int extend(int las, int c) {
int p = las, np = ++tot;
len[np] = len[p] + 1;
for (; p && !ch[p][c]; p = fa[p])
ch[p][c] = np;
if (!p)
fa[np] = 1;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
fa[np] = q;
else {
int nq = ++tot;
fa[nq] = fa[q], fa[q] = fa[np] = nq, len[nq] = len[p] + 1;
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
}
}
return np;
}
inline void build() {
queue<int> q;
pos[1] = 1, q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < S; ++i)
if (Trie::ch[u][i])
pos[Trie::ch[u][i]] = extend(pos[u], i), q.emplace(Trie::ch[u][i]);
}
}
inline ll solve() {
ll ans = 0;
for (int i = 2; i <= tot; ++i)
ans += len[i] - len[fa[i]];
return ans;
}
} // namespace SAM
signed main() {
scanf("%d", &n);
while (n--)
scanf("%s", str + 1), Trie::insert(str, strlen(str + 1));
SAM::build();
printf("%lld\n%d", SAM::solve(), SAM::tot);
return 0;
}
在线构建
考虑在 extend 函数中加一些特判避免“原来 \(lst\) 已经有一个 \(c\) 的出边导致出现空节点”的情况。
如果原来的 \(lst\) 已经有一个 \(c\) 的转移,记为 \(p \to q\) ,进行如下判断:
- 若 \(\mathrm{len}(q) = \mathrm{len}(p) + 1\) ,则说明这个转移是连续的,即当前串已经出现过,直接返回 \(q\) 。
- 否则需要新建状态,把一部分后缀拆出来,此时 \(nq\) 即为当前串对应的状态,返回 \(nq\) ,注意需要销毁 \(np\) 这个节点。
时间复杂度为字符串总长度。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e6 + 7, S = 26;
char str[N];
int n;
namespace SAM {
int ch[N][S], fa[N], len[N];
int tot = 1;
inline int extend(int las, int c) {
int p = las, np = ++tot;
len[np] = len[p] + 1;
for (; p && !ch[p][c]; p = fa[p])
ch[p][c] = np;
if (!p)
return fa[np] = 1, np;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
return p == las ? (--tot, q) : (fa[np] = q, np);
else {
if (p == las)
--tot, np = 0;
int nq = ++tot;
fa[nq] = fa[q], fa[q] = nq, len[nq] = len[p] + 1;
if (np)
fa[np] = nq;
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
return np ? np : nq;
}
}
}
inline ll solve() {
ll ans = 0;
for (int i = 2; i <= tot; ++i)
ans += len[i] - len[fa[i]];
return ans;
}
} // namespace SAM
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf("%s", str + 1);
int len = strlen(str + 1), las = 1;
for (int i = 1; i <= len; ++i)
las = SAM::extend(las, str[i] - 'a');
}
printf("%lld\n%d", SAM::solve(), SAM::tot);
return 0;
}
应用
基本应用
P3804 【模板】后缀自动机 (SAM)
求串 \(S\) 的所有出现次数 \(> 1\) 的子串的出现次数乘上该子串长度的最大值。
\(n \le 10^6\)
建出 SAM 后,可以发现对于一个 \(\mathrm{endpos}\) 的等价类,其中所有串的出现次数均等,因此只要取长度最长的串统计即可。
考虑求出每个 \(\mathrm{endpos}\) 等价类表示的串的出现次数(即 \(\mathrm{endpos}\) 大小),先将每个前缀的点的 \(siz\) 赋为 \(1\) (统计每个 \(\{ i \}\) ),那么一个类的出现次数即为子树求和。
一个简单的实现是将每个等价类按 \(len\) 计数排序,然后倒着贡献即可。
还有一些和子串出现次数有关的题目:
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e6 + 7, S = 26;
char str[N];
int n;
namespace SAM {
int ch[N][S], len[N], fa[N], cnt[N];
int tot = 1, las = 1;
inline void extend(int c) {
int p = las, np = las = ++tot;
len[np] = len[p] + 1, cnt[np] = 1;
for (; p && !ch[p][c]; p = fa[p])
ch[p][c] = np;
if (!p)
fa[np] = 1;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
fa[np] = q;
else {
int nq = ++tot;
fa[nq] = fa[q], fa[q] = fa[np] = nq, len[nq] = len[p] + 1;
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
}
}
}
inline ll solve() {
static int buc[N], id[N];
memset(buc, 0, sizeof(int) * (n + 1));
for (int i = 1; i <= tot; ++i)
++buc[len[i]];
for (int i = 1; i <= n; ++i)
buc[i] += buc[i - 1];
for (int i = tot; i; --i)
id[buc[len[i]]--] = i;
ll ans = 0;
for (int i = tot; i; --i) {
int x = id[i];
if (cnt[x] > 1)
ans = max(ans, 1ll * len[x] * cnt[x]);
cnt[fa[x]] += cnt[x];
}
return ans;
}
} // namespace SAM
signed main() {
scanf("%s", str + 1), n = strlen(str + 1);
for (int i = 1; i <= n; ++i)
SAM::extend(str[i] - 'a');
printf("%lld", SAM::solve());
return 0;
}
P2408 不同子串个数
给出串 \(S\) ,求其不同子串数量。
\(n \le 10^5\)
考虑在 SAM 上 DP,DAG 上一条从源点出发的路径都对应了一个子串。设 \(f_u\) 表示 \(u\) 出发的路径数,则 \(f_i = \sum_{(u, v)} f_v\) 。
更简单的方法是 \(ans = \sum \mathrm{len}(x) - \mathrm{len}(fa_x)\) ,正确性就是 SAM 上表示的子串不重复。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7, S = 26;
char str[N];
int n;
namespace SAM {
int ch[N][S], fa[N], len[N];
int tot = 1, las = 1;
inline void extend(int c) {
int p = las, np = las = ++tot;
len[np] = len[p] + 1;
for (; p && !ch[p][c]; p = fa[p])
ch[p][c] = np;
if (!p)
fa[np] = 1;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
fa[np] = q;
else {
int nq = ++tot;
fa[nq] = fa[q], fa[q] = fa[np] = nq, len[nq] = len[p] + 1;
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
}
}
}
inline ll solve() {
ll ans = 0;
for (int i = 1; i <= tot; ++i)
ans += len[i] - len[fa[i]];
return ans;
}
} // namespace SAM
signed main() {
scanf("%d%s", &n, str + 1);
for (int i = 1; i <= n; ++i)
SAM::extend(str[i] - 'a');
printf("%lld", SAM::solve());
return 0;
}
P4070 [SDOI2016]生成魔咒
按顺序在一个序列的末尾插入数字,每次求出插入后能得到的本质不同的子串个数。
\(n \le 10^5\) ,\(x \le 10^9\)
每次新加入一个数字时整个序列增加了 \(\mathrm{len}(np) - \mathrm{len}(fa_{np})\) 个不同串,在线统计答案输出即可。
由于 \(x \le 10^9\) ,故需要用 map 来存储转移边。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7, S = 26;
char str[N];
ll ans;
int n;
namespace SAM {
map<int, int> ch[N];
int fa[N], len[N];
int tot = 1, las = 1;
inline void extend(int c) {
int p = las, np = las = ++tot;
len[np] = len[p] + 1;
for (; p && ch[p].find(c) == ch[p].end(); p = fa[p])
ch[p][c] = np;
if (!p)
fa[np] = 1;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
fa[np] = q;
else {
int nq = ++tot;
fa[nq] = fa[q], fa[q] = fa[np] = nq, len[nq] = len[p] + 1, ch[nq] = ch[q];
for (; p && ch[p].find(c) != ch[p].end() && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
}
}
ans += len[np] - len[fa[np]];
}
} // namespace SAM
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
int x;
scanf("%d", &x);
SAM::extend(x);
printf("%lld\n", ans);
}
return 0;
}
P3975 [TJOI2015] 弦论
给出串 \(S\) ,求字典序第 \(k\) 小的子串,有两种统计类型:
- \(t = 0\) :不同位置的相同子串算作一个。
- \(t = 1\) :不同位置的相同子串算作多个。
\(n \le 5 \times 10^5\)
记 \(siz_i\) 表示 \(i\) 表示的 \(\mathrm{endpos}\) 集合大小,即 \(i\) 所对应字符串集合的出现次数。
类似上一题,求出 \(f_u\) 表示 \(u\) 出发的路径数,转移时若 \(t = 0\) 则令 \(siz_i = 1\) ,否则令 \(siz_i\) 为子树和。
求解每次贪心从字典序最小的出边枚举即可。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e6 + 7, S = 26;
char str[N];
int n, tp, k;
namespace SAM {
ll f[N];
int ch[N][S], fa[N], len[N], siz[N];
int tot = 1, las = 1;
inline void extend(int c) {
int p = las, np = las = ++tot;
len[np] = len[p] + 1, siz[np] = 1;
for (; p && !ch[p][c]; p = fa[p])
ch[p][c] = np;
if (!p)
fa[np] = 1;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
fa[np] = q;
else {
int nq = ++tot;
fa[nq] = fa[q], fa[q] = fa[np] = nq, len[nq] = len[p] + 1;
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
}
}
}
inline void build() {
static int cnt[N], id[N];
memset(cnt, 0, sizeof(int) * (n + 1));
for (int i = 1; i <= tot; ++i)
++cnt[len[i]];
for (int i = 1; i <= n; ++i)
cnt[i] += cnt[i - 1];
for (int i = tot; i; --i)
id[cnt[len[i]]--] = i;
for (int i = tot; i; --i) {
int x = id[i];
if (!tp)
siz[x] = 1;
f[x] = siz[x];
for (int j = 0; j < S; ++j)
if (ch[x][j])
f[x] += f[ch[x][j]];
siz[fa[x]] += siz[x];
}
f[1] -= siz[1], siz[1] = 0;
}
void dfs(int u, int rk) {
if (rk <= siz[u])
return;
rk -= siz[u];
for (int i = 0; i < S; ++i) {
int v = ch[u][i];
if (!v)
continue;
if (rk > f[v])
rk -= f[v];
else {
putchar('a' + i), dfs(v, rk);
break;
}
}
}
} // namespace SAM
signed main() {
scanf("%s%d%d", str + 1, &tp, &k), n = strlen(str + 1);
for (int i = 1; i <= n; ++i)
SAM::extend(str[i] - 'a');
SAM::build();
if (k > SAM::f[1])
return puts("-1"), 0;
SAM::dfs(1, k);
return 0;
}
与 AC 自动机联系
可以发现 SAM 的 \(fa\) 与 AC 自动机的 \(fail\) 都是在前面去掉若干字符能够跳到的点,因此可以将 SAM 理解为所有子串建出的 AC 自动机。
SP1811 LCS - Longest Common Substring
求两个串 \(S_1, S_2\) 的最长公共子串。
\(n, m \le 2.5 \times 10^5\)
记 \(p_i\) 表示以 \(i\) 结尾的位置的最长匹配长度,即 \(S_1[1, i]\) 的最长后缀满足其在 \(S_2\) 中出现过,答案即为 \(\max p_i\) 。
考虑对 \(S_1\) 建出 SAM,然后当作 AC 自动机使用,暴力跑匹配即可。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 5e5 + 7, S = 26;
char s[N], t[N];
int n, m;
namespace SAM {
int ch[N][S], fa[N], len[N];
int tot = 1, las = 1;
inline void extend(int c) {
int p = las, np = las = ++tot;
len[np] = len[p] + 1;
for (; p && !ch[p][c]; p = fa[p])
ch[p][c] = np;
if (!p)
fa[np] = 1;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
fa[np] = q;
else {
int nq = ++tot;
fa[nq] = fa[q], fa[q] = fa[np] = nq, len[nq] = len[p] + 1;
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
}
}
}
inline int query(char *str, int n) {
int ans = 0;
for (int i = 1, u = 1, plen = 0; i <= n; ++i) {
int c = str[i] - 'a';
while (u != 1 && !ch[u][c])
plen = len[u = fa[u]];
if (ch[u][c])
u = ch[u][c], ++plen;
ans = max(ans, plen);
}
return ans;
}
} // namespace SAM
signed main() {
scanf("%s%s", s + 1, t + 1), n = strlen(s + 1), m = strlen(t + 1);
for (int i = 1; i <= n; ++i)
SAM::extend(s[i] - 'a');
printf("%d", SAM::query(t, m));
return 0;
}
SP1812 LCS2 - Longest Common Substring II
求多个串的 LCS。
串的数量 \(\le 10\) ,长度 \(\le 10^5\)
类似双串 LCS,把第一个串当做基准串,其他的串建立 SAM,最终每个位置的 \(p\) 即为在其他 SAM 上跑到的 \(p\) 取 \(\min\) 后的结果。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7, M = 10, S = 26;
int p[N];
char s[N], t[N];
int n, m, tot;
struct SAM {
int ch[N][S], fa[N], len[N];
int tot = 1, las = 1;
inline void extend(int c) {
int p = las, np = las = ++tot;
len[np] = len[p] + 1;
for (; p && !ch[p][c]; p = fa[p])
ch[p][c] = np;
if (!p)
fa[np] = 1;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
fa[np] = q;
else {
int nq = ++tot;
fa[nq] = fa[q], fa[q] = fa[np] = nq, len[nq] = len[p] + 1;
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
}
}
}
inline void LCS(char *str, int n) {
for (int i = 1, u = 1, plen = 0; i <= n; ++i) {
int c = str[i] - 'a';
while (u != 1 && !ch[u][c])
plen = len[u = fa[u]];
if (ch[u][c])
u = ch[u][c], ++plen;
p[i] = min(p[i], plen);
}
}
} sam[M];
signed main() {
scanf("%s", s + 1), n = strlen(s + 1);
iota(p + 1, p + n + 1, 1);
while (~scanf("%s", t + 1)) {
m = strlen(t + 1), ++tot;
for (int i = 1; i <= m; ++i)
sam[tot].extend(t[i] - 'a');
sam[tot].LCS(s, n);
}
printf("%d", *max_element(p + 1, p + n + 1));
return 0;
}
P6640 [BJOI2020] 封印
给出只包含小写字母 \(a,b\) 的两个字符串 \(s, t\),\(q\) 次询问,每次询问 \(s[l, r]\) 和 \(t\) 的最长公共子串长度。
\(|s|, |t|, q \le 2 \times 10^5\)
先建立 \(T\) 的 SAM,然后让 \(S\) 在上面跑匹配,求出 \(S\) 的每个前缀 \(i\) 能够匹配的后缀长度 \(p_i\) 。
对于区间 \([l, r]\) 的限制,答案即为 \(\max_{i = l}^r \min(p_i, i - l + 1)\) 。内层的 \(\min\) 不好处理,考虑分讨将其去掉。
当 \(p_i \le i - l + 1\) 即 \(i - p_i + 1 \ge l\) 时取到 \(p_i\) ,由于 \(i - p_i + 1\) 单调不降(因为 \(i \to i + 1\) 时 \(p\) 至多 \(+1\) ),那么对于区间 \([l, r]\) ,可以二分出一个 \(x\) 满足 \(i \ge x\) 时 \(i - p_i + 1 \ge l\) ,那么在区间 \([x, r]\) 中取到 \(p_i\) ,在区间 \([l, x - 1]\) 中取到 \(i - l + 1\) 。
不难用 ST 表维护 \(p_i\) 做到 \(O(n \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 4e5 + 7, S = 26, LOGN = 19;
int p[N];
char s[N], t[N];
int n, m, q;
namespace SAM {
int ch[N][S], fa[N], len[N];
int tot = 1, las = 1;
inline void extend(int c) {
int p = las, np = las = ++tot;
len[np] = len[p] + 1;
for (; p && !ch[p][c]; p = fa[p])
ch[p][c] = np;
if (!p)
fa[np] = 1;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
fa[np] = q;
else {
int nq = ++tot;
fa[nq] = fa[q], fa[q] = fa[np] = nq, len[nq] = len[p] + 1;
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
}
}
}
inline void LCS(char *str, int n) {
for (int i = 1, u = 1, plen = 0; i <= n; ++i) {
int c = str[i] - 'a';
while (u != 1 && !ch[u][c])
plen = len[u = fa[u]];
if (ch[u][c])
u = ch[u][c], ++plen;
p[i] = plen;
}
}
} // namespace SAM
namespace ST {
int f[LOGN][N];
inline void prework() {
memcpy(f[0] + 1, p + 1, sizeof(int) * n);
for (int j = 1; j <= __lg(n); ++j)
for (int i = 1; i + (1 << j) - 1 <= n; ++i)
f[j][i] = max(f[j - 1][i], f[j - 1][i + (1 << (j - 1))]);
}
inline int query(int l, int r) {
int k = __lg(r - l + 1);
return max(f[k][l], f[k][r - (1 << k) + 1]);
}
} // namespace ST
signed main() {
scanf("%s%s%d", s + 1, t + 1, &q);
n = strlen(s + 1), m = strlen(t + 1);
for (int i = 1; i <= m; ++i)
SAM::extend(t[i] - 'a');
SAM::LCS(s, n), ST::prework();
while (q--) {
int l, r;
scanf("%d%d", &l, &r);
auto BinarySearch = [](int L, int R) {
int l = L, r = R, ans = r + 1;
while (l <= r) {
int mid = (l + r) >> 1;
if (mid - p[mid] + 1 >= L)
ans = mid, r = mid - 1;
else
l = mid + 1;
}
return ans;
};
int x = BinarySearch(l, r);
printf("%d\n", x == r + 1 ? r - l + 1 : max(x - l, ST::query(x, r)));
}
return 0;
}
CF235C Cyclical Quest
给定一个主串 \(S\) 和 \(n\) 个询问串,求每个询问串的所有本质不同循环同构串在主串中出现的次数总和。
\(|S| \le 10^6\) ,\(n \le 10^5\)
先不考虑本质不同的约束。
循环同构就是把首字符拿去放在最后面,即删掉首字符再在后面再加上一个字符。
如果目前这个串根本没有在文本中完整出现,那么这个删除操作可以忽略。
否则先将匹配长度减一,此时若匹配长度不属于 \((\mathrm{len}(fa_u), \mathrm{len}(u)]\) 里面就跳父亲,加字符的操作和子串匹配是类似的。
最后把匹配到的所有节点 \(\mathrm{endpos}\) 加起来即可。
接下来考虑去重,不难发现所有循环同构串的长度相等,因此对所有匹配的位置打标记最后统计即可达到去重的目的。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e6 + 7, S = 26;
char str[N];
int n, q;
namespace SAM {
int ch[N][S], fa[N], len[N], siz[N], vis[N];
int tot = 1, las = 1, timtag;
inline void extend(int c) {
int p = las, np = las = ++tot;
len[np] = len[p] + 1, siz[np] = 1;
for (; p && !ch[p][c]; p = fa[p])
ch[p][c] = np;
if (!p)
fa[np] = 1;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
fa[np] = q;
else {
int nq = ++tot;
fa[nq] = fa[q], fa[q] = fa[np] = nq, len[nq] = len[p] + 1;
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
}
}
}
inline void build() {
static int cnt[N], id[N];
memset(cnt, 0, sizeof(int) * (n + 1));
for (int i = 1; i <= tot; ++i)
++cnt[len[i]];
for (int i = 1; i <= n; ++i)
cnt[i] += cnt[i - 1];
for (int i = tot; i; --i)
id[cnt[len[i]]--] = i;
for (int i = tot; i > 1; --i)
siz[fa[id[i]]] += siz[id[i]];
}
inline int query(char *str, int n) {
int u = 1, plen = 0;
for (int i = 1; i <= n; ++i) {
int c = str[i] - 'a';
while (u != 1 && !ch[u][c])
plen = len[u = fa[u]];
if (ch[u][c])
u = ch[u][c], ++plen;
}
int ans = 0;
++timtag;
for (int i = 1; i <= n; ++i) {
if (plen == n) {
if (vis[u] != timtag)
vis[u] = timtag, ans += siz[u];
if (--plen == len[fa[u]])
u = fa[u];
}
int c = str[i] - 'a';
while (u != 1 && !ch[u][c])
plen = len[u = fa[u]];
if (ch[u][c])
u = ch[u][c], ++plen;
}
return ans;
}
} // namespace SAM
signed main() {
scanf("%s%d", str + 1, &q), n = strlen(str + 1);
for (int i = 1; i <= n; ++i)
SAM::extend(str[i] - 'a');
SAM::build();
while (q--) {
scanf("%s", str + 1), n = strlen(str + 1);
printf("%d\n", SAM::query(str, n));
}
return 0;
}
P4770 [NOI2018] 你的名字
给定长度为 \(n\) 的字符串 \(S\) ,\(q\) 次询问,每次给出 \(l, r\) 与串 \(T\) ,求满足其不为 \(S[l, r]\) 子串的 \(T\) 的本质不同子串数量。
\(n, q \le 5 \times 10^5\) ,\(\sum |T| \le 10^6\)
询问考虑补集转化,求同时在 \(T\) 和 \(S[l, r]\) 出现的子串数量,\(T\) 的本质不同子串数量不难建 SAM 求出。
先求出 \(p_i\) 表示 \(T[1, i]\) 的最长匹配后缀长度,这个可以直接在 \(S\) 的 SAM 跑即可。
接下来考虑 \(T\) 的 SAM 上的每个状态 \(u\) ,记其任意一个 \(\mathrm{endpos}\) 为 \(x\) ,则该状态的贡献为 \(|\mathbb{Z} \cap (\mathrm{len}(fa_u), \mathrm{len}(u)] \cap (-\infty, p_x]|\) 。
求解 \(p_i\) 有一些细节,由于有区间限制,因此需要判定一个状态 \((u, L)\) (表示 \(u\) 的长度为 \(L\) 的后缀)能否在 \([l, r]\) 内出现。用线段树合并维护 \(\mathrm{endpos}\) ,则只需判断 \([l + L - 1, r]\) 内是否有 \(\mathrm{endpos}\) 即可。注意此时不能直接跳父亲(因为状态不是 \(u\) 而是 \((u, L)\) ),因此只能自减 \(L\) 判定下一个转移态的合法性。
视 \(n, q, |T|\) 同阶,时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e6 + 7, S = 26;
int p[N];
char str[N];
int n, q;
namespace SAM_S {
int ch[N][S], fa[N], len[N];
int tot = 1, las = 1;
namespace SMT {
const int S = 3e7 + 7;
int rt[N], lc[S], rc[S];
int tot;
void insert(int &x, int nl, int nr, int p) {
if (!x)
x = ++tot;
if (nl == nr)
return;
int mid = (nl + nr) >> 1;
if (p <= mid)
insert(lc[x], nl, mid, p);
else
insert(rc[x], mid + 1, nr, p);
}
int merge(int a, int b, int l, int r) {
if (!a || !b)
return a | b;
int x = ++tot;
if (l == r)
return x;
int mid = (l + r) >> 1;
lc[x] = merge(lc[a], lc[b], l, mid), rc[x] = merge(rc[a], rc[b], mid + 1, r);
return x;
}
bool query(int x, int nl, int nr, int l, int r) {
if (l <= nl && nr <= r)
return true;
int mid = (nl + nr) >> 1;
return (l <= mid && lc[x] && query(lc[x], nl, mid, l, r)) || (r > mid && rc[x] && query(rc[x], mid + 1, nr, l, r));
}
} // namespace SMT
inline void extend(int c) {
int p = las, np = las = ++tot;
len[np] = len[p] + 1, SMT::insert(SMT::rt[np], 1, n, len[np]);
for (; p && !ch[p][c]; p = fa[p])
ch[p][c] = np;
if (!p)
fa[np] = 1;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
fa[np] = q;
else {
int nq = ++tot;
fa[nq] = fa[q], fa[q] = fa[np] = nq, len[nq] = len[p] + 1;
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
}
}
}
inline void build() {
static int cnt[N], id[N];
memset(cnt, 0, sizeof(int) * (n + 1));
for (int i = 1; i <= tot; ++i)
++cnt[len[i]];
for (int i = 1; i <= n; ++i)
cnt[i] += cnt[i - 1];
for (int i = tot; i; --i)
id[cnt[len[i]]--] = i;
for (int i = tot; i > 1; --i)
SMT::rt[fa[id[i]]] = SMT::merge(SMT::rt[fa[id[i]]], SMT::rt[id[i]], 1, n);
}
inline bool check(int x, int len, int l, int r) {
return SMT::query(SMT::rt[x], 1, n, l + len - 1, r);
}
inline void query(int l, int r, char *str, int m) {
int u = 1, plen = 0;
for (int i = 1; i <= m; ++i) {
int c = str[i] - 'a';
while (plen && !(ch[u][c] && check(ch[u][c], plen + 1, l, r)))
if (--plen == len[fa[u]])
u = fa[u];
if (ch[u][c] && check(ch[u][c], plen + 1, l, r))
u = ch[u][c], ++plen;
p[i] = plen;
}
}
} // namespace SAM_S
namespace SAM_T {
int ch[N][S], fa[N], len[N], edp[N];
int tot, las;
inline int newnode() {
++tot, fa[tot] = len[tot] = 0;
memset(ch[tot], 0, sizeof(int) * S);
return tot;
}
inline void prework() {
tot = 0, las = newnode();
}
inline void extend(int c) {
int p = las, np = las = newnode();
len[np] = len[p] + 1, edp[np] = len[np];
for (; p && !ch[p][c]; p = fa[p])
ch[p][c] = np;
if (!p)
fa[np] = 1;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
fa[np] = q;
else {
int nq = newnode();
fa[nq] = fa[q], fa[q] = fa[np] = nq, len[nq] = len[p] + 1, edp[nq] = edp[q];
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
}
}
}
inline ll solve(int l, int r, char *str, int m) {
SAM_S::query(l, r, str, m);
ll ans = 0;
for (int i = 2; i <= tot; ++i)
ans += len[i] - len[fa[i]];
for (int i = 2; i <= tot; ++i)
ans -= max(0, min(len[i], p[edp[i]]) - len[fa[i]]);
return ans;
}
} // namespace SAM_T
signed main() {
scanf("%s%d", str + 1, &q), n = strlen(str + 1);
for (int i = 1; i <= n; ++i)
SAM_S::extend(str[i] - 'a');
SAM_S::build();
while (q--) {
int l, r;
scanf("%s%d%d", str + 1, &l, &r);
int m = strlen(str + 1);
SAM_T::prework();
for (int i = 1; i <= m; ++i)
SAM_T::extend(str[i] - 'a');
printf("%lld\n", SAM_T::solve(l, r, str, m));
}
return 0;
}
CF427D Match & Catch
给出两个字符串 \(S, T\) ,求最短的子串满足其在 \(S\) 和 \(T\) 中恰好出现一次。
\(|S|, |T| \le 5000\)
对 \(S, T\) 分别建出 SAM,然后让 \(T\) 在 \(S\) 的 SAM 上跑匹配。记 \(T[1, i]\) 在 \(T\) 的 SAM 上状态为 \(v\) ,在 \(S\) 的 SAM 上匹配到的状态为 \(u\) ,匹配长度为 \(l\) 。
首先 \(u, v\) 必须是叶子节点(否则说明出现不止一次),然后必须有 \(l > \max(\mathrm{len}(fa_u), \mathrm{len}(fa_v))\) (匹配的长度必须属于 \(u, v\) ),此时可以对答案产生 \(\max(\mathrm{len}(fa_u), \mathrm{len}(fa_v)) + 1\) 的贡献。
时间复杂度 \(O(|S| + |T|)\) 。
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e4 + 7;
int pre[N];
char s[N], t[N];
int n, m;
struct SAM {
int ch[N][26], fa[N], len[N];
bool tag[N];
int tot = 1, las = 1;
inline void extend(int c) {
int p = las, np = las = ++tot;
len[np] = len[p] + 1;
for (; p && !ch[p][c]; p = fa[p])
ch[p][c] = np;
if (!p)
fa[np] = 1;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
fa[np] = q;
else {
int nq = ++tot;
fa[nq] = fa[q], fa[q] = fa[np] = nq, len[nq] = len[p] + 1;
memcpy(ch[nq], ch[q], sizeof(int) * 26);
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
}
}
}
inline void solve() {
for (int i = 2; i <= tot; ++i)
tag[fa[i]] = true;
}
} S, T;
signed main() {
scanf("%s%s", s + 1, t + 1);
n = strlen(s + 1), m = strlen(t + 1);
for (int i = 1; i <= n; ++i)
S.extend(s[i] - 'a');
for (int i = 1; i <= m; ++i)
T.extend(t[i] - 'a'), pre[i] = T.las;
S.solve(), T.solve();
int ans = inf;
for (int i = 1, u = 1, plen = 0; i <= m; ++i) {
int c = t[i] - 'a', v = pre[i];
while (u > 1 && !S.ch[u][c])
plen = S.len[u = S.fa[u]];
if (S.ch[u][c])
u = S.ch[u][c], ++plen;
if (!S.tag[u] && !T.tag[v] && plen > max(S.len[S.fa[u]], T.len[T.fa[v]]))
ans = min(ans, max(S.len[S.fa[u]] + 1, T.len[T.fa[v]] + 1));
}
printf("%d", ans == inf ? -1 : ans);
return 0;
}
维护 endpos 集合
CF1037H Security
给出长度为 \(n\) 的字符串 \(S\) ,\(q\) 次询问子串 \(S[l, r]\) 中字典序最小的满足其字典序严格大于 \(T\) 的子串,若无解输出 \(-1\) 。
\(n \le 10^5\) ,\(q \le 2 \times 10^5\) ,\(\sum |T| \le 2 \times 10^5\)
考虑贪心,先求出 \(T\) 在 \(S[l, r]\) 中的最大匹配串,若当前状态存在一个字典序大于下一位字符的转移,则选其作为答案一定最优,否则不断跳父亲判定即可。
接下来考虑求出 \(T\) 在 \(S[l, r]\) 中的最大匹配串,可以暴力匹配,然后判定 \(\mathrm{endpos}\) 是否存在 \([l + p - 1, r]\) 内的元素,其中 \(p\) 为判定前的匹配长度。
考虑线段树合并维护 \(\mathrm{endpos}\) 集合,那么一次判定就是区间查询。
视 \(|S|\) 与 \(|T|\) 同阶,时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 7, S = 26;
char str[N], t[N];
int n, q;
namespace SMT {
const int S = 3e7 + 7;
int lc[S], rc[S];
int rt[N];
int tot;
void insert(int &x, int nl, int nr, int p) {
if (!x)
x = ++tot;
if (nl == nr)
return;
int mid = (nl + nr) >> 1;
if (p <= mid)
insert(lc[x], nl, mid, p);
else
insert(rc[x], mid + 1, nr, p);
}
int merge(int a, int b, int l, int r) {
if (!a || !b)
return a | b;
int x = ++tot;
if (l == r)
return x;
int mid = (l + r) >> 1;
lc[x] = merge(lc[a], lc[b], l, mid), rc[x] = merge(rc[a], rc[b], mid + 1, r);
return x;
}
int query(int x, int nl, int nr, int l, int r) {
if (!x)
return -1;
if (nl == nr)
return nl;
int mid = (nl + nr) >> 1;
if (r <= mid)
return query(lc[x], nl, mid, l, r);
else if (l > mid)
return query(rc[x], mid + 1, nr, l, r);
else {
int res = query(rc[x], mid + 1, nr, l, r);
return ~res ? res : query(lc[x], nl, mid, l, r);
}
}
} // namespace SMT
namespace SAM {
int ch[N][S], fa[N], len[N];
int tot = 1, las = 1;
inline void extend(int c) {
int p = las, np = las = ++tot;
len[np] = len[p] + 1, SMT::insert(SMT::rt[np], 1, n, len[np]);
for (; p && !ch[p][c]; p = fa[p])
ch[p][c] = np;
if (!p)
fa[np] = 1;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
fa[np] = q;
else {
int nq = ++tot;
fa[nq] = fa[q], fa[q] = fa[np] = nq, len[nq] = len[p] + 1;
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
}
}
}
inline void build() {
static int cnt[N], id[N];
memset(cnt, 0, sizeof(int) * (n + 1));
for (int i = 1; i <= tot; ++i)
++cnt[len[i]];
for (int i = 1; i <= n; ++i)
cnt[i] += cnt[i - 1];
for (int i = tot; i; --i)
id[cnt[len[i]]--] = i;
for (int i = tot; i > 1; --i)
SMT::rt[fa[id[i]]] = SMT::merge(SMT::rt[fa[id[i]]], SMT::rt[id[i]], 1, n);
len[0] = -1;
}
inline string query(char *t, int m, int l, int r) {
pair<int, int> ans = make_pair(-1, -1);
for (int u = 1, plen = 0;; u = ch[u][t[++plen] - 'a']) {
for (int i = S - 1; i >= (plen == m ? 0 : t[plen + 1] - 'a' + 1); --i)
if (ch[u][i]) {
int p = SMT::query(SMT::rt[ch[u][i]], 1, n, l, r);
if (~p && p - plen >= l)
ans = make_pair(p - plen, p);
}
if (plen == m || !ch[u][t[plen + 1] - 'a'])
break;
}
return ans == make_pair(-1, -1) ? "-1" : string(str + ans.first, str + ans.second + 1);
}
} // namespace SAM
signed main() {
scanf("%s%d", str + 1, &q), n = strlen(str + 1);
for (int i = 1; i <= n; ++i)
SAM::extend(str[i] - 'a');
SAM::build();
while (q--) {
int l, r, m;
scanf("%d%d%s", &l, &r, t + 1), m = strlen(t + 1);
puts(SAM::query(t, m, l, r).c_str());
}
return 0;
}
P4094 [HEOI2016/TJOI2016] 字符串
给定长度为 \(n\) 的字符串 \(S\) ,\(m\) 次询问 \(S[a, b]\) 的所有子串与 \(S[c, d]\) 的最长公共前缀的最大值。
\(n, m \le 10^5\)
先反转字符串,这样就可以建立后缀数据结构了。
考虑二分答案 \(len\) ,问题转化为 \(S[d - len + 1, d]\) 是否在 \(S[a, b]\) 中出现过。用线段树合并维护 \(\mathrm{endpos}\) ,那么只要判定 \(S[d - len + 1, d]\) 对应的状态是否存在一个 \(\mathrm{endpos}\) 属于 \([a + len - 1, b]\) 即可。
考虑如何定位一个子串的状态,注意到 SAM 跳 \(fa\) 的操作相当于在开头删去若干字符,于是只要找到 \(S[1, d]\) 的状态然后树上倍增判定长度即可。
时间复杂度 \(O(n \log^2 n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 7, S = 26, LOGN = 19;
int id[N];
char str[N];
int n, m;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
namespace SMT {
const int S = 3e7 + 7;
int lc[S], rc[S], rt[N];
int tot;
void insert(int &x, int nl, int nr, int p) {
if (!x)
x = ++tot;
if (nl == nr)
return;
int mid = (nl + nr) >> 1;
if (p <= mid)
insert(lc[x], nl, mid, p);
else
insert(rc[x], mid + 1, nr, p);
}
int merge(int a, int b, int l, int r) {
if (!a || !b)
return a | b;
int x = ++tot;
if (l == r)
return x;
int mid = (l + r) >> 1;
lc[x] = merge(lc[a], lc[b], l, mid), rc[x] = merge(rc[a], rc[b], mid + 1, r);
return x;
}
bool query(int x, int nl, int nr, int l, int r) {
if (!x)
return false;
if (l <= nl && nr <= r)
return true;
int mid = (nl + nr) >> 1;
if (r <= mid)
return query(lc[x], nl, mid, l, r);
else if (l > mid)
return query(rc[x], mid + 1, nr, l, r);
else
return query(lc[x], nl, mid, l, r) || query(rc[x], mid + 1, nr, l, r);
}
} // namespace SMT
namespace SAM {
int ch[N][S], fa[N][LOGN], len[N];
int tot = 1, las = 1;
inline void extend(int c) {
int p = las, np = las = ++tot;
len[np] = len[p] + 1, SMT::insert(SMT::rt[np], 1, n, len[np]);
for (; p && !ch[p][c]; p = fa[p][0])
ch[p][c] = np;
if (!p)
fa[np][0] = 1;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
fa[np][0] = q;
else {
int nq = ++tot;
fa[nq][0] = fa[q][0], fa[q][0] = fa[np][0] = nq, len[nq] = len[p] + 1;
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p][0])
ch[p][c] = nq;
}
}
}
inline void build() {
static int cnt[N], id[N];
memset(cnt, 0, sizeof(int) * (n + 1));
for (int i = 1; i <= tot; ++i)
++cnt[len[i]];
for (int i = 1; i <= n; ++i)
cnt[i] += cnt[i - 1];
for (int i = tot; i > 1; --i)
id[cnt[len[i]]--] = i;
for (int i = tot; i > 1; --i)
SMT::rt[fa[id[i]][0]] = SMT::merge(SMT::rt[fa[id[i]][0]], SMT::rt[id[i]], 1, n);
for (int j = 1; j < LOGN; ++j)
for (int i = 1; i <= tot; ++i)
fa[i][j] = fa[fa[i][j - 1]][j - 1];
}
inline int jump(int x, int k) {
for (int i = LOGN - 1; ~i; --i)
if (fa[x][i] && len[fa[x][i]] >= k)
x = fa[x][i];
return x;
}
} // namespace SAM
signed main() {
n = read(), m = read(), scanf("%s", str + 1);
reverse(str + 1, str + n + 1);
for (int i = 1; i <= n; ++i)
SAM::extend(str[i] - 'a'), id[i] = SAM::las;
SAM::build();
while (m--) {
int b = n - read() + 1, a = n - read() + 1, d = n - read() + 1, c = n - read() + 1,
l = 1, r = min(b - a + 1, d - c + 1), ans = 0;
while (l <= r) {
int mid = (l + r) >> 1;
if (SMT::query(SMT::rt[SAM::jump(id[d], mid)], 1, n, a + mid - 1, b))
ans = mid, l = mid + 1;
else
r = mid - 1;
}
printf("%d\n", ans);
}
return 0;
}
SP687 REPEATS - Repeats
给出一个长度为 \(n\) 的字符串 \(S\) ,求最大的 \(k\) 使得 \(S\) 有子串满足 \(k\) 是其一个循环节。
\(n \le 5 \times 10^4\)
循环节很容易想到 border,考虑枚举 border 在 SAM 上的状态。若 \(\mathrm{endpos}\) 中两两元素最小差值为 \(g\) ,则对答案的贡献为 \(\lfloor \frac{len + g}{g} \rfloor\) ,注意判断两个 border 中间不能有东西。
用启发式合并可以做到 \(O(n \log^2 n)\) ,比较好写,如果上线段树合并可以做到 \(O(n \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7, S = 26;
int n;
namespace SAM {
set<int> endpos[N];
int ch[N][S], fa[N], len[N];
int tot, las;
inline int newnode() {
++tot, fa[tot] = len[tot] = 0, endpos[tot].clear();
memset(ch[tot], 0, sizeof(int) * S);
return tot;
}
inline void prework() {
tot = 0, las = newnode();
}
inline void extend(int c) {
int p = las, np = las = newnode();
len[np] = len[p] + 1, endpos[np].emplace(len[np]);
for (; p && !ch[p][c]; p = fa[p])
ch[p][c] = np;
if (!p)
fa[np] = 1;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
fa[np] = q;
else {
int nq = newnode();
fa[nq] = fa[q], fa[q] = fa[np] = nq, len[nq] = len[p] + 1;
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
}
}
}
inline int solve() {
static int cnt[N], id[N];
memset(cnt, 0, sizeof(int) * (n + 1));
for (int i = 1; i <= tot; ++i)
++cnt[len[i]];
for (int i = 1; i <= n; ++i)
cnt[i] += cnt[i - 1];
for (int i = tot; i; --i)
id[cnt[len[i]]--] = i;
int ans = 1;
for (int i = tot; i > 1; --i) {
int x = id[i], f = fa[x];
if (endpos[x].size() > endpos[f].size())
swap(endpos[x], endpos[f]);
for (int it : endpos[x]) {
auto now = endpos[f].lower_bound(it);
if (now != endpos[f].begin() && it - *prev(now) <= len[f])
ans = max(ans, len[f] / (it - *prev(now)) + 1);
if (now != endpos[f].end() && *now - it <= len[f])
ans = max(ans, len[f] / (*now - it) + 1);
endpos[f].emplace(it);
}
}
return ans;
}
} // namespace SAM
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while (T--) {
cin >> n;
SAM::prework();
for (int i = 1; i <= n; ++i) {
char c;
cin >> c;
SAM::extend(c - 'a');
}
printf("%d\n", SAM::solve());
}
return 0;
}
CF1276F Asterisk Substrings
给定长度为 \(n\) 的小写字符串 \(S\) ,定义 \(T_i = S[1, i - 1] + \text{*} + S[i + 1, n]\) ,求 \(\{ S, T_{1 \sim n} \}\) 的本质不同子串数量(包括空串)。
\(n \le 10^5\)
可以发现合法子串只会形如 \(\{ \empty, \text{*}, s, s \text{*}, \text{*} s, s \text{*} t \}\) 。
前两者直接在答案上加上 \(2\) 即可,第三种即为 \(s\) 的本质不同子串数,四五两种即为本质不同子串数减去无法在前/后加 \(\text{*}\) 的子串数即可。
考虑最后一种情况,可以发现对于 \(\mathrm{endpos}\) 相同的 \(s\) ,可选的 \(t\) 构成的集合都是一样的,记 \(\mathrm{endpos}(s) = p_{1 \sim k}\) ,则可选的 \(t\) 即为 \(\{ S[p_i + 2 :] \}\) ,于是只要求出这样的串的并集大小。
对反串建立 SAM,则可选的数量即为若干链的并,线段树合并维护即可。
时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7, S = 26, LOGN = 19;
int pre[N], suf[N];
char str[N];
int n;
struct SAM {
int ch[N][S], fa[N], len[N];
int tot = 1, las = 1;
inline int extend(int c) {
int p = las, np = las = ++tot;
len[np] = len[p] + 1;
for (; p && !ch[p][c]; p = fa[p])
ch[p][c] = np;
if (!p)
fa[np] = 1;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
fa[np] = q;
else {
int nq = ++tot;
fa[nq] = fa[q], fa[q] = fa[np] = nq, len[nq] = len[p] + 1;
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
}
}
return np;
}
} A, B;
namespace Tree {
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int fa[N][LOGN], dep[N], dfn[N], id[N];
int dfstime;
void dfs(int u, int f) {
fa[u][0] = f, dep[u] = dep[f] + 1, id[dfn[u] = ++dfstime] = u;
for (int i = 1; i < LOGN; ++i)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (int v : G.e[u])
dfs(v, u);
}
inline int LCA(int x, int y) {
if (dep[x] < dep[y])
swap(x, y);
for (int i = 0, h = dep[x] - dep[y]; h; ++i, h >>= 1)
if (h & 1)
x = fa[x][i];
if (x == y)
return x;
for (int i = LOGN - 1; ~i; --i)
if (fa[x][i] != fa[y][i])
x = fa[x][i], y = fa[y][i];
return fa[x][0];
}
} // namespace Tree
namespace SMT {
const int S = 1e7 + 7;
struct Node {
ll s;
int l, r;
} nd[S];
int rt[N], lc[S], rc[S];
int tot;
inline void pushup(int x) {
if (!lc[x])
nd[x] = nd[rc[x]];
else if (!rc[x])
nd[x] = nd[lc[x]];
else
nd[x] = (Node){nd[lc[x]].s + nd[rc[x]].s - B.len[Tree::LCA(Tree::id[nd[lc[x]].r], Tree::id[nd[rc[x]].l])],
nd[lc[x]].l, nd[rc[x]].r};
}
void insert(int &x, int nl, int nr, int p) {
if (!x)
x = ++tot;
if (nl == nr) {
nd[x] = (Node){B.len[Tree::id[p]], p, p};
return;
}
int mid = (nl + nr) >> 1;
if (p <= mid)
insert(lc[x], nl, mid, p);
else
insert(rc[x], mid + 1, nr, p);
pushup(x);
}
int merge(int a, int b) {
if (!a || !b)
return a | b;
lc[a] = merge(lc[a], lc[b]), rc[a] = merge(rc[a], rc[b]);
return pushup(a), a;
}
} // namespace SMT
signed main() {
scanf("%s", str + 1), n = strlen(str + 1);
for (int i = 1; i <= n; ++i)
pre[i] = A.extend(str[i] - 'a');
for (int i = n; i; --i)
suf[i] = B.extend(str[i] - 'a');
ll ans = 2; // case 1 ("") and case 2 ("*")
for (int i = 2; i <= A.tot; ++i) // case 3 : s
ans += A.len[i] - A.len[A.fa[i]];
for (int i = 2; i <= A.tot; ++i) // case 4 : s*
if (A.len[i] != n)
ans += A.len[i] - A.len[A.fa[i]];
for (int i = 2; i <= B.tot; ++i) // case 5 : *s
if (B.len[i] != n)
ans += B.len[i] - B.len[B.fa[i]];
for (int i = 2; i <= B.tot; ++i)
Tree::G.insert(B.fa[i], i);
Tree::dfs(1, 0);
for (int i = 1; i <= n - 2; ++i)
SMT::insert(SMT::rt[pre[i]], 1, B.tot, Tree::dfn[suf[i + 2]]);
static int cnt[N], id[N];
memset(cnt, 0, sizeof(int) * (n + 1));
for (int i = 1; i <= A.tot; ++i)
++cnt[A.len[i]];
for (int i = 1; i <= n; ++i)
cnt[i] += cnt[i - 1];
for (int i = A.tot; i; --i)
id[cnt[A.len[i]]--] = i;
for (int i = A.tot; i > 1; --i) {
int x = id[i];
ans += 1ll * (A.len[x] - A.len[A.fa[x]]) * SMT::nd[SMT::rt[x]].s; // case 6 : s*t
SMT::rt[A.fa[x]] = SMT::merge(SMT::rt[A.fa[x]], SMT::rt[x]);
}
printf("%lld", ans);
return 0;
}
Parent Tree 上的信息统计
P5161 WD与数列
给定序列 \(a_{1 \sim n}\) ,求有多少对不相交的匹配子串,两个子串 \(A, B\) 匹配当且仅当:
- \(|A| = |B|\) 。
- \(\forall 1 \le i, j \le |A|, A_i - B_i = A_j - B_j\) 。
\(n \le 3 \times 10^5\)
先判掉子串长度为 \(1\) 的情况,则判定条件转化为差分数组相等。
考虑两个 LCS 为 \(t\) 的两个前缀 \(S[1, i], S[1, j]\) ,那么合法的左端点取值为 \(\min(|i - j| - 1, t)\) 。
考虑建出 SAM,那么两个状态的 LCA 即为它们的 LCS。
考虑在 LCA 处统计贡献,可以发现统计两个集合之间的贡献是困难的,但是统计单点和集合之间的贡献形如分段一次函数,是比较好统计的。不难想到 dsu-on-tree 配合 BIT,即可做到时间复杂度 \(O(n \log^2 n)\) ,常数很小。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 6e5 + 7, S = 26;
int a[N];
ll ans;
int n;
struct BIT {
ll c[N];
inline void update(int x, int k) {
for (; x <= n; x += x & -x)
c[x] += k;
}
inline ll ask(int x) {
ll res = 0;
for (; x; x -= x & -x)
res += c[x];
return res;
}
inline ll query(int l, int r) {
return l <= r ? ask(r) - ask(l - 1) : 0;
}
} bit1, bit2;
namespace SAM {
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
map<int, int> ch[N];
int fa[N], len[N], siz[N], son[N];
bool ed[N];
int tot = 1, las = 1;
inline void extend(int c) {
int p = las, np = las = ++tot;
len[np] = len[p] + 1, ed[np] = true;
for (; p && ch[p].find(c) == ch[p].end(); p = fa[p])
ch[p][c] = np;
if (!p)
fa[np] = 1;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
fa[np] = q;
else {
int nq = ++tot;
fa[nq] = fa[q], fa[q] = fa[np] = nq, len[nq] = len[p] + 1, ch[nq] = ch[q];
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
}
}
}
inline void build() {
for (int i = 2; i <= tot; ++i)
G.insert(fa[i], i);
}
set<int> st[N];
void update(int u, int k) {
if (ed[u])
bit1.update(len[u], k), bit2.update(len[u], len[u] * k);
for (int v : G.e[u])
update(v, k);
}
void calc(int u, int lim) {
if (ed[u]) {
int l = max(1, len[u] - 1 - lim), r = min(n, lim + len[u] + 1);
ans += (bit1.query(1, l - 1) + bit1.query(r + 1, n)) * lim;
ans += bit1.query(l, len[u] - 1) * (len[u] - 1) - bit2.query(l, len[u] - 1);
ans += bit2.query(len[u] + 1, r) - bit1.query(len[u] + 1, r) * (len[u] + 1);
}
for (int v : G.e[u])
calc(v, lim);
}
void dfs1(int u) {
siz[u] = ed[u];
for (int v : G.e[u]) {
dfs1(v), siz[u] += siz[v];
if (siz[v] > siz[son[u]])
son[u] = v;
}
}
void dfs2(int u, bool remain) {
for (int v : G.e[u])
if (v != son[u])
dfs2(v, false);
if (son[u])
dfs2(son[u], true);
if (ed[u]) {
ans += bit1.query(1, len[u] - 1) * (len[u] - 1) - bit2.query(1, len[u] - 1);
int r = min(len[u] * 2 + 1, n);
ans += bit2.query(len[u] + 1, r) - bit1.query(len[u] + 1, r) * (len[u] + 1);
ans += bit1.query(r + 1, n) * len[u];
bit1.update(len[u], 1), bit2.update(len[u], len[u]);
}
for (int v : G.e[u])
if (v != son[u])
calc(v, len[u]), update(v, 1);
if (!remain)
update(u, -1);
}
} // namespace SAM
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
ans = 1ll * n * (n - 1) / 2;
for (int i = 2; i <= n; ++i)
SAM::extend(a[i] - a[i - 1]);
SAM::build(), SAM::dfs1(1), SAM::dfs2(1, false);
printf("%lld", ans);
return 0;
}
P4482 [BJWC2018] Border 的四种求法
给定长度为 \(n\) 的字符串 \(S\) ,\(q\) 次询问一个子串的最长 border。
\(n, q \le 2 \times 10^5\)
不难发现 \(S[l, r]\) 的 border 即为最大的 \(p \in [l, r - 1]\) 满足 \(\mathrm{LCS}(S[1, p], S[1, r]) \ge p - l + 1\) ,此时 \(p - l + 1\) 即为 border 长度。
建出 SAM 结构,一个暴力是枚举 \(S[1, r]\) 的所有祖先 \(u\) ,则最优的 \(p\) 为 \([l, \min(r - 1, l + \mathrm{len}(u) - 1)]\) 中最大的 \(\mathrm{endpos}\) ,时间复杂度上界是平方级别的。
考虑轻重链剖分,将一个链查询转化为 \(O(\log n)\) 个单点对重链前缀的查询。于是可以离线,将每个询问挂在 \(O(\log n)\) 个重链前缀上。
接下来对于一条重链,从上到下枚举重链前缀,并同时加入轻子树的 \(\mathrm{endpos}\) 。由于轻子树的大小总和是 \(O(n \log n)\) 级别的,因此无需线段树合并,直接暴力遍历即可。注意此时有可能出现枚举的 LCA 比实际 LCA 浅的情况(加入了 \(S[1, p]\) 作为轻子树时子树内的点),但是这样显然不优,无需特殊处理。
记 \(t_p = p - \mathrm{len}(u)\) ,其中 \(u\) 为 \(S[1, p]\) 所属的重链祖先,即枚举到点 \(u\) 时 \(S[1, p]\) 为在 \(u\) 轻子树内。则一个 \(p\) 的限制条件转化为 \(p \in [l, r - 1] \and t_p < l\) ,那么区间维护 \(t_p\) 的最小值,然后做线段树二分即可。
但是这样会漏掉前缀末尾点的重子树的贡献,由于这类贡献为 \(O(\log n)\) 个单点,因为 set 启发式合并维护 \(\mathrm{endpos}\) 集合再查询即可。
时间复杂度 \(O(n \log^2 n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 4e5 + 7, S = 26;
int pre[N], ans[N];
char str[N];
int n, q;
namespace SMT {
int mn[N << 2];
inline int ls(int x) {
return x << 1;
}
inline int rs(int x) {
return x << 1 | 1;
}
void build(int x, int l, int r) {
mn[x] = inf;
if (l == r)
return;
int mid = (l + r) >> 1;
build(ls(x), l, mid), build(rs(x), mid + 1, r);
}
void update(int x, int nl, int nr, int p, int k) {
if (nl == nr) {
mn[x] = k;
return;
}
int mid = (nl + nr) >> 1;
if (p <= mid)
update(ls(x), nl, mid, p, k);
else
update(rs(x), mid + 1, nr, p, k);
mn[x] = min(mn[ls(x)], mn[rs(x)]);
}
int query(int x, int nl, int nr, int l, int r, int k) {
if (nl == nr)
return nl;
int mid = (nl + nr) >> 1;
if (r > mid && mn[rs(x)] < k) {
int res = query(rs(x), mid + 1, nr, l, r, k);
if (~res)
return res;
}
return l <= mid && mn[ls(x)] < k ? query(ls(x), nl, mid, l, r, k) : -1;
}
} // namespace SMT
namespace SAM {
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
vector<tuple<int, int, int> > qry[N];
set<int> endpos[N];
int ch[N][S], fa[N], len[N];
int dep[N], siz[N], son[N], top[N];
int tot = 1, las = 1;
inline void extend(int c) {
int p = las, np = las = ++tot;
len[np] = len[p] + 1, endpos[np].emplace(len[np]);
for (; p && !ch[p][c]; p = fa[p])
ch[p][c] = np;
if (!p)
fa[np] = 1;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
fa[np] = q;
else {
int nq = ++tot;
fa[nq] = fa[q], fa[q] = fa[np] = nq, len[nq] = len[p] + 1;
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
}
}
}
void dfs1(int u) {
dep[u] = dep[fa[u]] + 1, siz[u] = 1;
for (int v : G.e[u]) {
dfs1(v), siz[u] += siz[v];
if (siz[v] > siz[son[u]])
son[u] = v;
}
}
void dfs2(int u, int topf) {
top[u] = topf;
if (son[u])
dfs2(son[u], topf);
for (int v : G.e[u])
if (v != son[u])
dfs2(v, v);
}
inline void build() {
for (int i = 2; i <= tot; ++i)
G.insert(fa[i], i);
dfs1(1), dfs2(1, 1);
}
inline void insert(int x, int l, int r, int id) {
for (; x; x = fa[top[x]])
qry[x].emplace_back(l, r, id);
}
void update(int u, int r, int k) {
if (!endpos[u].empty())
SMT::update(1, 1, n, len[u], k == 1 ? len[u] - len[r] : inf);
for (int v : G.e[u])
update(v, r, k);
}
void dfs3(int u) {
for (int v : G.e[u]) {
dfs3(v);
if (endpos[v].size() > endpos[u].size())
swap(endpos[u], endpos[v]);
for (int it : endpos[v])
endpos[u].emplace(it);
}
for (auto it : qry[u]) {
int l = get<0>(it), r = get<1>(it), id = get<2>(it);
auto x = endpos[u].upper_bound(min(r - 1, l + len[u] - 1));
if (x != endpos[u].begin())
ans[id] = max(ans[id], *prev(x) - l + 1);
}
}
inline void solve() {
SMT::build(1, 1, n);
for (int i = 1; i <= tot; ++i) {
if (i == son[fa[i]])
continue;
for (int u = i; u; u = son[u]) {
if (!endpos[u].empty())
SMT::update(1, 1, n, len[u], 0);
for (int v : G.e[u])
if (v != son[u])
update(v, u, 1);
for (auto it : qry[u]) {
int l = get<0>(it), r = get<1>(it), id = get<2>(it);
ans[id] = max(ans[id], SMT::query(1, 1, n, l, r - 1, l) - l + 1);
}
}
for (int u = i; u; u = son[u]) {
if (!endpos[u].empty())
SMT::update(1, 1, n, len[u], inf);
for (int v : G.e[u])
if (v != son[u])
update(v, u, -1);
}
}
dfs3(1);
}
} // namespace SAM
signed main() {
scanf("%s%d", str + 1, &q), n = strlen(str + 1);
for (int i = 1; i <= n; ++i)
SAM::extend(str[i] - 'a'), pre[i] = SAM::las;
SAM::build();
for (int i = 1; i <= q; ++i) {
int l, r;
scanf("%d%d", &l, &r);
if (l < r)
SAM::insert(pre[r], l, r, i);
}
SAM::solve();
for (int i = 1; i <= q; ++i)
printf("%d\n", ans[i]);
return 0;
}
LCT 动态维护 Parent Tree
P5212 SubString
给定一个字符串 \(S\) ,\(m\) 次操作,操作有:
- 在当前串后插入一个字符串。
- 询问某个串 \(T\) 的出现次数。
\(|S|, m \le 6 \times 10^5\) ,\(\sum |T| \le 3 \times 10^6\) ,强制在线
查询出现次数,即 \(|\mathrm{endpos}|\) ,只要做一遍子树求和即可。
由于有末端插入字符的操作,这意味着 Parent Tree 是动态的,用 LCT 维护即可。
时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1.2e6 + 7, S = 2;
char str[N];
int n, m;
namespace Decode {
inline void decodeWithMask(int mask) {
scanf("%s", str);
int n = strlen(str);
for (int j = 0; j < n; j++) {
mask = (mask * 131 + j) % n;
swap(str[mask], str[j]);
}
}
int mask;
} // namespace Decode
namespace LCT {
int ch[N][2], fa[N], s[N], tag[N], sta[N];
inline bool isroot(int x) {
return x != ch[fa[x]][0] && x != ch[fa[x]][1];
}
inline int dir(int x) {
return x == ch[fa[x]][1];
}
inline void spread(int x, int k) {
s[x] += k, tag[x] += k;
}
inline void pushdown(int x) {
if (tag[x]) {
if (ch[x][0])
spread(ch[x][0], tag[x]);
if (ch[x][1])
spread(ch[x][1], tag[x]);
tag[x] = 0;
}
}
inline void rotate(int x) {
int y = fa[x], z = fa[y], d = dir(x);
if (!isroot(y))
ch[z][dir(y)] = x;
fa[x] = z, ch[y][d] = ch[x][d ^ 1];
if (ch[x][d ^ 1])
fa[ch[x][d ^ 1]] = y;
ch[x][d ^ 1] = y, fa[y] = x;
}
inline void splay(int x) {
int top = 1;
sta[++top] = x;
for (int cur = x; !isroot(cur); cur = fa[cur])
sta[++top] = fa[cur];
while (top)
pushdown(sta[top--]);
for (int f = fa[x]; !isroot(x); rotate(x), f = fa[x])
if (!isroot(f))
rotate(dir(f) == dir(x) ? f : x);
}
inline void access(int x) {
for (int y = 0; x; y = x, x = fa[x])
splay(x), ch[x][1] = y;
}
inline void link(int x, int y) {
access(y), splay(y), access(x);
ch[y][1] = x, fa[x] = y;
spread(y, s[x]), s[x] = 0;
}
inline void cut(int x) {
access(x), splay(x), spread(ch[x][0], -s[x]);
fa[ch[x][0]] = 0, ch[x][0] = 0;
}
inline int query(int x) {
return access(x), splay(x), s[x];
}
} // namespace LCT
namespace SAM {
int ch[N][S], fa[N], len[N];
int tot = 1, las = 1;
inline void extend(int c) {
int p = las, np = las = ++tot;
len[np] = len[p] + 1, LCT::spread(np, 1);
for (; p && !ch[p][c]; p = fa[p])
ch[p][c] = np;
if (!p)
LCT::link(np, fa[np] = 1);
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
LCT::link(np, fa[np] = q);
else {
int nq = ++tot;
LCT::cut(q), LCT::link(nq, fa[nq] = fa[q]), LCT::link(q, fa[q] = nq), LCT::link(np, fa[np] = nq);
len[nq] = len[p] + 1, memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
}
}
}
inline int query(char *str, int n) {
int u = 1;
for (int i = 0; i < n && u; ++i)
u = ch[u][str[i] - 'A'];
return u ? LCT::query(u) : 0;
}
} // namespace SAM
signed main() {
scanf("%d%s", &m, str);
int n = strlen(str);
for (int i = 0; i < n; ++i)
SAM::extend(str[i] - 'A');
while (m--) {
char op[5];
scanf("%s", op);
Decode::decodeWithMask(Decode::mask);
int n = strlen(str);
if (op[0] == 'A') {
for (int i = 0; i < n; ++i)
SAM::extend(str[i] - 'A');
} else {
int ans = SAM::query(str, n);
printf("%d\n", ans), Decode::mask ^= ans;
}
}
return 0;
}
Parent Tree 上的 DP
CF700E Cool Slogans
给出一个字符串 \(S\) ,需要构造字符串序列 \(s_{1 \sim k}\) 满足任意 \(s_i\) 都是 \(S\) 的子串,且任意相邻的两个串满足前者在后者中出现至少两次(可以重叠),最大化序列长度 \(k\) 。
\(n \le 2 \times 10^5\)
对 \(S\) 建出 SAM,线段树合并维护 \(\mathrm{endpos}\) 集合。
考虑在 Parent Tree 上从上往下 DP,设 \(f_i\) 表示 \(i\) 结尾时序列的最长长度。
考虑转移,若父节点在子节点出现至少两次,则转移时 \(f\) 加一,否则不变。
考虑如何判定上面这个条件,对于一对父子关系 \((u, f)\) ,记上一个串为 \(g\) ,找到 \(u\) 的任意一个 \(\mathrm{endpos}\) 记为 \(p\) ,则 \(g\) 的 \(\mathrm{endpos}\) 在 \(p\) 处出现一次,只要在 \([p - \mathrm{len}(u) + \mathrm{len(g)}, p - 1]\) 中再出现一次即可。
时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 4e5 + 7, S = 26;
char str[N];
int n;
namespace SMT {
const int S = 3e7 + 7;
int lc[S], rc[S];
int rt[N];
int tot;
void insert(int &x, int nl, int nr, int p) {
if (!x)
x = ++tot;
if (nl == nr)
return;
int mid = (nl + nr) >> 1;
if (p <= mid)
insert(lc[x], nl, mid, p);
else
insert(rc[x], mid + 1, nr, p);
}
int merge(int a, int b, int l, int r) {
if (!a || !b)
return a | b;
int x = ++tot;
if (l == r)
return x;
int mid = (l + r) >> 1;
lc[x] = merge(lc[a], lc[b], l, mid), rc[x] = merge(rc[a], rc[b], mid + 1, r);
return x;
}
int query(int x, int nl, int nr, int l, int r) {
if (nl == nr)
return nl;
int mid = (nl + nr) >> 1;
if (lc[x] && l <= mid) {
int res = query(lc[x], nl, mid, l, r);
if (~res)
return res;
}
return rc[x] && r > mid ? query(rc[x], mid + 1, nr, l, r) : -1;
}
} // namespace SMT
namespace SAM {
int ch[N][S], fa[N], len[N], f[N], g[N];
int tot = 1, las = 1;
inline void extend(int c) {
int p = las, np = las = ++tot;
len[np] = len[p] + 1, SMT::insert(SMT::rt[np], 1, n, len[np]);
for (; p && !ch[p][c]; p = fa[p])
ch[p][c] = np;
if (!p)
fa[np] = 1;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
fa[np] = q;
else {
int nq = ++tot;
fa[nq] = fa[q], fa[q] = fa[np] = nq, len[nq] = len[p] + 1;
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
}
}
}
inline int solve() {
static int cnt[N], id[N];
memset(cnt, 0, sizeof(int) * (n + 1));
for (int i = 1; i <= tot; ++i)
++cnt[len[i]];
for (int i = 1; i <= n; ++i)
cnt[i] += cnt[i - 1];
for (int i = tot; i; --i)
id[cnt[len[i]]--] = i;
for (int i = tot; i > 1; --i)
SMT::rt[fa[id[i]]] = SMT::merge(SMT::rt[fa[id[i]]], SMT::rt[id[i]], 1, n);
for (int i = 2; i <= tot; ++i) {
int x = id[i];
if (fa[x] == 1) {
f[x] = 1, g[x] = x;
continue;
}
int p = SMT::query(SMT::rt[x], 1, n, 1, n);
if (~SMT::query(SMT::rt[g[fa[x]]], 1, n, p - len[x] + len[g[fa[x]]], p - 1))
f[x] = f[fa[x]] + 1, g[x] = x;
else
f[x] = f[fa[x]], g[x] = g[fa[x]];
}
return *max_element(f + 1, f + tot + 1);
}
} // namespace SAM
signed main() {
scanf("%d%s", &n, str + 1);
for (int i = 1; i <= n; ++i)
SAM::extend(str[i] - 'a');
printf("%d", SAM::solve());
return 0;
}
广义 SAM
P3346 [ZJOI2015] 诸神眷顾的幻想乡
给出一棵树,每个点有 \([0, c - 1]\) 之间的颜色,一条树上路径可以对应一个字符串,求所有路径构成的本质不同字符串数量。
\(n \le 10^5\) ,\(c \le 10\) ,度数为 \(1\) 的点的数量 \(\le 20\)
可以发现自动机结构表示字符串的路径都是从上往下的,而树的结构则更加灵活。
观察发现,对于树上任意一条路径,一定在某个叶子为根时成为一条从上往下的路径。
由于叶子数量很少,枚举每个叶子为根,将这些 Trie 树合并建立广义 SAM 即可,时间复杂度 \(O(n \times cnt)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 4e6 + 7, S = 10;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int a[N];
int n, c;
namespace Trie {
int ch[N][S], fa[N], edg[N];
int tot = 1;
inline int extend(int u, int c) {
if (!ch[u][c])
ch[u][c] = ++tot, fa[tot] = u, edg[tot] = c;
return ch[u][c];
}
} // namespace Trie
void dfs(int u, int f, int x) {
x = Trie::extend(x, a[u]);
for (int v : G.e[u])
if (v != f)
dfs(v, u, x);
}
namespace SAM {
int ch[N][S], fa[N], len[N], pos[N];
int tot = 1;
inline int extend(int las, int c) {
int p = las, np = ++tot;
len[np] = len[p] + 1;
for (; p && !ch[p][c]; p = fa[p])
ch[p][c] = np;
if (!p)
fa[np] = 1;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
fa[np] = q;
else {
int nq = ++tot;
fa[nq] = fa[q], fa[q] = fa[np] = nq, len[nq] = len[p] + 1;
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
}
}
return np;
}
inline void build() {
pos[1] = 1;
queue<int> q;
for (int i = 0; i < S; ++i)
if (Trie::ch[1][i])
q.emplace(Trie::ch[1][i]);
while (!q.empty()) {
int u = q.front();
q.pop(), pos[u] = extend(pos[Trie::fa[u]], Trie::edg[u]);
for (int i = 0; i < S; ++i)
if (Trie::ch[u][i])
q.emplace(Trie::ch[u][i]);
}
}
inline ll solve() {
ll ans = 0;
for (int i = 2; i <= tot; ++i)
ans += len[i] - len[fa[i]];
return ans;
}
} // namespace SAM
signed main() {
scanf("%d%d", &n, &c);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
for (int i = 1; i <= n; ++i)
if (G.e[i].size() == 1)
dfs(i, 0, 1);
SAM::build();
printf("%lld", SAM::solve());
return 0;
}
CF452E Three strings
给出三个字符串 \(A, B, C\) ,对于每个 \(i \in [1, \min(|A|, |B|, |C|)]\) ,求满足 \(A[a, a + i - 1] = B[b, b + i - 1] = C[c, c + i - 1]\) 的三元组 \((a, b, c)\) 的数量 \(\bmod (10^9 + 7)\) 。
\(|A| + |B| + |C| \le 3 \times 10^5\)
建出广义 SAM,维护一下每个状态在 \(A, B, C\) 中的出现次数 \(|\mathrm{endpos}|\) ,则一个状态 \(u\) 会对 \([\mathrm{len}(fa_u) + 1, \mathrm{len}(u)]\) 产生 \(|\mathrm{endpos}_A(u)| \times |\mathrm{endpos}_B(u)| \times |\mathrm{endpos}_C(u)|\) 的贡献。
时间复杂度 \(O(|A| + |B| + |C|)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 6e5 + 7, S = 26;
int ans[N];
char a[N], b[N], c[N];
int na, nb, nc;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline tuple<int, int, int> operator + (tuple<int, int, int> a, tuple<int, int, int> b) {
return make_tuple(add(get<0>(a), get<0>(b)), add(get<1>(a), get<1>(b)), add(get<2>(a), get<2>(b)));
}
namespace Trie {
tuple<int, int, int> val[N];
int ch[N][S], fa[N], edg[N];
int tot = 1;
inline void insert(char *str, int n, tuple<int, int, int> k) {
int u = 1;
for (int i = 1; i <= n; ++i) {
int c = str[i] - 'a';
if (!ch[u][c])
ch[u][c] = ++tot, fa[tot] = u, edg[tot] = c;
u = ch[u][c], val[u] = val[u] + k;
}
}
} // namespace Trie
namespace SAM {
tuple<int, int, int> val[N];
int ch[N][S], fa[N], len[N], pos[N];
int tot = 1;
inline int extend(int las, int c) {
int p = las, np = ++tot;
len[np] = len[p] + 1;
for (; p && !ch[p][c]; p = fa[p])
ch[p][c] = np;
if (!p)
fa[np] = 1;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
fa[np] = q;
else {
int nq = ++tot;
fa[nq] = fa[q], fa[q] = fa[np] = nq, len[nq] = len[p] + 1;
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
}
}
return np;
}
inline void build() {
queue<int> q;
pos[1] = 1, q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop(), val[pos[u]] = Trie::val[u];
for (int i = 0; i < S; ++i)
if (Trie::ch[u][i])
pos[Trie::ch[u][i]] = extend(pos[u], i), q.emplace(Trie::ch[u][i]);
}
}
inline void solve() {
static int cnt[N], id[N];
int n = *max_element(len + 1, len + tot + 1);
memset(cnt, 0, sizeof(int) * n);
for (int i = 1; i <= tot; ++i)
++cnt[len[i]];
for (int i = 1; i <= n; ++i)
cnt[i] += cnt[i - 1];
for (int i = tot; i; --i)
id[cnt[len[i]]--] = i;
for (int i = tot; i > 1; --i)
val[fa[id[i]]] = val[fa[id[i]]] + val[id[i]];
for (int i = 2; i <= tot; ++i) {
int x = id[i], res = 1ll * get<0>(val[x]) * get<1>(val[x]) % Mod * get<2>(val[x]) % Mod;
ans[len[fa[x]] + 1] = add(ans[len[fa[x]] + 1], res), ans[len[x] + 1] = dec(ans[len[x] + 1], res);
}
}
} // namespace SAM
signed main() {
scanf("%s%s%s", a + 1, b + 1, c + 1);
Trie::insert(a, na = strlen(a + 1), make_tuple(1, 0, 0));
Trie::insert(b, nb = strlen(b + 1), make_tuple(0, 1, 0));
Trie::insert(c, nc = strlen(c + 1), make_tuple(0, 0, 1));
SAM::build(), SAM::solve();
for (int i = 1; i <= min(na, min(nb, nc)); ++i)
printf("%d ", ans[i] = add(ans[i], ans[i - 1]));
return 0;
}
P4022 [CTSC2012] 熟悉的文章
给出字典 \(T_{1 \sim m}\) ,多次询问一个字符串 \(S\) 的 \(L_0\) ,其中 \(L_0\) 表示最大的 \(l\) 满足将 \(S\) 划分为若干段之后,所有长度 \(\ge l\) 且在字典中出现(是某个 \(T_i\) 的子串)的子串的长度和 \(\ge 0.9 |S|\) 。
\(\sum |T_i| + \sum |S| \le 1.1 \times 10^6\) ,字符集为 \(\{ 0, 1 \}\)
考虑二分 \(L_0 = l\) 判定合法性。先建立字典 \(T_{1 \sim m}\) 的广义 SAM,求出 \(p_i\) 表示 \(S[1, i]\) 的最长匹配后缀。设 \(f_i\) 表示 \(S[1, i]\) 分段后的最长匹配长度,则:
可以发现 \(i - p_i\) 和 \(i - l\) 都是单调的,因此可以单调队列优化到线性。
时间复杂度 \(O(\sum |T_i| + \sum |S| \log |S|)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 3e6 + 7, S = 2;
int p[N], f[N];
char str[N];
int n, m;
namespace SAM {
int ch[N][S], fa[N], len[N];
int tot = 1;
inline int extend(int las, int c) {
int p = las, np = ++tot;
len[np] = len[p] + 1;
for (; p && !ch[p][c]; p = fa[p])
ch[p][c] = np;
if (!p)
return fa[np] = 1, np;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
return p == las ? (--tot, q) : (fa[np] = q, np);
else {
if (p == las)
--tot, np = 0;
int nq = ++tot;
fa[nq] = fa[q], fa[q] = nq, len[nq] = len[p] + 1;
if (np)
fa[np] = nq;
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
return np ? np : nq;
}
}
}
inline void insert(char *str, int n) {
for (int i = 1, las = 1; i <= n; ++i)
las = extend(las, str[i] & 15);
}
inline void query(char *str, int n) {
int u = 1, plen = 0;
for (int i = 1; i <= n; ++i) {
int c = str[i] & 15;
while (u > 1 && !ch[u][c])
plen = len[u = fa[u]];
if (ch[u][c])
u = ch[u][c], ++plen;
p[i] = plen;
}
}
} // namespace SAM
inline bool check(int k) {
deque<int> q;
for (int i = 1; i <= m; ++i) {
if (i >= k) {
while (!q.empty() && f[q.back()] - q.back() <= f[i - k] - (i - k))
q.pop_back();
q.emplace_back(i - k);
}
while (!q.empty() && q.front() < i - p[i])
q.pop_front();
f[i] = max(f[i - 1], q.empty() ? 0 : f[q.front()] - q.front() + i);
}
return f[m] >= m * 0.9;
}
signed main() {
scanf("%d%d", &n, &m);
while (m--)
scanf("%s", str + 1), SAM::insert(str, strlen(str + 1));
while (n--) {
scanf("%s", str + 1);
SAM::query(str, m = strlen(str + 1));
int l = 0, r = m, ans = 0;
while (l <= r) {
int mid = (l + r) >> 1;
if (check(mid))
ans = mid, l = mid + 1;
else
r = mid - 1;
}
printf("%d\n", ans);
}
return 0;
}
CF204E Little Elephant and Strings
给出 \(n\) 个字符串 \(S_{1 \sim n}\) ,对于每个 \(S_i\) ,求其有多少区间 \([l, r]\) 满足 \(S_i[l, r]\) 至少是 \(k\) 个 \(S_j\) 的子串。
\(n, k, \sum |S_i| \le 10^5\)
先对 \(S_{1 \sim n}\) 建立广义 SAM,求出每个状态是多少个 \(S_i\) 的子串,只要把每个 \(S_i[1, j]\) 的点到根的链的并 \(+1\) 即可,不难处理。
然后对于一个 \(S_i\) ,考虑对每个前缀 \(S_i[1, j]\) 求出有多少后缀满足条件,这可以通过在 Parent Tree 上倍增解决。
时间复杂度 \(O((\sum |S_i|) \log (\sum |S_i|))\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7, S = 26, LOGN = 17;
vector<int> pre[N];
string str[N];
int n, k;
namespace SAM {
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int ch[N][S], fa[N][LOGN], len[N], dep[N], dfn[N], num[N];
int tot = 1, dfstime;
inline int extend(int las, int c) {
int p = las, np = ++tot;
len[np] = len[p] + 1;
for (; p && !ch[p][c]; p = fa[p][0])
ch[p][c] = np;
if (!p)
return fa[np][0] = 1, np;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
return p == las ? (--tot, q) : (fa[np][0] = q, np);
else {
if (p == las)
--tot, np = 0;
int nq = ++tot;
fa[nq][0] = fa[q][0], fa[q][0] = nq, len[nq] = len[p] + 1;
if (np)
fa[np][0] = nq;
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p][0])
ch[p][c] = nq;
return np ? np : nq;
}
}
}
inline vector<int> insert(string &str) {
vector<int> vec;
for (int i = 0, x = 1; i < str.length(); ++i)
vec.emplace_back(x = extend(x, str[i] - 'a'));
return vec;
}
void dfs1(int u) {
dep[u] = dep[fa[u][0]] + 1, dfn[u] = ++dfstime;
for (int i = 1; i < LOGN; ++i)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (int v : G.e[u])
dfs1(v);
}
inline int LCA(int x, int y) {
if (dep[x] < dep[y])
swap(x, y);
for (int i = 0, h = dep[x] - dep[y]; h; ++i, h >>= 1)
if (h & 1)
x = fa[x][i];
if (x == y)
return x;
for (int i = LOGN - 1; ~i; --i)
if (fa[x][i] != fa[y][i])
x = fa[x][i], y = fa[y][i];
return fa[x][0];
}
inline void build() {
for (int i = 2; i <= tot; ++i)
G.insert(fa[i][0], i);
dfs1(1);
}
void dfs2(int u) {
for (int v : G.e[u])
dfs2(v), num[u] += num[v];
}
inline int query(int x) {
if (num[x] >= k)
return len[x];
for (int i = LOGN - 1; ~i; --i)
if (fa[x][i] && num[fa[x][i]] < k)
x = fa[x][i];
return len[fa[x][0]];
}
} // namespace SAM
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> k;
for (int i = 1; i <= n; ++i)
cin >> str[i], pre[i] = SAM::insert(str[i]);
SAM::build();
for (int i = 1; i <= n; ++i) {
vector<int> vec = pre[i];
sort(vec.begin(), vec.end(), [](const int &a, const int &b) {
return SAM::dfn[a] < SAM::dfn[b];
});
for (int it : vec)
++SAM::num[it];
for (int j = 1; j < vec.size(); ++j)
--SAM::num[SAM::LCA(vec[j - 1], vec[j])];
}
SAM::dfs2(1);
for (int i = 1; i <= n; ++i) {
ll ans = 0;
for (int j = 0; j < str[i].length(); ++j)
ans += SAM::query(pre[i][j]);
cout << ans << ' ';
}
return 0;
}
P4081 [USACO17DEC] Standing Out from the Herd P
给出 \(n\) 个字符串 \(S_{1 \sim n}\) ,对每个串 \(S_i\) 求有多少本质不同子串满足在其他串 \(S_j (j \ne i)\) 中未出现。
\(n, \sum |S_i| \le 10^5\)
和上一题类似,维护每个状态的出现次数,只统计出现次数为 \(1\) 的状态即可。
一个简单的实现是对于 \(S_i\) 的每个前缀暴力跳父亲,并打上标记 \(i\) 表示该状态属于 \(S_i\) 。若跳到一个被标记的点,若该标记等于 \(i\) 则返回,否则若标记不为 \(-1\) 则将其打上 \(-1\) 的标记表示至少在两个串中出现,若标记为 \(-1\) 则返回。
时间复杂度 \(O(\sum |S_i|)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7, S = 26;
vector<int> pre[N];
string str[N];
ll ans[N];
int n, k;
namespace SAM {
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int ch[N][S], fa[N], len[N], bel[N];
int tot = 1, dfstime;
inline int extend(int las, int c) {
int p = las, np = ++tot;
len[np] = len[p] + 1;
for (; p && !ch[p][c]; p = fa[p])
ch[p][c] = np;
if (!p)
return fa[np] = 1, np;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
return p == las ? (--tot, q) : (fa[np] = q, np);
else {
if (p == las)
--tot, np = 0;
int nq = ++tot;
fa[nq] = fa[q], fa[q] = nq, len[nq] = len[p] + 1;
if (np)
fa[np] = nq;
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p])
ch[p][c] = nq;
return np ? np : nq;
}
}
}
inline vector<int> insert(string &str) {
vector<int> vec;
for (int i = 0, x = 1; i < str.length(); ++i)
vec.emplace_back(x = extend(x, str[i] - 'a'));
return vec;
}
inline void update(int x, int k) {
for (; x && ~bel[x] && bel[x] != k; x = fa[x])
bel[x] = (bel[x] ? -1 : k);
}
} // namespace SAM
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> str[i], pre[i] = SAM::insert(str[i]);
for (int i = 1; i <= n; ++i)
for (int it : pre[i])
SAM::update(it, i);
for (int i = 2; i <= SAM::tot; ++i)
if (SAM::bel[i] && ~SAM::bel[i])
ans[SAM::bel[i]] += SAM::len[i] - SAM::len[SAM::fa[i]];
for (int i = 1; i <= n; ++i)
printf("%lld\n", ans[i]);
return 0;
}
CF666E Forensic Examination
给出一个字符串 \(S\) 与 \(m\) 个字符串 \(T_{1 \sim m}\) ,\(q\) 次询问 \(S[L, R]\) 在 \(T_{l \sim r}\) 的哪个串中出现次数最多及出现次数。
\(|S|, q \le 5 \times 10^5\) ,\(\sum |T_i| \le 5 \times 10^4\)
对 \(T_{1 \sim m}\) 建出广义 SAM 后,直接线段树合并维护每个状态在 \(T_{1 \sim m}\) 中的出现次数即可,时间复杂度一只 \(\log\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 5e5 + 7, S = 26, LOGN = 17;
pair<int, int> pre[N];
char str[N], t[N];
int n, m, q;
namespace SMT {
const int S = 3e7 + 7;
pair<int, int> mx[S];
int rt[N], lc[S], rc[S];
int tot;
void insert(int &x, int nl, int nr, int p) {
if (!x)
x = ++tot;
if (nl == nr) {
mx[x] = make_pair(1, -p);
return;
}
int mid = (nl + nr) >> 1;
if (p <= mid)
insert(lc[x], nl, mid, p);
else
insert(rc[x], mid + 1, nr, p);
mx[x] = max(mx[lc[x]], mx[rc[x]]);
}
int merge(int a, int b, int l, int r) {
if (!a || !b)
return a | b;
int x = ++tot;
if (l == r) {
mx[x] = make_pair(mx[a].first + mx[b].first, -l);
return x;
}
int mid = (l + r) >> 1;
lc[x] = merge(lc[a], lc[b], l, mid), rc[x] = merge(rc[a], rc[b], mid + 1, r);
return mx[x] = max(mx[lc[x]], mx[rc[x]]), x;
}
pair<int, int> query(int x, int nl, int nr, int l, int r) {
if (!x)
return make_pair(0, 0);
if (l <= nl && nr <= r)
return mx[x];
int mid = (nl + nr) >> 1;
if (r <= mid)
return query(lc[x], nl, mid, l, r);
else if (l > mid)
return query(rc[x], mid + 1, nr, l, r);
else
return max(query(lc[x], nl, mid, l, r), query(rc[x], mid + 1, nr, l, r));
}
void dfs(int x, int l, int r) {
if (!x)
return;
if (l == r) {
printf("------%d %d\n", mx[x].first, -mx[x].second);
return;
}
int mid = (l + r) >> 1;
dfs(lc[x], l, mid), dfs(rc[x], mid + 1, r);
}
} // namespace SMT
namespace SAM {
int ch[N][S], fa[N][LOGN], len[N];
int tot = 1;
inline int extend(int las, int c) {
int p = las, np = ++tot;
len[np] = len[p] + 1;
for (; p && !ch[p][c]; p = fa[p][0])
ch[p][c] = np;
if (!p)
return fa[np][0] = 1, np;
else {
int q = ch[p][c];
if (len[q] == len[p] + 1)
return p == las ? (--tot, q) : (fa[np][0] = q, np);
else {
if (p == las)
--tot, np = 0;
int nq = ++tot;
fa[nq][0] = fa[q][0], fa[q][0] = nq, len[nq] = len[p] + 1;
if (np)
fa[np][0] = nq;
memcpy(ch[nq], ch[q], sizeof(int) * S);
for (; p && ch[p][c] == q; p = fa[p][0])
ch[p][c] = nq;
return np ? np : nq;
}
}
}
inline void insert(char *str, int n, int id) {
for (int i = 1, x = 1; i <= n; ++i)
x = extend(x, str[i] - 'a'), SMT::insert(SMT::rt[x], 1, m, id);
}
inline void build() {
static int cnt[N], id[N];
int n = *max_element(len + 1, len + tot + 1);
memset(cnt, 0, sizeof(int) * (n + 1));
for (int i = 1; i <= tot; ++i)
++cnt[len[i]];
for (int i = 1; i <= n; ++i)
cnt[i] += cnt[i - 1];
for (int i = tot; i; --i)
id[cnt[len[i]]--] = i;
for (int i = tot; i > 1; --i)
SMT::rt[fa[id[i]][0]] = SMT::merge(SMT::rt[fa[id[i]][0]], SMT::rt[id[i]], 1, m);
for (int j = 1; j < LOGN; ++j)
for (int i = 1; i <= tot; ++i)
fa[i][j] = fa[fa[i][j - 1]][j - 1];
}
inline void query(char *str, int n) {
int u = 1, plen = 0;
for (int i = 1; i <= n; ++i) {
int c = str[i] - 'a';
while (u > 1 && !ch[u][c])
plen = len[u = fa[u][0]];
if (ch[u][c])
u = ch[u][c], ++plen;
pre[i] = make_pair(u, plen);
}
}
inline int locate(int x, int k) {
for (int i = LOGN - 1; ~i; --i)
if (fa[x][i] && len[fa[x][i]] >= k)
x = fa[x][i];
return x;
}
} // namespace SAM
signed main() {
scanf("%s%d", str + 1, &m), n = strlen(str + 1);
for (int i = 1; i <= m; ++i)
scanf("%s", t + 1), SAM::insert(t, strlen(t + 1), i);
SAM::build(), SAM::query(str, n);
scanf("%d", &q);
while (q--) {
int l, r, L, R;
scanf("%d%d%d%d", &l, &r, &L, &R);
if (R - L + 1 > pre[R].second) {
printf("%d %d\n", l, 0);
continue;
}
int x = SAM::locate(pre[R].first, R - L + 1);
auto ans = SMT::query(SMT::rt[x], 1, m, l, r);
if (ans == make_pair(0, 0))
printf("%d %d\n", l, 0);
else
printf("%d %d\n", -ans.second, ans.first);
}
return 0;
}

浙公网安备 33010602011771号