后缀自动机(SAM)& 广义后缀自动机

零、工具

SAM Drawer:一个画后缀自动机的工具

一、 模板

1. 后缀自动机(单串)
(1)插入(建SAM)
char s[N];
int ch[N*2][26],fa[N*2],len[N*2];
int last=1,tot=1;
void insert(int x)
{
	int p=last;
	int np=last=++tot;
	len[np]=len[p]+1;
	while(p&&!ch[p][x]) { ch[p][x]=np; p=fa[p]; }
	if(!p) fa[np]=1;
	else
	{
		int q=ch[p][x];
		if(len[q]==len[p]+1) fa[np]=q;
		else
		{
			int nq=++tot;
			for(int i=0;i<26;i++) ch[nq][i]=ch[q][i];
			fa[nq]=fa[q]; fa[np]=fa[q]=nq;
			len[nq]=len[p]+1;
			while(p&&ch[p][x]==q)
			{ ch[p][x]=nq; p=fa[p]; }
		}
	}
}
int main()
{
	scanf("%s",s+1);
	int ls=strlen(s+1);
	for(int i=1;i<=ls;i++)
		insert(s[i]-'a');
	return 0; 
} 
(2)匹配
	int p=1,now=0;
	for(int i=1;i<=lt;i++)
	{
		int x=t[i]-'a';
		if(ch[p][x])
		{ now++; p=ch[p][x];}
		else
		{
			while(p&&!ch[p][x])	p=fa[p];
			if(!p){p=1;now=0;}
			else
			{ now=len[p]+1; p=ch[p][x];}
		}
	}
2. 广义后缀自动机(多串)

广义自动机有在线/离线两种写法。(在线写法是流传比较广的,能通过大部分题目,但是有人说是假的,不知道为什么……)
(模板为洛谷广义后缀自动机

(1)在线版

每次新加入一个串的时候,将last改为1。如果对应节点存在,进行修改;否则,新建节点。

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#define LL long long
using namespace std;
const int N=2e6+10;
int ch[N][26],len[N],fa[N];
char s[N];
int last,tot=1;
int insert(int c,int last)
{
	int p=last;
	if(ch[p][c])
	{
		int np=ch[p][c];
		if(len[p]+1==len[np])	return np;
		else
		{
			int nq=++tot;
			len[nq]=len[p]+1;
			for(int i=0;i<26;i++)	ch[nq][i]=ch[np][i];
			while(p&&ch[p][c]==np)	ch[p][c]=nq,p=fa[p];
			fa[nq]=fa[np],fa[np]=nq;
			return nq;	
		}
	}
	int q=++tot;
	len[q]=len[p]+1;
	while(p&&!ch[p][c]) ch[p][c]=q,p=fa[p];
	if(!p)	fa[q]=1;
	else
	{
		int np=ch[p][c];
		if(len[p]+1==len[np])	fa[q]=np;
		else
		{
			int nq=++tot;
			len[nq]=len[p]+1;
			for(int i=0;i<26;i++)	ch[nq][i]=ch[np][i];
			while(p&&ch[p][c]==np)	ch[p][c]=nq,p=fa[p];
			fa[nq]=fa[np],fa[np]=fa[q]=nq; 
		}
	}
	return q;
}
int main()
{
	int n;
	LL ans=0;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%s",s+1);
		last=1;
		int l=strlen(s+1);
		for(int j=1;j<=l;j++)	last=insert(s[j]-'a',last);
	}
	for(int i=2;i<=tot;i++)	ans+=len[i]-len[fa[i]];
	printf("%lld ",ans);
	return 0;
}
(2)离线版

