SAM 后缀自动机,类似 SA 的字符串处理工具。
SAM 是一个有向无环图,图上从 Root 出发的每一条路径表示字符串s的一个后缀,每一条路径表示一个字串。
定义对于每一个节点:
len: 表示该 Root 到该节点的最长路径长度(该状态所能接受的最长字串)
min = p->pre->len+1:该状态所能接受的最短字串
nxt[alpha]:该节点的有向连边
pre:可理解为 AC自动机 的 fail 指针
right:表示这个状态在字符串中出现了多少次
len - pre.len:从Root走到该状态的方案数
且 ∑toti=1right[i]∗(len[i]−len[pre[i]])=n×(n+1)2 ∑ i = 1 t o t r i g h t [ i ] ∗ ( l e n [ i ] − l e n [ p r e [ i ] ] ) = n × ( n + 1 ) 2 (一个没有任何用的结论)
另外,SAM保证节点数<2n,边数<4n(可证明)
构造(从前往后逐字插入):
void extend(int c) {
int now = ++sz,p = last; st[now].len = st[last].len + 1;
for (;p!=-1 && !st[p].nxt[c];p=st[p].pre) st[p].nxt[c] = now;
if (p == -1) st[now].pre = 0; else {
int q = st[p].nxt[c];
if (st[p].len+1 == st[q].len) st[now].pre = q; else {
int clone = ++sz;
st[clone] = st[q]; st[clone].len = st[p].len + 1;
for (;p!=-1 && st[p].nxt[c] == q;p=st[p].pre) st[p].nxt[c] = clone;
st[now].pre = st[q].pre = clone;
}
} last = now;
}
GetRight:
void GetRight() {
static int t[N], que[N];
memset(t,0,sizeof(t)); memset(right,0,sizeof(right));
for (int i=0;i<=sz;i++) ++t[st[i].len];
for (int i=1;i<=L;i++) t[i] += t[i-1];
//对所有状态按len排序
for (int i=0,p=0;i'a']] = 1;
for (int i=sz;i>=0;i--) right[st[que[i]].pre] += right[que[i]];
//字结点向父结点贡献答案
right[0] = 0;
}
求 s 内出现大于 k 的字串:right
求 s 内至少出现2次的不重叠子串
for (int I=sz,i;I>=0;I--) {i = q[I];
L[st[i].pre] = min(L[st[i].pre],L[i]);
R[st[i].pre] = max(R[st[i].pre],R[i]);
ans = max(ans,min(R[i] - L[i], st[i].len));
}
可认为 SAM 就是用s的所有字串构造的 AC自动机,可以像 AC自动机 一样进行字符串匹配,用 pre指针 转跳
从 last 开始,通过 pre 转移可到达所有 end 状态
但其构造时间是 O(n)
广义SAM:
对于一颗trie树上的所有后缀构造的SAM
离线:先建出trie,然后bfs extend,extend 函数中增加 last 变量
//BZOJ 3277
#include
#define N 300300
using namespace std;
struct state {int nxt[26], len, pre;} st[N];
int n, k, sz, vis[N], tot[N], to[N][26];
char str[N], *s[N];
queueint ,int> > Q;
int extend(int last,int c) {
int now = ++sz, p = last; st[now].len = st[last].len + 1;
for (;p!=-1 && st[p].nxt[c] == 0;p=st[p].pre) st[p].nxt[c] = now;
if (p == -1) st[now].pre = 0; else {
int q = st[p].nxt[c];
if (st[q].len == st[p].len + 1) st[now].pre = q; else {
int clone = ++sz; st[clone] = st[q]; st[clone].len = st[p].len + 1;
for (;p!=-1 && st[p].nxt[c] == q;p=st[p].pre) st[p].nxt[c] = clone;
st[now].pre = st[q].pre = clone;
}
} return last = now;
}
char T[1000];
void debug(int u=0,int d=0) {
puts(T);
for (int i=0;i<26;i++) if (st[u].nxt[i]) {
T[d] = 'a' + i;
debug(st[u].nxt[i],d+1);
} T[d] = 0;
}
int main() {// freopen("_in.txt","r",stdin);
scanf("%d%d",&n,&k); s[1] = str; st[0].pre = -1;
for (int i=1;i<=n;i++) {
scanf("%s",s[i]); s[i+1] = s[i] + strlen(s[i]) + 1;
for (int j=0,p=0;s[i][j];j++) if (to[p][s[i][j]-'a']) p = to[p][s[i][j]-'a']; else p = to[p][s[i][j]-'a'] = ++sz;
} sz = 0;
for (Q.push(make_pair(0,0));!Q.empty();Q.pop()) {
pair<int,int> u = Q.front();
for (int i=0;i<26;i++) if (to[u.first][i]) Q.push(make_pair(to[u.first][i],extend(u.second,i)));
}
for (int i=1;i<=n;i++) for (int j=0,p=0;s[i][j];j++) {
p = st[p].nxt[s[i][j] - 'a'];
for (int q=p;q && vis[q] != i;q=st[q].pre) ++tot[q], vis[q] = i;
}
for (int i=1;i<=n;i++) {
int ans = 0;
for (int j=0,p=0;s[i][j];j++) {
p = st[p].nxt[s[i][j] - 'a'];
while (p && tot[p] < k) p = st[p].pre;
if (tot[p] >= k) ans += st[p].len;
} printf("%d ",ans);
}
}
在线(复杂度稍高):
void extend(int c) {
if (st[last].nxt[c]) {
int p = last, q = st[last].nxt[c];
if (st[q].len == st[p].len + 1) last = q; else {
int clone = ++sz; st[clone] = st[q]; st[clone].len = st[p].len + 1;
for (;p!=-1 && st[p].nxt[c] == q;p=st[p].pre) st[p].nxt[c] = clone;
last = st[q].pre = clone;
} return ;
}
int now = ++sz, p = last; st[now].len = st[last].len + 1;
for (;p!=-1 && st[p].nxt[c] == 0;p=st[p].pre) st[p].nxt[c] = now;
if (p == -1) st[now].pre = 0; else {
int q = st[p].nxt[c];
if (st[q].len == st[p].len + 1) st[now].pre = q; else {
int clone = ++sz; st[clone] = st[q]; st[clone].len = st[p].len + 1;
for (;p!=-1 && st[p].nxt[c] == q;p=st[p].pre) st[p].nxt[c] = clone;
st[now].pre = st[q].pre = clone;
}
} last = now;
}
可实现功能:
1. 判断一个字符串是否为s的字串或后缀
2. 构造后缀树(倒序 extend(),连接个点和该点的 pre指针
3. AC自动机
4. 计算每个点的 Right集合 的大小。