SAM总结(持续更新到放假)(好吧我已经放假了)

本文深入讲解SAM(Suffix Automaton)算法的应用,涵盖LCS、恐吓信、生成魔咒等经典问题,通过代码实例解析SAM在不同场景下的实现技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一.LCS(1e6)
最基础的SAM题,建第一个串的SAM,然后把第二个串放在上面跑的时候如果跑不动就回到root,不然就走下去。
Code:

#include<bits/stdc++.h>
const int N=2000005;
typedef long long ll;
using namespace std;
char s[N],t[N];
int n,len=0,ans=0;
struct SuffixAutoMaton{
    int last,tot;int ch[N<<1][26],fa[N<<1],l[N<<1];
    void ins(int c){
    	int p=last,np=++tot;last=np;l[np]=l[p]+1;
    	for(;p&&!ch[p][c];p=fa[p]) ch[p][c]=np;
    	if(!p) fa[np]=1;	
    	else{
    		int q=ch[p][c];
    		if(l[q]==l[p]+1) fa[np]=q;
    		else{
    			int nq=++tot;l[nq]=l[p]+1;
    			memcpy(ch[nq],ch[q],sizeof(ch[q]));
    			fa[nq]=fa[q];fa[q]=fa[np]=nq;
    			for(;ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
            }
        }
    }
    void build(){
        scanf("%s",s+1);scanf("%s",t+1);n=strlen(t+1);int len=strlen(s+1);
        last=tot=1;for(int i=1;i<=len;i++)ins(s[i]-'a');
    }
    void calc(){len=0;
    	int p=1;
        for(int i=1;i<=n;i++){
            int c=t[i]-'a';
            if(ch[p][c]) len++,p=ch[p][c];
            else{
                for(;p&&!ch[p][c];p=fa[p]);
                if(p) len=l[p]+1,p=ch[p][c];
                else len=0,p=1;
            }
            ans=max(ans,len);
        }
        cout<<ans<<endl;
    }
}sam;
int main(){
    sam.build();
    sam.calc();
}

二.恐吓信(usaco2011dec)
和LCS那个差不多,还要简单一点,回原点的时候ans+1
Code(略)

三.[SDOI2016]生成魔咒
几乎是模板题,插入一个字符会生成一个新Suffix和它的所有后缀,贡献为它到后缀链接的长度
Code:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline int read(){
	int res=0,f=1;char ch=getchar();
	while(!isdigit(ch)) {if(ch=='-') f=-f;ch=getchar();}
	while(isdigit(ch)) {res=(res<<3)+(res<<1)+(ch^48);ch=getchar();}
	return res*f;
}
const int N=2000005;
int n,add;
ll ans=0;
map<int,int>ch[N];
int fa[N],l[N],last,tot;
inline int calc(int x){return l[x]-l[fa[x]];}
void ins(int c){
	int flag=0;
	int p=last,np=++tot;last=np;l[np]=l[p]+1;
	for(;p&&!ch[p][c];p=fa[p]) ch[p][c]=np;
	if(!p) fa[np]=1,ans+=calc(np);
	else{
		int q=ch[p][c];
		if(l[q]==l[p]+1) fa[np]=q,ans+=calc(np);
		else{
			int nq=++tot;l[nq]=l[p]+1;
			ch[nq]=ch[q];
			fa[nq]=fa[q];ans+=calc(nq)-calc(q);
			fa[q]=fa[np]=nq;ans+=calc(np)+calc(q);
			for(;ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
		}
	}
}
void work(){
	tot=last=1;n=read();
	for(int i=1;i<=n;i++) {add=read();ins(add);cout<<ans<<'\n';}
}
int main(){
	work();
	return 0;
}

四.[ZJOI2015]诸神眷顾的幻想乡
广义后缀自动机,把trie树上的每个叶子节点(此题只有20个)来倒着插入SAM,然后统计答案就行
Code:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline int read(){
	int res=0,f=1;char ch=getchar();
	while(!isdigit(ch)) {if(ch=='-') f=-f;ch=getchar();}
	while(isdigit(ch)) {res=(res<<3)+(res<<1)+(ch^48);ch=getchar();}
	return res*f;
}
const int N=2000010;
char s[N];
int vis[N],head[N],nxt[N],cnt=0;
inline void add(int x,int y){vis[++cnt]=y,nxt[cnt]=head[x],head[x]=cnt;}
namespace SAM{
	int in[N],c[N];
	int n,m;
	int l[N],fa[N],ch[N][26],last,tot;
	inline int ins(int c,int last){
		int p=last,np=++tot;last=np;l[np]=l[p]+1;
		for(;p&&!ch[p][c];p=fa[p]) ch[p][c]=np;
		if(!p) fa[np]=1;
		else{
			int q=ch[p][c];
			if(l[q]==l[p]+1) fa[np]=q;
			else{
				int nq=++tot;l[nq]=l[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				fa[nq]=fa[q];fa[q]=fa[np]=nq;
				for(;ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
			}
		}
		return np;
	}
	void dfs(int x,int y,int l){
		int la=ins(c[x],l);
		for(int i=head[x];i;i=nxt[i]) if(vis[i]!=y) dfs(vis[i],x,la);
 	}
	void work(){
		n=read();m=read();tot=1;
    	for(int i=1;i<=n;i++) c[i]=read();
    	for(int i=1;i<=n-1;i++){
	    	int x=read(),y=read();
	        add(x,y);add(y,x);
	        in[x]++;in[y]++;
	    }
	    for(int i=1;i<=n;i++) if(in[i]==1) dfs(i,0,1);
		long long ans=0;
	    for(int i=1;i<=tot;i++) ans+=l[i]-l[fa[i]];
	    printf("%lld\n",ans);
	}	
}
signed main(){
	SAM::work();
	return 0;
}

五.Musical Themes
实际上这题原数据n=20000, O ( n 2 ) O(n^2) O(n2)显然不可做
我们发现变调虽然会改变数值大小,但并不会影响相邻两个数之间的差值,所以我们把所有数与相邻的两个数作差,将得到的值插入SAM,然后就是求一个串的最长重复子串(重复不能重叠)的问题了,会处理right的童鞋应该就都会了
不会处理right的话可以看看DZYO的博客
Code(略)

六.[NOI2015]品酒大会
NOI2015Day1真的水 并查集离散化签到题,树剖果题都考了
先把串倒着插入,这样前缀就变成了后缀
发现串如果是r相似的,那么他们一定是r-1,r-2,…1,0相似的
所以可以用类似于前缀和的方式统计答案,然后就好做了
Code:

#include<bits/stdc++.h>
#define ll long long
#define inf 1000000000000000000ll
using namespace std;
inline int read(){
	int res=0,f=1;char ch=getchar();
	while(!isdigit(ch)) {if(ch=='-') f=-f;ch=getchar();}
	while(isdigit(ch)) {res=(res<<3)+(res<<1)+(ch^48);ch=getchar();}
	return res*f;
}
const int N=600005;
char s[N];
int vis[N],head[N],nxt[N],cnt=0;
inline void add(int x,int y){vis[++cnt]=y,nxt[cnt]=head[x],head[x]=cnt;}
ll lmax(ll a,ll b){return a>b?a:b;}
ll lmin(ll a,ll b){return a<b?a:b;}
ll ans1[N],ans2[N];
namespace SAM{
	int n;
	int siz[N];
	ll mx[N],mn[N];
	int l[N],fa[N],ch[N][26],last,tot;
	inline void ins(int c,int w){
		int p=last,np=++tot;last=np;l[np]=l[p]+1;
		siz[np]=1,mx[np]=mn[np]=w;
		for(;p&&!ch[p][c];p=fa[p]) ch[p][c]=np;
		if(!p) fa[np]=1;
		else{
			int q=ch[p][c];
			if(l[q]==l[p]+1) fa[np]=q;
			else{
				int nq=++tot;l[nq]=l[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				fa[nq]=fa[q];fa[q]=fa[np]=nq;
				for(;ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
			}
		}
	}
    inline void build(){
    	for(int i=2;i<=tot;i++)
    	add(fa[i],i);
	}
	inline bool check(int x){return mx[x]!=-inf&&mn[x]!=inf;}
	void dfs(int v){
		if(!mn[v]&&!mx[v]) mn[v]=inf,mx[v]=-inf;
		for(int i=head[v];i;i=nxt[i]){
			int y=vis[i];dfs(y);
			if(check(v)&&check(y)) ans2[l[v]]=lmax(ans2[l[v]],lmax(mx[v]*mx[y],mn[v]*mn[y]));
			ans1[l[v]]+=(ll)siz[v]*siz[y];
			mx[v]=lmax(mx[v],mx[y]);
			mn[v]=lmin(mn[v],mn[y]);
			siz[v]+=siz[y];
		}
	}
	ll a[N];
	void work(){
		n=read();scanf("%s",s+1);
		for(int i=1;i<=n;i++) a[i]=read();
		last=tot=1;for(int i=n;i>=1;i--) ins(s[i]-'a',a[i]);
		for(int i=0;i<=n;i++) ans2[i]=-inf;
		build();dfs(1);
		for(int i=n-1;i>=0;i--)
		ans2[i]=lmax(ans2[i],ans2[i+1]),ans1[i]+=ans1[i+1];
        for(int i=0;i<=n-1;i++) printf("%lld %lld\n",ans1[i],ans1[i]?ans2[i]:0);
	}	
}
int main(){
	SAM::work();
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值