先将所有串放在一起建立一棵Trie树,然后按照bfs序建立后缀自动机。(当有两棵树的时候,封进结构体太爽了)

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#define LL long long
using namespace std;
const int N=2e6+10;
const int M=1e6+10;
char ss[M];
struct Trie{
	int tot,col[M],fa[M],ch[M][26];
	Trie(){tot=1;}
	void insert(char s[])
	{
		int now=1;
		for(int i=1;s[i];i++)
		{
			int c=s[i]-'a';
			if(!ch[now][c])
				ch[now][c]=++tot,fa[tot]=now,col[tot]=c;
			now=ch[now][c];
		}
	}
}T1;
struct Suffix_Auto{
	int tot,pos[N],fa[N],len[N],ch[N][26];
	queue<int>Q;
	Suffix_Auto(){tot=1;}
	int insert(int c,int last)
	{
		int p=last;
		int q=++tot;
		len[q]=len[p]+1;
		while(p&&!ch[p][c]) ch[p][c]=q,p=fa[p];
		if(!p)fa[q]=1;
		else
		{
			int np=ch[p][c];
			if(len[p]+1==len[np])fa[q]=np;
			else
			{
				int nq=++tot;
				len[nq]=len[p]+1;
				for(int i=0;i<26;i++)
					ch[nq][i]=ch[np][i];
				while(p&&ch[p][c]==np)
					ch[p][c]=nq,p=fa[p];
				fa[nq]=fa[np],fa[q]=fa[np]=nq; 
			}
		}
		return q;
	 } 
	 void build()
	 {
	 	for(int i=0;i<26;i++)
	 		if(T1.ch[1][i])
	 			Q.push(T1.ch[1][i]);
	 	pos[1]=1;
	 	while(Q.empty()==0)
	 	{
	 		int x=Q.front();
	 		Q.pop();
	 		pos[x]=insert(T1.col[x],pos[T1.fa[x]]);
	 		for(int i=0;i<26;i++)
	 			if(T1.ch[x][i])
	 				Q.push(T1.ch[x][i]); 
		 }
	 }
	 void query()
	 {
	 	LL ans=0;
	 	for(int i=2;i<=tot;i++)	ans+=len[i]-len[fa[i]];
	 	printf("%lld",ans);
	 }
}SAM;
int main()
{
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%s",ss+1);
		T1.insert(ss);
	}
	SAM.build();
	SAM.query();
	return 0;
}

二、题目

1. CF gym100956 K
后缀自动机模板题

题意:给出S,T两个字符串。设S的一个子串为s[l,……r],要求s在T中出现,且r-l+1-max(l-1,ls-r)最大,其中ls为S串的长度。
题解:当l确定的时候,r越大,结果不会更差,所以,对于每一个l,求其最大匹配位置进行更新答案即可。用T串倒序建后缀自动机,用S在后缀自动机上跑匹配。

const int N=1e6+10;
const int inf=1e9+10;
char s[N],t[N];
int ch[N*2][26],fa[N*2],len[N*2],R[N],ans[N];
int last=1,tot=1;
void insert(int x)
{
	int p=last;
	int np=last=++tot;
	len[np]=len[p]+1;
	while(p&&!ch[p][x]) { ch[p][x]=np; p=fa[p]; }
	if(!p) fa[np]=1;
	else
	{
		int q=ch[p][x];
		if(len[q]==len[p]+1) fa[np]=q;
		else
		{
			int nq=++tot;
			for(int i=0;i<26;i++) ch[nq][i]=ch[q][i];
			fa[nq]=fa[q]; fa[np]=fa[q]=nq;
			len[nq]=len[p]+1;
			while(p&&ch[p][x]==q)
			{ ch[p][x]=nq; p=fa[p]; }
		}
	}
}
int main()
{
	int maxans=-inf,pos=-1;
	scanf("%s",s+1);
	int ls=strlen(s+1);
	scanf("%s",t+1);
	int lt=strlen(t+1);
	for(int i=lt;i>0;i--)	insert(t[i]-'a');
	int p=1,now=0;
	for(int i=1;i<=ls;i++)	ans[i]=-inf;
	for(int i=ls;i>0;i--)
	{
		int x=s[i]-'a';
		if(ch[p][x]){ p=ch[p][x]; now++;}
		else
		{
			while(p&&!ch[p][x]) p=fa[p];
			if(p) { now=len[p]+1;  p=ch[p][x];}
			else { now=0; p=1; }
		}
		if(now)
		{
			ans[i]=now-max(i-1,ls-(i+now-1));
			R[i]=i+now-1;
		}
	}
	for(int i=1;i<=ls;i++)
		if(ans[i]>maxans)	{	maxans=ans[i];	pos=i;	}
	if(pos==-1)	{	printf("-1 -1");	return 0;	}
	int i;
	for(i=pos;i<=R[pos];i++)
		if(i-pos+1-max(pos-1,ls-i)==ans[pos])
			break;
	printf("%d %d",pos-1,i-1);
	return 0; 
} 
2. (SPOJ8222)
利用后缀树

统计每一长度的出现频率最高的子串的出现次数
题解:按拓扑序统计,统计时向父节点更新即可。

void cal()
{
	for(int i=1;i<=cnt;i++) c[mx[i]]++;
	for(int i=1;i<=n;i++) c[i]+=c[i-1];
	for(int i=cnt;i>0;i--) a[c[mx[i]]--]=i;
	for(int i=cnt;i>0;i--)
	{
		int p=a[i];
		size[fa[p]]+=size[p];
		ans[mx[p]]=max(ans[mx[p]],size[p]);
	}
}
int main()
{
	scanf("%s",s+1);
	n=strlen(s+1);
	root=last=cnt=1;
	for(int i=1;i<=n;i++) insert(s[i]-'a');
	cal();
	for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
	return 0; 
} 
3. (SPOJ) LCS(最长公共子串)

