【模板】广义后缀自动机(广义 SAM)

II.【模板】广义后缀自动机(广义 SAM)

我们之前提到过一句话:“后缀自动机的构造是在线的,增量的。”而这题,便是其应用之一。

首先,有一种暴力的解法,是直接将所有东西全都依次插入SAM,每次插入从 1 开始。但是,这样插入完后,如果你输出构建的SAM,会发现有一些点是无法从根到达的!

我们重新翻出SAM的代码:

int Add(int x,int c){
	int xx=++cnt;t[xx].len=t[x].len+1;
	for(;x&&!t[x].ch[c];x=t[x].fa)t[x].ch[c]=xx;
	if(!x){t[xx].fa=1;return xx;}
	int y=t[x].ch[c];
	if(t[y].len==t[x].len+1){t[xx].fa=y;return xx;}
	int yy=++cnt;t[yy]=t[y];
	t[yy].len=t[x].len+1;
	t[y].fa=t[xx].fa=yy;
	for(;x&&t[x].ch[c]==y;x=t[x].fa)t[x].ch[c]=yy;
	return xx;
}

在插入的时候,有可能发生如下一些情况:

  1. x 已经有了 c 儿子,并且 c 儿子的 len 刚好是 x 自身的 len+1。换句话说,该 c 儿子同 xx 本身是等价的。这时,如果按照上文程序模拟的话,就会发现 xx 成为了孤立点。解决方法即为忽略 xx,直接 return c 儿子即可。

  2. x 已经有了 c 儿子,但 c 儿子不与 xx 等价。这时,就会发现 xx 未在SAM中出现,c 实际上被插到了 yy 位置上。

当然,尽管有着这样那样的错误,暴力插串的构建SAM方式,在大多数题中都是可以通过的

自然,学还是要学正解的。其解决的一种方式是,只要使得任何被插入的 x 在插入时都没有 c 儿子即可。

我们考虑一下为什么会出现有 c 儿子的情况——假如我们已经有了一个串 a,那若我们又来了一个同样的串 a,插在根节点后面,此时根节点就已经有 a 儿子了。而解决办法就是,若两个串具有相同的 LCP,只插入一次。也即,对所有串离线下来,建一棵trie树,然后只添加trie树上的边。

然后这又涉及到一个疑问——trie树上的边到底应该按照何种顺序加入呢?你可能会直接说“当然是dfs一遍加入啦”,但是dfs序加入就会使得我们第二次跳父亲的那重循环保证不了每条边最多只会被重定向一次,导致复杂度最多是 O(|T|2) 的,其中 |T| 是trie树中节点数。而解决方式就是采用bfs序加入。此时,就是 O(|T|) 的了。

但是,这种建trie的方式是离线的。有没有在线的方式呢?

有!只需要克服我们前面讲的两个问题就行了。

这里是正确的在线做法:

int Add(int x,int c){
	if(t[x].ch[c]){
		int y=t[x].ch[c];
		if(t[y].len==t[x].len+1)return y;//(x,c) has been added into the tree already.
		int yy=++cnt;t[yy]=t[y];
		t[yy].len=t[x].len+1,t[y].fa=yy;
		for(;x&&t[x].ch[c]==y;x=t[x].fa)t[x].ch[c]=yy;
		return yy;
	}
	int xx=++cnt;t[xx].len=t[x].len+1;
	for(;x&&!t[x].ch[c];x=t[x].fa)t[x].ch[c]=xx;
	if(!x){t[xx].fa=1;return xx;}
	int y=t[x].ch[c];
	if(t[y].len==t[x].len+1){t[xx].fa=y;return xx;}
	int yy=++cnt;t[yy]=t[y];
	t[yy].len=t[x].len+1;
	t[y].fa=t[xx].fa=yy;
	for(;x&&t[x].ch[c]==y;x=t[x].fa)t[x].ch[c]=yy;
	return xx;
}

可以看到,二者唯一的差别就是第二个前面加的一坨特判。这一坨特判刚好解决了上述两个问题,应该很容易理解(毕竟,这个特判是我自己yy出来的——足以发现它有多么清晰)

无论是在线还是离线,只要是合法的做法,都不会出现孤立点、到达不了的点或是其它奇奇怪怪的东西。可以直接当作普通SAM使用。

SAM应用2:求不同子串数量

不同子串数量可以由两种思路求得。

一种是从自动机角度想,从原点出发所有路径都是子串;而因为自动机的性质,任意一条子串的路径唯一,所以只需要统计自原点出发的路径数量即可。下文中错误的解法使用的是这种思路。

另一种是从parent tree角度想。parent tree所包含的过程是不断往串开头加东西的过程,而endpos相同的串(换句话说,所有相同的子串)被归到同一个点上。这也意味着只需统计所有点上不同串的数量即可。不同串的数量,就是 maxmin+1。两种正确的解法都使用了这种思路。

