后缀自动机
感觉其实是把 OI-wiki 誊抄了一遍……(捂脸)
本文的证明可能有点感性,请谨慎食用。
注:下文中字符串下标均从
定义
SAM 是接受
说人话就是,SAM 是一个有向图,边上有字符,从起点开始走,有一些终点,保证
下面是
从起点
构建
endpos
对于
若
不难知道 SAM 的每个节点对应着一个等价类。
引理 1
对于子串
,假设 。若 ,则 是 的后缀,反之亦然。
较为显然。
引理 2
对于子串
,假设 。则:
证明:若
引理 3
将同一等价类内的串按照长度从大到小排序,那么每个串都是前一个串的后缀,并且所有串的长度恰好覆盖一个区间
。
证明:由引理 1 得等价类中没有等长的子串,设
link
对于 SAM 中不是起点的状态
设
定义空串(即初始状态
引理 4
后缀链接构成一棵树,根节点为
。
证明:每次向
引理 5
按照
的包含关系建出的树(由引理 2 可知这确实是一棵树)和 连出的树相同,称其为 parent tree。
证明:对于状态
对于状态
由定义得
算法
SAM 的构建是在线的,支持往末尾加入字符。在过程中,只需要维护
初始时 SAM 仅包含初始状态
令
从
如果没有找到
否则设其通过
否则复制
最后,将
容易发现 SAM 的状态数不会超过
还能得到 SAM 的转移数不会超过
原理
创建
随后访问原串
否则找到一条转移
如果
否则需要将
最小性
实际上,SAM 所有节点构成了一棵 parent tree,每个节点的 endpos 集合都不一样就已经最小了。
OI-wiki 里提到了一个叫 Myhill–Nerode Theorem 的东西,这是什么啊?
Talk is cheap. Show me the code.
然而代码的下标是从
int tag[N << 1];
struct SAM {
struct node { int len, link, nxt[26]; };
int cnt, last; node t[N << 1];
SAM() { cnt = 1; }
void insert(int c) {
int cur = ++cnt;
t[cur].len = t[last].len + 1, tag[cur] = 1;
int p = last;
while (p && !t[p].nxt[c]) t[p].nxt[c] = cur, p = t[p].link;
if (!p) t[cur].link = 1;
else {
int q = t[p].nxt[c];
if (t[q].len == t[p].len + 1) t[cur].link = q;
else {
int copy = ++cnt;
t[copy] = t[q], t[copy].len = t[p].len + 1;
while (p && t[p].nxt[c] == q) t[p].nxt[c] = copy, p = t[p].link;
t[q].link = t[cur].link = copy;
}
}
last = cur;
}
} sam;
可以把上面代码中的 int nxt[26]
换成 unordered_map<int, int> nxt
,可以做到
应用
字符串匹配
直接在 SAM 上面走即可,如果走不动了就不匹配。这样同时可以得到模式串在文本串上出现的最长前缀。时间复杂度为
本质不同子串个数
答案为 SAM 上从
答案为
另外,在 SAM 上做拓扑排序并不需要把图建出来,只需要借助
for (int i = 1; i <= cnt; i++) buc[t[i].len]++;
for (int i = 1; i <= cnt; i++) buc[i] += buc[i - 1];
for (int i = 1; i <= cnt; i++) id[buc[t[i].len]--] = i;
本质不同子串长度和
在求出
加上
出现次数
答案为
最小表示法
将字符串复制一倍,现在要找所有长度为
本质不同第 k 小子串
先求出
第 k 小子串
需要更改上面
Code:
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e6 + 5;
struct node { int len, link, nxt[26]; } t[N];
int op, k, cnt = 1, last = 1, buc[N], id[N], siz[N], sum[N];
char s[N];
void insert(int c) {
int cur = ++cnt;
t[cur].len = t[last].len + 1, siz[cur] = 1;
int p = last;
while (p && !t[p].nxt[c]) t[p].nxt[c] = cur, p = t[p].link;
if (!p) t[cur].link = 1;
else {
int q = t[p].nxt[c];
if (t[q].len == t[p].len + 1) t[cur].link = q;
else {
int copy = ++cnt;
t[copy] = t[q], t[copy].len = t[p].len + 1;
while (p && t[p].nxt[c] == q) t[p].nxt[c] = copy, p = t[p].link;
t[q].link = t[cur].link = copy;
}
}
last = cur;
}
void topsort() {
for (int i = 1; i <= cnt; i++) buc[t[i].len]++;
for (int i = 1; i <= cnt; i++) buc[i] += buc[i - 1];
for (int i = 1; i <= cnt; i++) id[buc[t[i].len]--] = i;
for (int i = cnt; i >= 1; i--) siz[t[id[i]].link] += siz[id[i]];
for (int i = 1; i <= cnt; i++) sum[i] = op ? siz[i] : (siz[i] = 1);
siz[1] = sum[1] = 0;
for (int i = cnt; i >= 1; i--) for (int j = 0; j < 26; j++) sum[id[i]] += sum[t[id[i]].nxt[j]];
}
void kth(int p, int k) {
if (k <= siz[p]) return; k -= siz[p];
for (int i = 0, c; i < 26; i++) {
if (!(c = t[p].nxt[i])) continue;
if (k > sum[c]) k -= sum[c];
else return putchar('a' + i), kth(c, k), void();
}
}
signed main() {
scanf("%s%lld%lld", s, &op, &k);
for (int i = 0; s[i]; i++) insert(s[i] - 'a');
topsort();
if (sum[1] >= k) kth(1, k); else puts("-1");
}
双串最长公共子串
建出
int calc(char s[]) {
int p = 1, len = 0, ans = 0;
for (int i = 0, c; s[i]; i++) {
c = s[i] - 'a';
if (t[p].nxt[c]) p = t[p].nxt[c], len++;
else {
while (p && !t[p].nxt[c]) p = t[p].link;
if (p) len = t[p].len + 1, p = t[p].nxt[c];
else p = 1, len = 0;
}
ans = max(ans, len);
}
return ans;
}
多串最长公共子串
选择最短的串建立 SAM(为了保证复杂度不超过
代码里没有选择最短串。
void upd(char s[]) {
int p = 1, len = 0;
for (int i = 0, c; s[i]; i++) {
c = s[i] - 'a';
while (p && !t[p].nxt[c]) p = t[p].link, len = t[p].len;
if (p) len++, p = t[p].nxt[c], mx[p] = max(mx[p], len);
else p = 1, len = 0;
}
for (int i = cnt, u, f; i >= 1; i--) {
u = id[i], f = t[u].link;
mx[f] = max(mx[f], min(mx[u], t[f].len)), mn[u] = min(mn[u], mx[u]), mx[u] = 0;
}
}
int main() {
scanf("%s", s);
for (int i = 0; s[i]; i++) insert(s[i] - 'a');
topsort();
memset(mn, 0x3f, sizeof(mn));
while (scanf("%s", s) != EOF) upd(s);
for (int i = 1; i <= cnt; i++) ans = max(ans, mn[i]);
printf("%d", ans);
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律