题意:求S与T的最长公共子串
题解:以S建树,用T在自动机上跑匹配。
假设T[1……i-1]的后缀在后缀自动机上最大匹配位置为p,maxl为T[1……i-1]的后缀的最大匹配长度:
         (1) ch[p][T[i]-‘a’]存在,即可以继续向下匹配到T串上的第i位,则T[1……i]的最大匹配位置为ch[p][T[i]-‘a’],最大匹配位置为maxl+1;
         (2)ch[p][T[i]-‘a’]不存在,则向p的父节点跳转:
                  (a)如果跳转到某一位置p’,ch[p’][T[i]-‘a’]存在,则最大匹配位置为p’,最大匹配长度为len[p’]+1;
                  (b)如果一直跳转到0也没有匹配的位置,则最大匹配长度为0,(记得将p移动到树根)

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int N=250010;
char s[N],t[N];
int ch[N*2][26],len[N*2],fa[N*2];
int last=1,tot=1;
void insert(int x)
{
	int p=last;
	int np=last=++tot;
	len[np]=len[p]+1;
	while(p&&!ch[p][x]) { ch[p][x]=np; p=fa[p]; }
	if(!p) fa[np]=1;
	else
	{
		int q=ch[p][x];
		if(len[q]==len[p]+1) fa[np]=q;
		else
		{
			int nq=++tot;
			for(int i=0;i<26;i++) ch[nq][i]=ch[q][i];
			fa[nq]=fa[q]; fa[np]=fa[q]=nq;
			len[nq]=len[p]+1;
			while(p&&ch[p][x]==q)
			{ ch[p][x]=nq; p=fa[p]; }
		}
	}
}
int main()
{
	int ans=0,now=0;
	scanf("%s",s+1); scanf("%s",t+1);
	int ls=strlen(s+1); int lt=strlen(t+1);
	for(int i=1;i<=ls;i++)
		insert(s[i]-'a');
	int p=1;
	for(int i=1;i<=lt;i++)
	{
		int x=t[i]-'a';
		if(ch[p][x])
		{ p=ch[p][x]; now++;}
		else
		{
			while(p&&!ch[p][x]) p=fa[p];
			if(p)
			{ now=len[p]+1;  p=ch[p][x];}
			else
			{ now=0; p=1; }
		}
		ans=max(ans,now);
	}
	printf("%d",ans);
	return 0;
 } 
4. (SPOJ) LCS2

题意:10个字符串求最长公共子串
题解:将一个字符串建树,其他字符串在上面匹配。统计每个字符串在不同点上匹配的最大长度,并用其更新该点和该点的父节点

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cmath>
#include<cstring>
using namespace std;
const int N=1e5+10;
char s[N];
int ch[N*2][26],fa[N*2],len[N*2];
int last=1,tot=1;
void insert(int x)
{
	int p=last;
	int np=last=++tot;
	len[np]=len[p]+1;
	while(p&&!ch[p][x]) { ch[p][x]=np; p=fa[p]; }
	if(!p) fa[np]=1;
	else
	{
		int q=ch[p][x];
		if(len[q]==len[p]+1) fa[np]=q;
		else
		{
			int nq=++tot;
			for(int i=0;i<26;i++) ch[nq][i]=ch[q][i];
			fa[nq]=fa[q]; fa[np]=fa[q]=nq;
			len[nq]=len[p]+1;
			while(p&&ch[p][x]==q)
			{ ch[p][x]=nq; p=fa[p]; }
		}
	}
}
void get_tuopu()
{
	for(int i=1;i<=tot;i++)
		c[len[i]]++;
	for(int i=1;i<=tot;i++)
		c[i]+=c[i-1];
	for(int i=1;i<=tot;i++)
		rk[c[len[i]]--]=i;
}
int main()
{
	scanf("%s",s+1);
	int ls=strlen(s+1);
	for(int i=1;i<=ls;i++)
		insert(s[i]-'a');
	get_tuopu();
	for(int i=1;i<=tot;i++)
		mn[i]=len[i];
	while(scanf("%s",t+1)!=EOF)
	{
		int lt=strlen(t+1);
		int p=1,now=0;
		for(int i=1;i<=tot;i++)
			h[i]=0;
		for(int i=1;i<=lt;i++)
		{
			int x=t[i]-'a';
			if(ch[p][x])
			{
				now++; p=ch[p][x];
				h[p]=max(h[p],now);
			}
			else
			{
				while(p&&!ch[p][x]) p=fa[p];
				if(!p)
				{ p=1; now=0; }
				else
				{
					now=len[p]+1; p=ch[p][x];
					h[p]=max(h[p],now);
				}
			}
		}
		for(int i=tot;i>0;i--)
		{
			int x=rk[i];
			mn[x]=min(mn[x],h[x]);
			h[fa[x]]=max(h[fa[x]],min(h[x],len[fa[x]]));
		}
	}
	int ans=0;
	for(int i=1;i<=tot;i++)
		ans=max(ans,mn[i]);
	printf("%d",ans);
	return 0; 
} 
5.牛客多校2020第2场A
广义后缀自动机+KMP

