【模板】后缀数据结构-SAM-SA
参考文献
练手题
CF547E Mike and Friends。
后缀自动机
概念与符号
- 自动机,转移
- 终点/endpos/Right集合
- parent/link/fail
- len/极长后缀
正确性
考虑构造aabab的SAM,每一个加入的字符会:
- 建立一些节点;
- 在一些节点的Right集合中加入len+1;
- 改变一些link的关系。
考虑遍历last的link,直到第一个有c转移的p:
- 若p==-1:则原自动机不存在c的转移边,显然cur的Right集合只有一个元素len+1,它的唯一超集是全集。令st[cur].link = 0即可。
- 否则,设q=st[p].next[c]:
- 若st[q].len == st[p].len+1,则可以顺利地在q(以及它的所有link)的Right集合中加入len+1,只需要令st[cur].link = q即可。
- 否则,若st[q].len > st[p].len+1,则cur不能顺利插入,由于st[p].len的最大性,可知q的极长后缀不为cur的后缀。即Right(q)\subsetneq {trans(x,c)|x\in Right'(p)}。(否则,必然存在一个st[p].len更大的p,使得p的Right为last的Right的超集。)这时,应该建立一个新的节点clone,其Right集合为q的Right与{len+1}的并集,并将其link设为q的link,其转移与q一致。显然,q的link节点q_的len小于st[p].len+1(否则,st[p].next[c]应该是q_)
此时,一些p的(link树上的)祖先的Right集合可能发生改变,若其经过c转移后的r等于q,则需要在其Right集合中插入len+1,故需使得其转移后的结果变为clone;若r不等于q,则可知r为q的祖先,Right集合中已经有了len+1,不需要改变。
- 每一次sam_extend()后,若p经过转移c后到了q\ne -1,总有Right(q)={x|x\in Right(p), S_{x+1}=c}。
P3804 【模板】后缀自动机 (SAM)
给定一个只包含小写字母的字符串S,请你求出 S 的所有出现次数不为 1 的子串的出现次数乘上该子串长度的最大值。
解答1
后缀自动机即可。 a.cpp Accepted
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6+10, maxt = maxn<<1;
struct sam{
int len, link, endpos;
int next[26];
sam() { memset(next, -1, sizeof(next)); }
}st[maxt];
int sz, last;
vector<int> son[maxt];
void sam_init() {
st[0].len = 0;
st[0].link = -1;
last = 0;
sz++;
}
void sam_extend(int c) {
int cur = sz++;
st[cur].len = st[last].len+1;
st[cur].endpos = 1;
int p = last;
while (p != -1 && !~st[p].next[c]) {
st[p].next[c] = cur;
p = st[p].link;
}
if (p == -1) {
st[cur].link = 0;
} else {
int q = st[p].next[c];
if (st[q].len == st[p].len+1) {
st[cur].link = q;
} else {
int clone = sz++;
st[clone].link = st[q].link;
memcpy(st[clone].next, st[q].next, sizeof(st[clone].next));
st[clone].len = st[p].len+1;
while (p != -1 && st[p].next[c] == q) {
st[p].next[c] = clone;
p = st[p].link;
}
st[q].link = st[cur].link = clone;
}
}
last = cur;
}
char s[maxn];
typedef long long ll;
ll ans;
void dfs(int u) {
for (auto v: son[u]) {
dfs(v);
st[u].endpos += st[v].endpos;
}
if (st[u].endpos >= 2) {
ans = max(ans, (ll)st[u].len*st[u].endpos);
}
}
int main() {
scanf("%s", s);
int len = strlen(s);
sam_init();
for (int i = 0; i < len; ++i) {
sam_extend(s[i]-'a');
}
for (int i = 1; i < sz; ++i) {
son[st[i].link].push_back(i);
}
dfs(0);
printf("%lld\n", ans);
}
P3809 【模板】后缀排序 (SA)
a.cpp Accepted
约定
- sa[i]表示排名为i的后缀的位置;
- rak[i]表示i后缀的排名;
- sec[i]表示第二关键字的大小,意义与sa一样;
- t为桶排使用的数组。
桶排
-
注意最后一个循环中的数组嵌套的意义!
int t[maxn];
void sort() {
memset(t, 0, sizeof(t));
for (int i = 1; i <= n; ++i) t[rak[i]]++;
for (int i = 1; i <= m; ++i) t[i] += t[i-1];
for (int i = n; i >= 1; --i) sa[t[rak[sec[i]]]--] = sec[i];
}
代码
#include <cstdio>
#include <cstring>
#define cmp(i, j, w) (sec[i] == sec[j] && sec[i+w] == sec[j+w])
const int maxn = 1e6+10;
int n, sa[maxn], rak[maxn], sec[maxn], m = 200;
char s[maxn];
namespace my{
int t[maxn];
void sort() {
memset(t, 0, sizeof(t));
for (int i = 1; i <= n; ++i) t[rak[i]]++;
for (int i = 1; i <= m; ++i) t[i] += t[i-1];
for (int i = n; i >= 1; --i) sa[t[rak[sec[i]]]--] = sec[i];
}
}
using my::sort;
int main() {
scanf("%s", s+1);
n = strlen(s+1);
for (int i = 1; i <= n; ++i) {
rak[i] = s[i];
sec[i] = i;
}
sort();
for (int w = 1, p = 1; p < n; w<<=1, m=p) {
for (p = 1; p <= w; ++p) sec[p] = n-w+p;
for (int i = 1; i <= n; ++i) if (sa[i] > w) sec[p++] = sa[i]-w;
sort();
memcpy(sec, rak, sizeof(sec));
rak[sa[1]] = p = 1;
for (int i = 2; i <= n; ++i) {
rak[sa[i]] = (cmp(sa[i], sa[i-1], w) ? p : ++p);
}
}
for (int i = 1; i <= n; ++i) {
printf("%d ", sa[i]);
}
}