文章目录
SAM
一、SAM的性质:
S
A
M
SAM
SAM是个状态机。一个起点,若干终点。原串的所有子串和从
S
A
M
SAM
SAM起点开始的所有路径一一对应,不重不漏。所以终点就是包含后缀的点。
每个点包含若干子串,每个子串都一一对应一条从起点到该点的路径。且这些子串一定是里面最长子串的连续后缀。
S
A
M
SAM
SAM问题中经常考虑两种边:
(
1
)
(1)
(1) 普通边,类似于
T
r
i
e
Trie
Trie。表示在某个状态所表示的所有子串的后面添加一个字符。
(
2
)
(2)
(2)
L
i
n
k
、
F
a
t
h
e
r
Link、Father
Link、Father。表示将某个状态所表示的最短子串的首字母删除。这类边构成一棵树。
二、SAM的构造思路
e n d p o s ( s ) endpos(s) endpos(s):子串s所有出现的位置(尾字母下标)集合。 S A M SAM SAM中的每个状态都一一对应一个 e n d p o s endpos endpos的等价类。
e
n
d
p
o
s
endpos
endpos的性质:
(
1
)
(1)
(1) 令
s
1
,
s
2
s_1,s_2
s1,s2 为
S
S
S 的两个子串 ,不妨设
∣
s
1
∣
≤
∣
s
2
∣
|s_1|≤|s_2|
∣s1∣≤∣s2∣ (我们用
∣
s
∣
|s|
∣s∣ 表示
s
s
s 的长度 ,此处等价于
s
1
s_1
s1 不长于
s
2
s_2
s2 )。则
s
1
s_1
s1 是
s
2
s_2
s2 的后缀当且仅当
e
n
d
p
o
s
(
s
1
)
⊇
e
n
d
p
o
s
(
s
2
)
endpos(s1)⊇endpos(s2)
endpos(s1)⊇endpos(s2) ,
s
1
s_1
s1 不是
s
2
s_2
s2 的后缀当且仅当
e
n
d
p
o
s
(
s
1
)
∩
e
n
d
p
o
s
(
s
2
)
=
∅
endpos(s1)∩endpos(s2)=∅
endpos(s1)∩endpos(s2)=∅ 。
(
2
)
(2)
(2)两个不同子串的
e
n
d
p
o
s
endpos
endpos,要么有包含关系,要么没有交集。
(
3
)
(3)
(3) 两个子串的
e
n
d
p
o
s
endpos
endpos相同,那么短串为长串的后缀。
(
4
)
(4)
(4) 对于一个状态
s
t
st
st ,以及任意的
l
o
n
g
e
s
t
(
s
t
)
longest(st)
longest(st) 的后缀
s
s
s ,如果
s
s
s 的长度满足:
∣
s
h
o
r
t
e
s
t
(
s
t
)
∣
≤
∣
s
∣
≤
∣
l
o
n
g
s
e
s
t
(
s
t
)
∣
|shortest(st)|≤|s|≤|longsest(st)|
∣shortest(st)∣≤∣s∣≤∣longsest(st)∣ ,那么
s
∈
s
u
b
s
t
r
i
n
g
s
(
s
t
)
s∈substrings(st)
s∈substrings(st) 。
三、SAM的构造过程
分类讨论,具体看板书。
证明较为复杂,略。
四、SAM时间复杂度
线性。
证明较为复杂,略。
作者:yxc
链接:https://www.acwing.com/activity/content/code/content/585844/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
AcWing 2766. 后缀自动机
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f
using namespace std;
typedef long long LL;
const int N = 2e6 + 10;
char str[N];
struct Node
{
int len, fa; //len该点表示的集合内最长串的长度,fa最短串去掉首字母后的子串所在状态
int ch[26];
} node[N];
int tot = 1, last = 1;
LL cnt[N], ans; //cnt[i] 状态i表示的endpos(i)的元素个数
void extend(int c)
{
int p = last, np = last = ++ tot;
cnt[tot] = 1;//包含原串前缀的状态的cnt=1
node[np].len = node[p].len + 1;
for( ; p && !node[p].ch[c]; p = node[p].fa) node[p].ch[c] = np;
if(!p) node[np].fa = 1;
else
{
int q = node[p].ch[c];
if(node[q].len == node[p].len + 1) node[np].fa = q;
else
{
int nq = ++ tot;
node[nq] = node[q], node[nq].len = node[p].len + 1;
node[q].fa = node[np].fa = nq;
for( ; p && node[p].ch[c] == q; p = node[p].fa) node[p].ch[c] = nq;
}
}
}
int idx, head[N], e[N], ne[N];
void add(int u, int v)
{
e[++ idx] = v, ne[idx] = head[u], head[u] = idx;
}
void dfs(int u)
{
for(int i = head[u]; i; i = ne[i])
{
dfs(e[i]);
cnt[u] += cnt[e[i]];
}
if(cnt[u] > 1) ans = max(ans, cnt[u] * node[u].len);
}
int main()
{
#ifdef LOCAL
freopen("in.in", "r", stdin);
freopen("out.out", "w", stdout);
#endif
cin >> str;
//逐个插入,构建str的后缀自动机
for(int i = 0; str[i]; i ++) extend(str[i] - 'a');
for(int i = 2; i <= tot; i ++) //构建parent树,1点没有父节点
add(node[i].fa, i);
dfs(1); //递归求cnt
cout << ans << endl;
return 0;
}
AcWing 1283. 玄武密码
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f
using namespace std;
typedef long long LL;
const int N = 1e7 + 10;
int n, m;
char str[N];
struct Node
{
int len, fa;
int ch[4];
} node[N << 1];
int tot = 1, last = 1;
int get(char ch)
{
if(ch == 'E') return 0;
if(ch == 'S') return 1;
if(ch == 'W') return 2;
return 3;
}
void extend(int c)
{
int p = last, np = last = ++ tot;
node[np].len = node[p].len + 1;
for(; p && !node[p].ch[c]; p = node[p].fa) node[p].ch[c] = np;
if(!p) node[np].fa = 1;
else
{
int q = node[p].ch[c];
if(node[q].len == node[p].len + 1) node[np].fa = q;
else
{
int nq = ++ tot;
node[nq] = node[q], node[nq].len = node[p].len + 1;
node[q].fa = node[np].fa = nq;
for(; p && node[p].ch[c] == q; p = node[p].fa) node[p].ch[c] = nq;
}
}
}
int main()
{
#ifdef LOCAL
freopen("in.in", "r", stdin);
freopen("out.out", "w", stdout);
#endif
scanf("%d%d", &n, &m);
scanf("%s", str);
for(int i = 0; i < n; i ++) extend(get(str[i]));
while(m --)
{
scanf("%s", str);
int ans = 0, p = 1;
for(int i = 0; str[i]; i ++) //从串的前缀沿着边走就行了,类似Tire
{
int c = get(str[i]);
if(node[p].ch[c]) p = node[p].ch[c], ans ++;
else break;
}
printf("%d\n", ans);
}
return 0;
}
AcWing 2811. 最长公共子串
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f
using namespace std;
typedef long long LL;
const int N = 1e4 + 10, M = 2e4 + 10;
int n;
char str[N];
struct Node
{
int len, fa;
int ch[26];
} node[M];
int tot = 1, last = 1;
int now[M], ans[M];
//now[i]当前串在i这个状态集合里面匹配的最大长度
void extend(int c) //模版背过
{
int p = last, np = last = ++ tot;
node[np].len = node[p].len + 1;
for(; p && !node[p].ch[c]; p = node[p].fa) node[p].ch[c] = np;
if(!p) node[np].fa = 1;
else
{
int q = node[p].ch[c];
if(node[q].len == node[p].len + 1) node[np].fa = q;
else
{
int nq = ++ tot;
node[nq] = node[q], node[nq].len = node[p].len + 1;
node[q].fa = node[np].fa = nq;
for(; p && node[p].ch[c] == q; p = node[p].fa) node[p].ch[c] = nq;
}
}
}
int idx, head[M], e[M], ne[M];
void add(int u, int v)
{
e[++ idx] = v, ne[idx] = head[u], head[u] = idx;
}
void dfs(int u)
{
for(int i = head[u]; i; i = ne[i])
{
dfs(e[i]);
now[u] = max(now[u], min(now[e[i]], node[u].len));
}
}
int main()
{
#ifdef LOCAL
freopen("in.in", "r", stdin);
freopen("out.out", "w", stdout);
#endif
scanf("%d%s", &n, str);
for(int i = 0; str[i]; i ++) extend(str[i] - 'a'); //构建第一个串的后缀自动机
for(int i = 2; i <= tot; i ++) add(node[i].fa, i);
memset(ans, 0x3f, sizeof ans);
for(int i = 2; i <= n; i ++) //用每一个串与第一个串匹配
{
scanf("%s", str);
memset(now, 0, sizeof now);
int t = 0, p = 1; //当前匹配长度, 当前在后缀自动机中的状态
for(int j = 0; str[j]; j ++)
{
int c = str[j] - 'a';
while(p > 1 && !node[p].ch[c]) p = node[p].fa, t = node[p].len;
//如果当前状态没有c的出边,则沿着fa边向上走
//直到p存在c的出边或走到了空
if(node[p].ch[c]) p = node[p].ch[c], t ++; //如果是p存在c边的情况,则匹配长度+1
now[p] = max(now[p], t);
}
dfs(1); //向上传递一下now,更新fa的now
for(int j = 1; j <= tot; j ++) ans[j] = min(ans[j], now[j]); //对于每一个串匹配的now,取一个min
}
int res = 0;
for(int i = 1; i <= tot; i ++) res = max(res, ans[i]); //在所有的状态中取一个max
printf("%d\n", res);
return 0;
}
P4070 [SDOI2016]生成魔咒
每插入一个魔咒字符
x
x
x(注意,在这里面
12
12
12相当于是一个字符,被坑惨了),求当前字符串
S
S
S本质不同的子串数量。
在这里,每一个状态可能有的出边有
1
e
9
1e9
1e9种,肯定不能开一个
2
∗
1
e
5
∗
1
e
9
2*1e5*1e9
2∗1e5∗1e9的数组。但是能用到的出边却不会很多,所以结构体里面的
c
h
ch
ch数组可以开成一个
m
a
p
<
i
n
t
,
i
n
t
>
c
h
map<int,int> ch
map<int,int>ch
还有,每加入一个字符,新增的子串个数就是新建出来的节点 n p np np的贡献, n q nq nq对答案是没有贡献的。
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f
using namespace std;
typedef long long LL;
const int N = 2e5 + 10;
struct Node
{
int len, fa;
map<int, int> ch;
} node[N];
int tot = 1, last = 1;
void extend(int c)
{
int p = last, np = last = ++ tot;
node[np].len = node[p].len + 1;
for(; p && !node[p].ch[c]; p = node[p].fa) node[p].ch[c] = np;
if(!p) node[np].fa = 1;
else
{
int q = node[p].ch[c];
if(node[q].len == node[p].len + 1) node[np].fa = q;
else
{
int nq = ++ tot;
node[nq] = node[q], node[nq].len = node[p].len + 1;
node[q].fa = node[np].fa = nq;
for(; p && node[p].ch[c] == q; p = node[p].fa) node[p].ch[c] = nq;
}
}
}
int main()
{
#ifdef LOCAL
freopen("in.in", "r", stdin);
freopen("out.out", "w", stdout);
#endif
int n; LL ans = 0;
char str[15];
scanf("%d", &n);
while(n --)
{
int x; scanf("%d", &x);
extend(x);
ans += node[last].len - node[node[last].fa].len;
printf("%lld\n", ans);
}
return 0;
}
字典序第k小子串
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f
using namespace std;
typedef long long LL;
const int N = 1e5 + 10, M = N * 2;
int n, m;
char str[N];
struct Node
{
int len, fa;
int ch[26];
} node[M];
int tot = 1, last = 1;
void extend(int c)
{
int p = last, np = last = ++ tot;
node[np].len = node[p].len + 1;
for(; p && !node[p].ch[c]; p = node[p].fa) node[p].ch[c] = np;
if(!p) node[np].fa = 1;
else
{
int q = node[p].ch[c];
if(node[q].len == node[p].len + 1) node[np].fa = q;
else
{
int nq = ++ tot;
node[nq] = node[q], node[nq].len = node[p].len + 1;
node[q].fa = node[np].fa = nq;
for(; p && node[p].ch[c] == q; p = node[p].fa) node[p].ch[c] = nq;
}
}
}
int to[M]; //to[i]:从状态i的路径条数
int dfs(int u)
{
if(to[u]) return to[u]; //防止重复计算
to[u] = 1;
for(int i = 0; i < 26; i ++)
{
int v = node[u].ch[i];
if(!v) continue;
to[u] += dfs(v);
}
return to[u];
}
void find(int k) //找第k小子串
{
int p = 1; //从root开始
while(k)
{
if(!k) break;
for(int i = 0; i < 26; i ++) //从小到大,如果是求第k大,可以从大到小
{
int q = node[p].ch[i];
if(!q) continue;
if(k > to[q]) k -= to[q];
else
{
putchar(i + 'a'); //边找边输出
p = q; k --; //这里k一定要-1
break;
}
}
}
puts("");
}
int main()
{
#ifdef LOCAL
freopen("in.in", "r", stdin);
freopen("out.out", "w", stdout);
#endif
scanf("%s%d", str, &m);
for(int i = 0; str[i]; i ++) extend(str[i] - 'a');
dfs(1);
while(m --)
{
int k; scanf("%d", &k);
find(k);
}
return 0;
}
P3975 [TJOI2015]弦论
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f
using namespace std;
typedef long long LL;
const int N = 5e5 + 10, M = 1e6 + 10;
int n, m, t, k;
char str[N];
struct Node
{
int len, fa;
int ch[26];
} node[M];
int tot = 1, last = 1;
int sz[M]; //sz[i]:endpos(i)集合的大小,(出现次数)
LL sum[M]; //sum[i]:点i能够到达的本质不同的字符串个数
void extend(int c)
{
int p = last, np = last = ++ tot;
sz[np] = 1;
node[np].len = node[p].len + 1;
for(; p && !node[p].ch[c]; p = node[p].fa) node[p].ch[c] = np;
if(!p) node[np].fa = 1;
else
{
int q = node[p].ch[c];
if(node[q].len == node[p].len + 1) node[np].fa = q;
else
{
int nq = ++ tot;
node[nq] = node[q], node[nq].len = node[p].len + 1;
node[q].fa = node[np].fa = nq;
for(; p && node[p].ch[c] == q; p = node[p].fa) node[p].ch[c] = nq;
}
}
}
int idx, head[M], e[M], ne[M];
void add(int u, int v)
{
e[++ idx] = v, ne[idx] = head[u], head[u] = idx;
}
void get_size(int u) //求sz
{
for(int i = head[u]; i; i = ne[i])
{
int v = e[i];
get_size(v);
sz[u] += sz[v];
}
}
bool vis[M];
void dfs(int u) //求sum
{
if(vis[u]) return ;
vis[u] = true;
for(int i = 0; i < 26; i ++)
{
int v = node[u].ch[i];
dfs(v);
sum[u] += sum[v];
}
}
void pr(int k)
{
int p = 1;
while(k)
{
for(int i = 0; i < 26; i ++)
{
int q = node[p].ch[i];
if(!q) continue;
if(k > sum[q]) k -= sum[q];
else
{
putchar(i + 'a');
p = q, k -= sz[p]; //这里减去的是sz[p]
break;
}
}
}
putchar('\n');
}
int main()
{
#ifdef LOCAL
freopen("in.in", "r", stdin);
freopen("out.out", "w", stdout);
#endif
scanf("%s", str);
for(int i = 0; str[i]; i ++) extend(str[i] - 'a');
scanf("%d%d", &t, &k);
for(int i = 2; i <= tot; i ++) add(node[i].fa, i);
get_size(1);
for(int i = 1; i <= tot; i ++)
t ? (sum[i] = sz[i]) : (sum[i] = sz[i] = 1);
sz[1] = sum[1] = 0; //去掉sz[1]代表的空串
dfs(1);
if(sum[1] < k) puts("-1");
else pr(k);
return 0;
}
exSAM
S A M SAM SAM是构建一个字符串的后缀自动机, e x S A M exSAM exSAM是将多个串构建在一个后缀自动机中。
P6139 【模板】广义后缀自动机(广义 SAM)
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f
using namespace std;
typedef long long LL;
const int N = 2e6 + 10, MOD = 9982443553;
int n, m;
char str[N];
struct Node
{
int len, fa;
int ch[26];
} node[N];
int tot = 1, last = 1;
void extend(int c)
{
if(node[last].ch[c])//此处新增代码就是在线版的广义后缀自动机的插入
{
int p = last, q = node[p].ch[c];
if(node[q].len == node[p].len + 1) last = q;
else
{
int nq = ++ tot;
node[nq] = node[q], node[nq].len = node[p].len + 1;
for(; p && node[p].ch[c] == q; p = node[p].fa) node[p].ch[c] = nq;
node[q].fa = last = nq;
}
return ;
}
//以下代码与后缀自动机的插入完全一致
int p = last, np = last = ++ tot;
node[np].len = node[p].len + 1;
for(; p && !node[p].ch[c]; p = node[p].fa) node[p].ch[c] = np;
if(!p) node[np].fa = 1;
else
{
int q = node[p].ch[c];
if(node[q].len == node[p].len + 1) node[np].fa = q;
else
{
int nq = ++ tot;
node[nq] = node[q], node[nq].len = node[p].len + 1;
node[q].fa = node[np].fa = nq;
for(; p && node[p].ch[c] == q; p = node[p].fa) node[p].ch[c] = nq;
}
}
}
int main()
{
#ifdef LOCAL
freopen("in.in", "r", stdin);
freopen("out.out", "w", stdout);
#endif
scanf("%d", &n);
for(int i = 0; i < n; i ++)
{
scanf("%s", str); last = 1; //每次新插入一个串就将last置成起点
for(int j = 0; str[j]; j ++) extend(str[j] - 'a');
}
LL ans = 0;
for(int i = 2; i <= tot; i ++)
ans += node[i].len - node[node[i].fa].len;
printf("%lld\n", ans);
return 0;
}