题解

6.[SCOI2012]喵星球上的点名

题意:每一个人的名字由两个字符串构成,如果询问串为某个人的某个串的子串,那么这个人即被点到。对于每一个询问串,输出点到了几个人。全部询问结束后,输出每个人被点到的次数。
思路
  对名字建广义后缀自动机。
询问一:预处理每一个节点会被多少个人的名字包含。枚举每一个人的名字,对于每一个节点,向上跳fa,对其所有后缀的sz进行更新。对每一个询问,找到对应节点,输出sz即可。
询问二:对于每一个询问串,对其所在节点vist更新。全部询问结束后,枚举每一个人的名字,对于其中每一个节点,向上统计其所有后缀的出现次数。

由于字符集较大,需要用map记录

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<map>
#include<vector>
#define LL long long
using namespace std;
const int N=4e5+10;
map<int, int> ch[N];
int len[N],fa[N], lazy[N], ans[N], vist[N], sz[N], ss[N], a[N][2];
vector<int>s[N][2];
int tot=1;
int insert(int c,int last)
{
	int p=last;
	if(ch[p].find(c) != ch[p].end())
	{
		int np=ch[p][c];
		if(len[p]+1==len[np])	return np;
		else
		{
			int nq=++tot;
			len[nq]=len[p]+1;
			ch[nq]=ch[np];
			while(p && ch[p].find(c) != ch[p].end() && ch[p][c]==np)	ch[p][c]=nq,p=fa[p];
			fa[nq]=fa[np],fa[np]=nq;
			return nq;	
		}
	}
	int q=++tot;
	len[q]=len[p]+1;
	while(p&& ch[p].find(c) == ch[p].end()) ch[p][c]=q,p=fa[p];
	if(!p)	fa[q]=1;
	else
	{
		int np=ch[p][c];
		if(len[p]+1==len[np])	fa[q]=np;
		else
		{
			int nq=++tot;
			len[nq]=len[p]+1;
			ch[nq]=ch[np];
			while(p && ch[p].find(c) != ch[p].end() && ch[p][c]==np)	ch[p][c]=nq,p=fa[p];
			fa[nq]=fa[np],fa[np]=fa[q]=nq; 
		}
	}
	return q;
}
void update_sz(int x, int y)
{
	for(;x && lazy[x] != y; x = fa[x])
	{
		lazy[x] = y;
		sz[x]++;
	}
}
void update_ans(int x, int y)
{
	for(; x && lazy[x] != y; x = fa[x])
	{
		lazy[x] = y;
		ans[y] += vist[x];
	}
}
int main()
{
	int n, m;
	scanf("%d%d",&n, &m);
	for(int i=1;i<=n;i++)
	{
		for(int k = 0; k <= 1; k++)
		{
			int last = 1;
			scanf("%d", &a[i][k]);
			for(int j = 1; j <= a[i][k]; j++)
			{
				int c;
				scanf("%d", &c);
				s[i][k].push_back(c); 
				last=insert(c, last);
			}	
		}
	}
	for(int i = 1; i <= n; i++)
	{
		for(int k = 0; k <= 1; k++)
		{
			int now = 1;
			for(int j = 0; j < a[i][k]; j++)
			{
				now = ch[now][s[i][k][j]];
				update_sz(now, i);	
			}
		}
	}
	for(int i = 1;i <= m; i++)
	{
		int k;
		scanf("%d", &k);
		int now = 1;
		int flag = 1;
		for(int j = 1;j <= k; j++)
			scanf("%d", &ss[j]);
		for(int j = 1; j <= k; j++)
		{
			int c = ss[j];
			if(ch[now].find(c) == ch[now].end())
			{
				flag = 0;
				break;
			} 
			else
				now = ch[now][c];
		}
		if(flag)
		{
			printf("%d\n", sz[now]);
			vist[now]++;
		}
		else
			printf("0\n"); 
	}
	for(int i = 1; i <= tot; i++)	lazy[i] = 0;
	for(int i = 1; i <= n; i++)
	{
		for(int k = 0; k <= 1; k++)
		{
			int now = 1;
			for(int j = 0; j < a[i][k]; j++)
			{
				now = ch[now][s[i][k][j]];
				update_ans(now, i);
			}
		}
	}
	for(int i = 1;i <= n; i++)
	{
		printf("%d ",ans[i]);
	}
	printf("\n");
	return 0;
}

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值