闲话 22.12.21
闲话
什么时候结束字符串啊(
我想看多项式了(
怎么有人开始推歌了(
那我也整点杂的活吧
今日推歌是 须臾永恒 Zeno & 星尘 Minus
建议听一下 还是比较震撼的 好听!
已经循环不知道几百遍了
\减减/\减减/\减减/\减减/
广义后缀自动机
大部分是贺辰星凌的博客。
简称 GSA(General Suffix Automaton)。没必要新开一篇讲,内容也不多。
SAM 帮助我们找到了一个字符串的所有子串的信息。假如我们需要多个字符串的所有子串呢?等价地,给定一棵 Trie 树后我们该如何找到所有子串呢?
我们称建立在一棵 Trie 上的后缀自动机为广义后缀自动机,简称 GSA。
我们先讲如何实现。
首先是离线构造。我们直接把串插入一棵 Trie,随后在其上 bfs。一个 Trie 的节点插入时对应的 lst 就是它在 Trie 上的父亲在 GSA 里的节点。
这个正确性应该比较显然。我们在 Trie 上 bfs 等价于插入字符串,只是仅插入一次公共前缀。这样压缩掉了 LCP,因此有最小性。
code
struct GSA {
int mlc = 1, lens;
int link[N], len[N], son[N][26];
int Trie[N][26], Tmlc = 1;
char ch[N];
void insert() {
cin >> ch + 1;
lens = strlen(ch + 1);
int p = 1;
rep(i,1,lens) {
if (!Trie[p][ ch[i] - 'a' ])
Trie[p][ ch[i] - 'a' ] = ++ Tmlc;
p = Trie[p][ ch[i] - 'a' ];
}
}
int extend(int c, int lst) {
int now = ++ mlc, p = lst;
len[now] = len[lst] + 1;
while (p and !son[p][c])
son[p][c] = now, p = link[p];
if (!p) link[now] = 1;
else {
int q = son[p][c];
if (len[q] == len[p] + 1) link[now] = q;
else {
int kage = ++ mlc;
len[kage] = len[p] + 1, link[kage] = link[q];
link[q] = link[now] = kage;
memcpy(son[kage], son[q], sizeof son[kage]);
while (p and son[p][c] == q)
son[p][c] = kage, p = link[p];
}
} return now;
}
int pos[N];
queue<tuple<int,int,int> > que;
void bfs() {
rep (i, 0, 25) if (Trie[1][i])
que.emplace(Trie[1][i], i, 1);
while (que.size()) {
auto[u, c, lst] = que.front(); que.pop();
lst = extend(c, lst);
rep (i, 0, 25) if (Trie[u][i])
que.emplace(Trie[u][i], i, lst);
}
}
void build(int m) {
while (m --) insert();
bfs();
}
} S;
然后是在线构造。推荐写在线构造,码量小点,常数小点(\(2s\to 1s\))。
在线构造就是按照 \(SAM\) 的方式构造,每插入一个字符时最开始就把 \(lst\) 移到 \(t_0\) 位置,然后按照有改动的 \(\text{extend}\) 函数插入。函数返回当前串对应的节点作为下一次插入的 \(lst\)。那我们怎么找到当前串对应的节点呢?
我们回忆 SAM 的构造过程。假如 \(lst\) 没有沿当前字符 \(c\) 的转移,一切如常。反之我们直接跳转到“找到了这样的转移”段,按照添加新转移进行即可。如果存在 \(lst\to now\),就返回 \(now\),反之拆点。
code
struct GSA {
int mlc = 1, lst, lens;
int link[N], len[N], son[N][26];
char ch[N];
int extend(int c, int lst) {
if (son[lst][c]) {
int q = son[lst][c], p = lst;
if (len[q] == len[p] + 1) return q;
else {
int kage = ++ mlc;
len[kage] = len[p] + 1, link[kage] = link[q];
link[q] = kage;
memcpy(son[kage], son[q], sizeof son[kage]);
while (p and son[p][c] == q)
son[p][c] = kage, p = link[p];
return kage;
}
} else {
int now = ++ mlc, p = lst;
len[now] = len[lst] + 1;
while (p and !son[p][c])
son[p][c] = now, p = link[p];
if (!p) link[now] = 1;
else {
int q = son[p][c];
if (len[q] == len[p] + 1) link[now] = q;
else {
int kage = ++ mlc;
len[kage] = len[p] + 1, link[kage] = link[q];
link[q] = link[now] = kage;
memcpy(son[kage], son[q], sizeof son[kage]);
while (p and son[p][c] == q)
son[p][c] = kage, p = link[p];
}
} return now;
}
}
void build(int m) {
while (m --) {
cin >> ch + 1;
lens = strlen(ch + 1), lst = 1;
rep(i,1,lens) lst = extend(ch[i] - 'a', lst);
}
}
} S;
别的不写了(
上例题吧。
GSA 维护两个字符串的信息
以 [HAOI2016]找相同字符 为例。给定两个字符串,求两个字符串的相同子串数量。
相同子串想到在 SAM 上刻画,对两个字符串分别建 SAM。
然后 bfs 这两个 SAM,如果有相同的边就 bfs 过去,不能再走的位置就是一个 LCP,统计一个节点对应子串在原串上的出现次数 \(siz[A/B]\),在 bfs 时每个节点算一遍 \(siz[A]\times siz[B]\) 就是答案。
然后是 GSA 的做法。对这两个串建一个 GSA,在每个节点上统计对应子串在两个串上分别的出现次数。然后对每个节点求 \((\text{len} - \text{len}\text{ link}) \times siz[A]\times siz[B]\) 的和就是答案。
GSA + DP
以 [CTSC2012]熟悉的文章 为例。题面太长,不粘。
首先二分答案转化为判定。
然后判定需要 DP。怎么 DP 呢?我们设 \(f(n)\) 为前 \(n\) 个字符最多能被覆盖多少。
首先可以不覆盖。\(f(n)\to f(n + 1)\)。
然后我们需要一段 \([m, n]\) 是这些串的一个子串。这样可以转移 \(f(m) + (n - m) \to f(n)\)。假设以 \(i\) 为右端点最长的公共子串为 \(t\),需要满足 \(m \in [i - t, i - mid]\)。
发现 \(i - t\) 单不减,\(f(m) - m\) 单不增。因此可以决策单调性,如果能快速求得 \(t\) 就能 \(O(n)\) 求解。
然后 \(t\) 怎么求呢?我们初始化一个指针 \(p\),初始在 \(t_0\)。每次加入一个字符就沿着 \(p\) 向这个字符对应的转移走,如果没有这样的转移就跳后缀链接。这样总时间复杂度是 \(O(n)\) 的。
因此能在 \(O(n\log n)\) 的复杂度内得到答案。
code
#include <bits/stdc++.h>
using namespace std;
#define rep(i,s,t) for (register int i = (s), i##_ = (t) + 1; i < i##_; ++ i)
#define pre(i,s,t) for (register int i = (s), i##_ = (t) - 1; i > i##_; -- i)
const int N = 2e6 + 10;
int n, m;
struct GSA {
int mlc = 1, lst, lens;
int link[N], len[N], son[N][2];
char ch[N];
int extend(int c, int lst) {
if (son[lst][c]) {
int q = son[lst][c], p = lst;
if (len[q] == len[p] + 1) return q;
else {
int kage = ++ mlc;
len[kage] = len[p] + 1, link[kage] = link[q];
link[q] = kage;
memcpy(son[kage], son[q], sizeof son[kage]);
while (p and son[p][c] == q)
son[p][c] = kage, p = link[p];
return kage;
}
} else {
int now = ++ mlc, p = lst;
len[now] = len[lst] + 1;
while (p and !son[p][c])
son[p][c] = now, p = link[p];
if (!p) link[now] = 1;
else {
int q = son[p][c];
if (len[q] == len[p] + 1) link[now] = q;
else {
int kage = ++ mlc;
len[kage] = len[p] + 1, link[kage] = link[q];
link[q] = link[now] = kage;
memcpy(son[kage], son[q], sizeof son[kage]);
while (p and son[p][c] == q)
son[p][c] = kage, p = link[p];
}
} return now;
}
}
void build(int m) {
while (m --) {
cin >> ch + 1;
lens = strlen(ch + 1), lst = 1;
rep(i,1,lens) lst = extend(ch[i] - '0', lst);
}
}
int cans[N];
void init() {
int ans = 0, p = 1, c;
cin >> ch + 1; lens = strlen(ch + 1);
rep(i,1,lens) {
c = ch[i] - '0';
if (son[p][c]) {
++ ans, p = son[p][c];
} else {
while (p and !son[p][c]) p = link[p];
if (p) ans = len[p] + 1, p = son[p][c];
else ans = 0, p = 1;
} cans[i] = ans;
}
}
int f[N];
int que[N], hd, tl;
bool check(int l) {
hd = 1, tl = 0;
rep(i, 1, l - 1) f[i] = 0;
rep(i, l, lens) {
f[i] = f[i - 1];
while (hd <= tl and f[que[tl]] - que[tl] <= f[i - l] - (i - l)) -- tl;
que[++ tl] = i - l;
while (hd <= tl and que[hd] < i - cans[i]) ++ hd;
if (hd <= tl) f[i] = max(f[i], f[que[hd]] + i - que[hd]);
}
return 0.9 * lens <= f[lens];
}
} S;
signed main() {
ios::sync_with_stdio(false); cin.tie(0), cout.tie(0);
cin >> n >> m;
S.build(m);
while ( n -- ) {
S.init();
int l = 0, r = S.lens, mid;
while (l <= r) {
mid = l + r >> 1;
if (S.check(mid)) l = mid + 1;
else r = mid - 1;
} cout << l - 1 << '\n';
}
}
以下是博客签名,与正文无关。
请按如下方式引用此页:
本文作者 joke3579,原文链接:https://www.cnblogs.com/joke3579/p/chitchat221221.html。
遵循 CC BY-NC-SA 4.0 协议。
请读者尽量不要在评论区发布与博客内文完全无关的评论,视情况可能删除。