错误代码(事实上可以AC):

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1001000;
int cnt=1;
struct Suffix_Automaton{int ch[26],fa,len;}t[N<<1];
int Add(int x,int c){
	int xx=++cnt;t[xx].len=t[x].len+1;
	for(;x&&!t[x].ch[c];x=t[x].fa)t[x].ch[c]=xx;
	if(!x){t[xx].fa=1;return xx;}
	int y=t[x].ch[c];
	if(t[y].len==t[x].len+1){t[xx].fa=y;return xx;}
	int yy=++cnt;t[yy]=t[y];
	t[yy].len=t[x].len+1;
	t[y].fa=t[xx].fa=yy;
	for(;x&&t[x].ch[c]==y;x=t[x].fa)t[x].ch[c]=yy;
	return xx;
}
char s[N];
int n,S,in[N<<1];
queue<int>q;
ll f[N<<1],res;
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%s",s),S=strlen(s);
		for(int j=0,las=1;j<S;j++)las=Add(las,s[j]-'a');
	}
	for(int i=1;i<=cnt;i++)for(int j=0;j<26;j++)if(t[i].ch[j])in[t[i].ch[j]]++;
	for(int i=1;i<=cnt;i++)if(!in[i])q.push(i);
	f[1]=1;
	while(!q.empty()){
		int x=q.front();q.pop();
		res+=f[x];
		for(int i=0;i<26;i++){
			if(!t[x].ch[i])continue;
			f[t[x].ch[i]]+=f[x];
			if(!--in[t[x].ch[i]])q.push(t[x].ch[i]);
		}
	}
	printf("%lld\n",res-1);
	return 0;
}

离线代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1001000;
namespace TR{
	int cnt=1;
	struct Trie{int ch[26],id;}t[N];
	void Add(char *s,int n){
		int x=1;
		for(int i=0;i<n;i++){
			if(!t[x].ch[s[i]-'a'])t[x].ch[s[i]-'a']=++cnt;
			x=t[x].ch[s[i]-'a'];
		}
	}
}
namespace SAM{
	int cnt=1;
	struct Suffix_Automaton{int ch[26],fa,len;}t[N<<1];
	int Add(int x,int c){
		int xx=++cnt;t[xx].len=t[x].len+1;
		for(;x&&!t[x].ch[c];x=t[x].fa)t[x].ch[c]=xx;
		if(!x){t[xx].fa=1;return xx;}
		int y=t[x].ch[c];
		if(t[y].len==t[x].len+1){t[xx].fa=y;return xx;}
		int yy=++cnt;t[yy]=t[y];
		t[yy].len=t[x].len+1;
		t[y].fa=t[xx].fa=yy;
		for(;x&&t[x].ch[c]==y;x=t[x].fa)t[x].ch[c]=yy;
		return xx;
	}	
}
using namespace SAM;
queue<int>q;
void build(){
	q.push(1),TR::t[1].id=1;
	while(!q.empty()){
		int x=q.front();q.pop();
		for(int i=0;i<26;i++)if(TR::t[x].ch[i])TR::t[TR::t[x].ch[i]].id=Add(TR::t[x].id,i),q.push(TR::t[x].ch[i]);
	}
}
char s[N];
int n,S;
ll res;
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%s",s),S=strlen(s),TR::Add(s,S);
	build();
	for(int i=1;i<=cnt;i++)res+=t[i].len-t[t[i].fa].len;
	printf("%lld\n",res);
	return 0;
}

在线做法:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1001000;
int cnt=1;
struct Suffix_Automaton{int ch[26],fa,len;}t[N<<1];
int Add(int x,int c){
	if(t[x].ch[c]){
		int y=t[x].ch[c];
		if(t[y].len==t[x].len+1)return y;//(x,c) has been added into the tree already.
		int yy=++cnt;t[yy]=t[y];
		t[yy].len=t[x].len+1,t[y].fa=yy;
		for(;x&&t[x].ch[c]==y;x=t[x].fa)t[x].ch[c]=yy;
		return yy;
	}
	int xx=++cnt;t[xx].len=t[x].len+1;
	for(;x&&!t[x].ch[c];x=t[x].fa)t[x].ch[c]=xx;
	if(!x){t[xx].fa=1;return xx;}
	int y=t[x].ch[c];
	if(t[y].len==t[x].len+1){t[xx].fa=y;return xx;}
	int yy=++cnt;t[yy]=t[y];
	t[yy].len=t[x].len+1;
	t[y].fa=t[xx].fa=yy;
	for(;x&&t[x].ch[c]==y;x=t[x].fa)t[x].ch[c]=yy;
	return xx;
}
char s[N];
int n,S,in[N<<1];
ll res;
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%s",s),S=strlen(s);
		for(int j=0,las=1;j<S;j++)las=Add(las,s[j]-'a');
	}
	for(int i=1;i<=cnt;i++)for(int j=0;j<26;j++)if(t[i].ch[j])in[t[i].ch[j]]++;
	for(int i=1;i<=cnt;i++)res+=t[i].len-t[t[i].fa].len;
	printf("%lld\n",res);
	return 0;
}

posted @   Troverld  阅读(107)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示