浅谈后缀自动机SAM模板

虽然是第二次学后缀自动机,这个学习的过程在我看来仍然是学习过程中最困难的,因为这个东西比较抽象,应用的性质很多,即使是构造理解起来也十分困难。另外,讲解这个东西的博客都太长了,一个一个写着预计阅读时间一个小时?而且看一句就要好好想一会,不时还要往上翻,晦涩的定义也太多了让人产生抗拒。

写得真的好
我知道了它为什么叫SAM

首先先不管太多,搞懂\(SAM\)的在线构建。
代码如下:

	int x=++tot;
	len[x]=len[u]+1;size[x]=1;
	for(;u&&trans[u][c]==0;u=fa[u])trans[u][c]=x;
	if(u==0)fa[x]=1;
	else{
		int v=trans[u][c];
		if(len[v]==len[u]+1)fa[x]=v;
		else{
			int w=++tot;len[w]=len[u]+1;
			fa[w]=fa[v];fa[x]=fa[v]=w;
			memcpy(trans[w],trans[v],sizeof(trans[v]));
			for(;u&&trans[u][c]==v;u=fa[u])trans[u][c]=w;
		}
	}
	u=x;

虽然但是,直接看代码还是不太明智。
首先要对\(SAM\)有一个印象。

想必看这篇博客的人都知道AC自动机。
在\(trie\)树上加上了\(fail\)边就成了\(AC\)自动机。
在\(parent\)树上加了一些\(trans\)边就成了\(SAM\)。

字符串"aababa"的parent树长这样
浅谈后缀自动机SAM模板
(PS:图是从上面链接中蒯的)
设字符串\(s\)在原串中所有结束位置的集合为\(Ep(s)\)
一个点代表一个子串的类,一个类中的子串\(s_i\)满足\(Ep(s_i)\)相同,也可以说这个点代表了这个相同的\(Ep(s_i)\)。
如\(a\)在原串中结尾的位置为\((1,2,4,6)\)、\(ababa\)和\(aababa\)在原串结尾位置都是\((6)\)
所以图中\((1,2,4,6)\)点代表的类是\(\{a\}\),图中\((6)\)点代表的类是\(\{baba,ababa,aababa\}\)。
根节点相当于一个虚拟节点。

