后缀自动机小结

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]Llen[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](vu)
最后 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 rminj的答案。

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;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值