SAM 小结
1. 定义
对给定字符串 s s s,其后缀自动机是一个最小化确定有限状态自动机,它能够接收字符串 s s s的所有后缀。
2.存在性问题
对于模式串 T T T,查询串 S S S为 T T T的子串当且仅当存在从初始节点出发,在 T T T 的 S A M SAM SAM上沿着 S S S的字符逐次转移生成的合法路径。
3.计数相关
对于任一状态, r i g h t right right集合的大小对应该状态子串的出现次数,可以花式维护 r i g h t right right 集合的 r m a x / r m i n / r i g h t s i z e r_{max}/r_{min}/right_{size} rmax/rmin/rightsize,某些题目可能需要用平衡树维护完整的 r i g h t right right集合。
对于任一状态 s s s,其表示的子串长度 L L L为 l e n [ s ] ≤ L ≤ l e n [ s u f [ s ] ] + 1 len[s]\le L \le len[suf[s]]+1 len[s]≤L≤len[suf[s]]+1。
4.方便的性质
对于串 S S S构建出的 p a r e n t parent parent树/ s u f suf suf链上祖先节点的 r i g h t right right集合为子节点的并集,且祖先的 l e n len len严格小于子节点的 l e n len len。实际上不同树链上的节点之间并不构成影响,所以我们只需要用基数排序(显然 l e n ∈ [ 1 , s t r l e n ( S ) ] len\in[1,strlen(S)] len∈[1,strlen(S)] )得到一个拓扑序,就可以从后往前更新,不遗漏不重复地维护我们需要的信息。
5.一些模板题
-
定义:
r [ i ] r[i] r[i] 为结点 i i i的 r i g h t right right集合的最小值/最大值/大小,依题目而定
l e n [ i ] len[i] len[i] 为结点 i i i 的后缀最大长度
s u f [ i ] suf[i] suf[i] 为结点 i i i 的 p a r e n t parent parent树/ s u f suf suf链上的父亲 -
POJ 1509
sol:求最小表示法下标。
将原串拷贝之后拼接在后面。在新串上建自动机,贪心地选择字典序最小的转移,走 l e n ( s ) len(s) len(s)步即可。
code:
//#include<bits/stdc++.h>
#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
const int maxn = 2e5+10;
const int s_sz = 26;
char str[maxn];
struct SAM{
int len[maxn],suf[maxn],r[maxn];
int ch[maxn][s_sz];
int S,sz,last;
void init(){
memset(ch,0,sizeof(ch[0]) * (sz+1));
memset(suf,0,sizeof(int)*(sz+1));
S = sz = last = 1;
}
void add(int x,int c){
int p = last,np = ++sz;
last = np;
len[np] = x;
while(p && !ch[p][c]){
ch[p][c] = np;
p = suf[p];
}
if(!p){
suf[np] = S;
return;
}
int q = ch[p][c];
if(len[q] == len[p] + 1) suf[np] = q;
else{
int nq = ++ sz;
len[nq] = len[p] + 1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
suf[nq] = suf[q];
suf[np] = suf[q] = nq;
while(ch[p][c] == q){
ch[p][c] = nq;
p = suf[p];
}
}
}
inline int idx(char c){
return c - 'a';
}
void build(char* s){
int len = strlen(s);
for(int i = 0;i<len;i++){
add(i+1,idx(s[i]));
}
}
}sam;
int main(){
int T;
scanf("%d",&T);
while(T--){
sam.init();
scanf("%s",str);
int len = strlen(str);
for(int i = len;i<2*len;i++){
str[i] = str[i-len];
}
str[len<<1] = '\0';
sam.build(str);
int ans = 0;
int now = sam.S;
for(int i = 1;i<=len;i++){
for(int j = 0;j<s_sz;j++){
if(sam.ch[now][j]!=0){
now = sam.ch[now][j];
break;
}
}
}
ans = sam.len[now] - len + 1;
printf("%d\n",ans);
}
return 0;
}
- SPOJ NSUBSTR
sol: 求每种长度子串出现次数的最大值。
对串 S S S建 S A M SAM SAM,维护出每个状态 r i g h t right right集合的大小。对于状态sta,更新 f ( l e n [ s t a ] ) = m a x ( f ( l e n [ s t a ] , r [ s t a ] ) f(len[sta]) = max(f(len[sta],r[sta]) f(len[sta])=max(f(len[sta],r[sta]),这里 r [ s t a ] r[sta] r[sta]指sta的 r i g h t right right集合大小。显然若以 r r r结尾的串出现 n n n次,则以 r r r结尾的更短的串必定同样出现 n n n次。按 l e n len len逆序更新即可。
code:
//#include<bits/stdc++.h>
#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<cstring>
#include<assert.h>
using namespace std;
typedef long long ll;
const int maxn = 5e5+5;
const int s_sz = 26;
char str[maxn];
int sum[maxn],tmp[maxn],f[maxn];
struct SAM{
int ch[maxn][s_sz];
int rt,sz,last;
int len[maxn],suf[maxn],r[maxn];
void init(){
memset(ch,0,sizeof(ch[0]) * (sz+1));
memset(suf,0,sizeof(int)*(sz+1));
memset(r,0,sizeof(int)*(sz+1));
rt = sz = last = 1;
}
inline void add(int x,int c){
int p = last,np = ++sz;
last = np;
len[np] = x;
while(p && !ch[p][c]){
ch[p][c] = np;
p = suf[p];
}
if(!p){
suf[np] = rt;
return;
}
int q = ch[p][c];
if(len[q] == len[p] + 1) suf[np] = q;
else{
int nq = ++ sz;
len[nq] = len[p] + 1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
suf[nq] = suf[q];
suf[np] = suf[q] = nq;
while(ch[p][c] == q){
ch[p][c] = nq;
p = suf[p];
}
}
}
inline int idx(char c){
return c - 'a';
}
inline void build(char* s){
int n = strlen(s);
for(int i = 0;i<n;i++){
add(i+1,idx(s[i]));
}
}
inline void work(char* s){
int u = rt;
int n = strlen(s);
for(int i = 0;i<n;i++){
int c = idx(s[i]);
u = ch[u][c];
r[u]++;
}
}
inline void Topsort(int n){
memset(sum,0,sizeof(int)*(n+1));
for(int i = 1;i<=sz;i++) sum[len[i]] ++ ;
for(int i = 1;i<=n;i++) sum[i] += sum[i-1];
for(int i = 1;i<=sz;i++) tmp[sum[len[i]]--] = i;
}
inline void get_right(){
for(int i = sz;i;i--){
int u = tmp[i];
if(suf[u]) r[suf[u]] += r[u];
}
}
}sam;
int main(){
scanf("%s",str);
sam.init();
sam.build(str);
int n = strlen(str);
sam.work(str);
sam.Topsort(n);
sam.get_right();
for(int i = 1;i<=sam.sz;i++){
int l = sam.len[i];
f[l] = max(f[l],sam.r[i]);
}
for(int i = n-1;i;i--) f[i] = max(f[i],f[i+1]);
for(int i = 1;i<=n;i++){
printf("%d\n",f[i]);
}
return 0;
}
- SPOJ LCS2
sol: 求多个串的最长公共子串。
对第一个串建后缀自动机,剩余串在自动机上转移。对于状态 s t a sta sta,维护每个串转移到该状态时的长度,取最小值(所有串都必须匹配,所以取最小值),对所有状态取最大值即可。考虑对于字符 c c c不存在合法转移的情况:沿 p a r e n t parent parent树不断向上走,直到遇到存在 c c c转移的结点 p p p或到达初始结点 r t rt rt。若走到结点 p p p,当前最大可能的 l c s lcs lcs长度为 l e n [ p ] len[p] len[p],转移到 c h [ p ] [ c ] , l e n [ p ] + 1 ch[p][c],len[p]+1 ch[p][c],len[p]+1即可。
code:
//#include<bits/stdc++.h>
#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<cstring>
#include<assert.h>
using namespace std;
typedef long long ll;
const int maxn = 2e5+5;
const int s_sz = 26;
char str[maxn];
int ans[maxn],sum[maxn],tmp[maxn];
struct SAM{
int ch[maxn][s_sz];
int rt,sz,last;
int len[maxn],suf[maxn];
int maxs[maxn];
void init(){
memset(ch,0,sizeof(ch[0]) * (sz+1));
memset(suf,0,sizeof(int)*(sz+1));
rt = sz = last = 1;
}
inline void add(int x,int c){
int p = last,np = ++sz;
last = np;
len[np] = x;
while(p && !ch[p][c]){
ch[p][c] = np;
p = suf[p];
}
if(!p){
suf[np] = rt;
return;
}
int q = ch[p][c];
if(len[q] == len[p] + 1) suf[np] = q;
else{
int nq = ++ sz;
len[nq] = len[p] + 1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
suf[nq] = suf[q];
suf[np] = suf[q] = nq;
while(ch[p][c] == q){
ch[p][c] = nq;
p = suf[p];
}
}
}
inline int idx(char c){
return c - 'a';
}
inline void build(char* s){
int n = strlen(s);
for(int i = 0;i<n;i++){
add(i+1,idx(s[i]));
}
for(int i = 0;i<=sz;i++){
ans[i] = len[i];
}
}
inline void work(char* s){
memset(maxs,0,sizeof(int)*(sz+1));
// memset(maxs,0,sizeof(maxs));
int u = rt;
int n = strlen(s);
int L = 0;
for(int i = 0;i<n;i++){
int c = idx(s[i]);
if(ch[u][c]){
L ++ ;
u = ch[u][c];
maxs[u] = max(L,maxs[u]);
continue;
}
while(u && !ch[u][c]) u = suf[u];
if(!u) u = rt,L = 0;
else{
L = len[u] + 1;
u = ch[u][c];
maxs[u] = max(L,maxs[u]);
}
}
}
inline void Topsort(int n){
memset(sum,0,sizeof(int)*(n+1));
for(int i = 1;i<=sz;i++) sum[len[i]] ++ ;
for(int i = 1;i<=n;i++) sum[i] += sum[i-1];
for(int i = 1;i<=sz;i++) tmp[sum[len[i]]--] = i;
}
inline void update(){
for(int i = sz;i;i--){
int u = tmp[i];
if(u && suf[u]) maxs[suf[u]] = max(maxs[suf[u]],maxs[u]);
}
for(int i = 1;i<=sz;i++){
ans[i] = min(ans[i],maxs[i]);
}
}
}sam;
int main(){
int cnt = 0;
scanf("%s",str);
sam.init();
sam.build(str);
while(~scanf("%s",str)){
sam.work(str);
sam.Topsort(strlen(str));
sam.update();
}
int res = 0;
for(int i = 1;i<=sam.sz;i++){
res = max(res,ans[i]);
}
printf("%d\n",res);
return 0;
}
- spoj SUBLEX
sol: 求第K小子串
如果可以得到以每个状态为前缀的方案个数,则可以递归求解。
d p [ u ] = 1 ( 空 串 ) + ∑ d p [ v ] ( v 为 u 可 以 合 法 转 移 的 状 态 ) dp[u] = 1 (空串)+ \sum dp[v] (v为u可以合法转移的状态) dp[u]=1(空串)+∑dp[v](v为u可以合法转移的状态)
最后 d p [ r t ] dp[rt] dp[rt]减一,因为不能有纯空串出现。
//#include<bits/stdc++.h>
#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<cstring>
#include<assert.h>
using namespace std;
typedef long long ll;
const int maxn = 2e5+5;
const int s_sz = 26;
char str[maxn],ans[maxn];
int sum[maxn],tmp[maxn],dp[maxn];
struct SAM{
int ch[maxn][s_sz];
int rt,sz,last;
int len[maxn],suf[maxn],r[maxn];
void init(){
memset(ch,0,sizeof(ch[0]) * (sz+1));
memset(suf,0,sizeof(int)*(sz+1));
memset(r,0,sizeof(int)*(sz+1));
rt = sz = last = 1;
}
inline void add(int x,int c){
int p = last,np = ++sz;
last = np;
len[np] = x;
while(p && !ch[p][c]){
ch[p][c] = np;
p = suf[p];
}
if(!p){
suf[np] = rt;
return;
}
int q = ch[p][c];
if(len[q] == len[p] + 1) suf[np] = q;
else{
int nq = ++ sz;
len[nq] = len[p] + 1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
suf[nq] = suf[q];
suf[np] = suf[q] = nq;
while(ch[p][c] == q){
ch[p][c] = nq;
p = suf[p];
}
}
}
inline int idx(char c){
return c - 'a';
}
inline void build(char* s){
int n = strlen(s);
for(int i = 0;i<n;i++){
add(i+1,idx(s[i]));
}
}
inline void Topsort(int n){
memset(sum,0,sizeof(int)*(n+1));
for(int i = 1;i<=sz;i++) sum[len[i]] ++ ;
for(int i = 1;i<=n;i++) sum[i] += sum[i-1];
for(int i = 1;i<=sz;i++) tmp[sum[len[i]]--] = i;
}
inline void sol(){
for(int i = sz;i;--i){
int u = tmp[i];
dp[u] = 1;
for(int i = 0;i<s_sz;i++){
dp[u] += dp[ch[u][i]];
}
}
dp[rt]--;
}
inline void work(int k){
int u = rt;
int cnt = 0;
int ret = k;
while(ret){
for(int i = 0;i<s_sz;i++){
if(!ch[u][i]) continue;
int v = ch[u][i];
if(dp[v]<ret) ret -= dp[v];
else {
ans[++cnt] = 'a' + i;
u = v;
break;
}
}
ret--;
}
ans[cnt+1] = '\0';
}
}sam;
int main(){
scanf("%s",str);
sam.init();
sam.build(str);
int n = strlen(str);
sam.Topsort(n);
sam.sol();
int q,k;
scanf("%d",&q);
while(q--){
scanf("%d",&k);
sam.work(k);
puts(ans+1);
}
return 0;
}
- HDU - 4622
sol: 给定串 s s s,求 [ l , r ] [l,r] [l,r]区间内本质不同的串的个数。
定义 S i S_i Si为以 s i s_i si为起点的后缀。考虑到 S S S长度不超过1000,暴力建出 S i ( i ∈ [ 1 , l e n ( s ) ] ) S_i (i\in [1,len(s)]) Si(i∈[1,len(s)]) 的后缀自动机。对于每个状态 s t a sta sta维护 r [ s t a ] r[sta] r[sta]为其 r i g h t right right集合的最小值,则子串在区间 [ l , r ] [l,r] [l,r]存在当且仅当其在 S l S_l Sl对应自动机上节点 u u u, r [ u ] ≤ r r[u] \le r r[u]≤r。初始时定义 d p [ i ] [ j ] dp[i][j] dp[i][j] 为 S i S_i Si对应的自动机上 r m i n = j r_{min} = j rmin=j的子串个数,从前往后转移即可得到 r m i n ≤ j r_{min} \le j rmin≤j的答案。
code:
// #include<bits/stdc++.h>
#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<cstring>
#include<assert.h>
#include<vector>
using namespace std;
typedef long long ll;
const int maxn = 4e3+5;
const int s_sz = 26;
const int inf = 0x3f3f3f3f;
#define fi first
#define se second
#define MP make_pair
#define pii pair<int,int>
char str[maxn];
int sum[maxn],tmp[maxn];
ll dp[maxn>>1][maxn>>1];
bool vis[maxn];
struct SAM{
int ch[maxn][s_sz];
int rt,sz,last;
int len[maxn],suf[maxn],r[maxn];
void init(){
memset(ch,0,sizeof(ch[0]) * (sz+1));
memset(suf,0,sizeof(int)*(sz+1));
memset(r,0,sizeof(int)*(sz+1));
rt = sz = last = 1;
}
inline void add(int x,int c){
int p = last,np = ++sz;
last = np;
len[np] = x;
while(p && !ch[p][c]){
ch[p][c] = np;
p = suf[p];
}
if(!p){
suf[np] = rt;
return;
}
int q = ch[p][c];
if(len[q] == len[p] + 1) suf[np] = q;
else{
int nq = ++ sz;
len[nq] = len[p] + 1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
suf[nq] = suf[q];
suf[np] = suf[q] = nq;
while(ch[p][c] == q){
ch[p][c] = nq;
p = suf[p];
}
}
}
inline int idx(char c){
return c - 'a';
}
inline void build(char* s){
int n = strlen(s);
for(int i = 0;i<n;i++){
add(i+1,idx(s[i]));
}
}
inline void Topsort(int n){
memset(sum,0,sizeof(int)*(n+1));
for(int i = 1;i<=sz;i++) sum[len[i]] ++ ;
for(int i = 1;i<=n;i++) sum[i] += sum[i-1];
for(int i = 1;i<=sz;i++) tmp[sum[len[i]]--] = i;
}
void get_right(char* s){
int u = rt;
int n = strlen(s);
memset(r,inf,sizeof(int)*(sz+1));
// cout<<s<<endl;
for(int i = 0;i < n;i++){
u = ch[u][idx(s[i])];
r[u] = i+1;
}
for(int i = sz;i;i--){
int u = tmp[i];
r[suf[u]] = min(r[suf[u]],r[u]);
}
}
void sol(int L,int n){
// cout<<"L = "<<L<<' '<<n<<endl;
for(int i = 1;i<=sz;i++){
if(r[i] != inf){
dp[L][r[i]] += len[i] - len[suf[i]];
}
}
for(int i = 1;i<=n;i++){
dp[L][i] += dp[L][i-1];
}
}
}sam;
int main(){
int T;
scanf("%d",&T);
while(T--){
scanf("%s",str);
int n = strlen(str);
memset(vis,0,sizeof(bool)*(n+1));
memset(dp,0,sizeof(dp[0])*(n+1));
int q;
scanf("%d",&q);
while(q--){
int l,r;
scanf("%d%d",&l,&r);
if(!vis[l]){
sam.init();
sam.build(str+l-1);
sam.Topsort(n-l+1);
sam.get_right(str+l-1);
sam.sol(l,n-l+1);
}
vis[l] = true;
printf("%lld\n",dp[l][r-l+1]);
}
}
return 0;
}