几个十分重要的也是显然的性质:
1、不同的子串的\(Ep(s_i)\)不是包含就是没有交集。所以一个类的儿子是这个类的子集,一个类不能有两个父亲(这里把节点当做集合看了)
2、一个类中的字符串如果有多个那一定是连续差一个字符的。(如上图中(6)的类中\(“aababa“\)是\(”ababa"\)多一个\(”a"\),\(“ababa"\)是\(”baba"\)多一个\(“b"\))
3、一个类中最短字符串长度是父亲类中最长字符串长度+1。
4、一个节点父亲类中的串是这个节点类中串的后缀。

\(trans\)边要满足从根开始沿着\(trans\)边走到任意一点经过的\(trans\)上字符连起来是原串的一个子串。

\(SAM\)就是\(parent\)树上的点与\(trans\)边构成的有向无环图(并不算上\(parent\)树的边)。

接下来解释构建\(SAM\)的代码。
\(SAM\)在每次插入一个字符c之后维护
我们除去辅助数据之外其实只关心两个东西,也就是\(SAM\)的两个部分:\(parent\)树上的点,\(trans\)边。

	int x=++tot;
	len[x]=len[u]+1;size[x]=1;

为了方便说话,把插入\(c\)之前的串叫做旧串,把插入\(c\)之后的串叫做新串(比如构建\(“aababa”\)的\(SAM\)时现在插入最后一个\(a\),\(“aabab”\)是旧串,\(“aababa"\)是新串)。
\(x\)是新加入一个字符\(c\)之后新的\(parent\)树的节点。\(len[x]\)是节点\(x\)代表类中子串的最长长度。
\(size[x]\)是广义\(SAM\)的内容先不用管,\(u\)是包含旧串的类。
为什么一定有新的节点?设插入前字符串长度为\(n\),上面说到\(parent\)树的节点代表一个子串类,新加入一个字符之后,以\(n+1\)位置结尾的类不存在所以一定有新的节点。

for(;u&&trans[u][c]==0;u=fa[u])trans[u][c]=x;

\(fa[u]\)是\(u\)在\(parent\)树上的父亲。考虑一直这样跳父亲的意义,其实这是在遍历所有后缀。
考虑把\(trans[u][c]\)设成\(x\)代表什么?代表u中字符串可以通过在后边加上一个字符\('c'\)成为\(x\)节点代表的串。显然,在插入一个字符之后需要修改的\(trans[u][c]\)的\(u\)只可能是代表旧串的后缀的类,即u的祖先。

if(u==0)fa[x]=1;

遍历了全部的祖先包括代表空串的根节点之后还是没有向\(c\)转移的边,说明\(c\)从来没有出现过。直接把\(x\)接到根上。因为c都没出现过其他的类必然不可能会包含\(n+1\)这个结束位置,所以只能接在代表全集的根上才能满足性质1——一个类的儿子是这个类的子集

	int v=trans[u][c];
	if(len[v]==len[u]+1)fa[x]=v;

\(len[v]==len[u]+1\)这个判断是板子里最难以理解的。

\(len[v]=len[u]+1\)代表了什么?\(len[v]!=len[u]+1\)代表了什么?
当\(len[v]!=len[u]+1\)时
\(len[u]\)不可能比\(len[v]\)大(考虑trans的意义),说明\(len[v]>len[u]+1\)。因为\(tran[u][c]\)的定义,\(u\)中最长的串加上\(c\)之后一定是\(v\)中最长串的后缀
浅谈后缀自动机SAM模板
图中是\(len[v]=len[u]+1+2\)的情形,可见这个\(v\)节点包含了三个字符串,其中长度大于\(len[u]+1\)的两个串结尾位置不能包含\(n+1\),因为前面找到的\(u\)是可以在其代表串之后加c的最长后缀,之前出现的串中,不存在长度比\(len[u\)]更大的串可以满足在之后加上\('c'\)结尾位置也就不可能为\(n+1\)。所以我们要做的是把这个代表三个串的节点分成一个长度为\(len[u]+1\)的节点\(w\)它的结尾位置可以为\(n+1\)和两个长度大于\(len[u]\)的节点\(v\)它们的结尾位置不能为\(n+1\),然后把\(x\)和\(v\)连在\(w\)上。
所以\(len[u]+1==len[v]\)时就直接把\(x\)连在\(v\)上,因为结尾位置\(n+1\)被\(v\)类包含。
就是这个代码

	if(len[v]==len[u]+1)fa[x]=v;
	else{
		int w=++tot;len[w]=len[u]+1;
		fa[w]=fa[v];fa[x]=fa[v]=w;
		memcpy(trans[w],trans[v],sizeof(trans[v]));
		for(;u&&trans[u][c]==v;u=fa[u])trans[u][c]=w;
	}

至此后缀自动机的构造结束。

广义后缀自动机就是满足多个串的后缀自动机。

struct SAM{
	int trans[N][30],cnt,head[N],tot,u,size[N],len[N],fa[N];
	struct edge{
		int to,nxt;
	}e[N];
	void add_edge(int u,int v){
		cnt++;
		e[cnt].nxt=head[u];
		e[cnt].to=v;
		head[u]=cnt;
	}
	void init(){tot=u=1;}
	void rebuild(){u=1;}
	void ins(int c){
		if(trans[u][c]){
			int v=trans[u][c];
			if(len[v]==len[u]+1)size[v]++,u=v;
			else{
				int x=++tot;
				size[x]=1;
				len[x]=len[u]+1;
				memcpy(trans[x],trans[v],sizeof(trans[x]));
				fa[x]=fa[v];fa[v]=x;
				for(;u&&trans[u][c]==v;u=fa[u])trans[u][c]=x;
				u=x;
			}
		}
		else{
			int x=++tot;
			len[x]=len[u]+1;size[x]=1;
			for(;u&&trans[u][c]==0;u=fa[u])trans[u][c]=x;
			if(u==0)fa[x]=1;
			else{
				int v=trans[u][c];
				if(len[v]==len[u]+1)fa[x]=v;
				else{
					int w=++tot;len[w]=len[u]+1;
					fa[w]=fa[v];fa[x]=fa[v]=w;
					memcpy(trans[w],trans[v],sizeof(trans[v]));
					for(;u&&trans[u][c]==v;u=fa[u])trans[u][c]=w;
				}
			}
			u=x;
		}
	}
}

模板的代码只是多了一个判断,跟普通的后缀自动机差不多。

上一篇:鸿蒙源码分析(九)


下一篇:YBTOJ:斐波拉契(矩阵快速幂)