后缀自动机线性构造方法

版权声明:本文为博主原创文章,你们可以随便转载 https://blog.csdn.net/Jaihk662/article/details/82824469

前置(后缀自动机SAM基本概念):https://blog.csdn.net/Jaihk662/article/details/82823251

来源:http://hihocoder.com/problemset/problem/1445

hihocoder已经讲的很清楚了,这里当然还是简略的描述一下吧

构造的核心问题:每当在末尾新増一个字符S[i],如何让SAM额外识别子串S[1..i], S[2..i], …, S[i]

一、构造过程中的一些变量/函数

字符串“aabbabd”的后缀自动机:

  • tre[k].next[ch]:就是trans(k, ch),即longest(k)后面接上字符ch,得到的新字符串所在的节点,例如图中tre[5]['a'] = 6
  • tre[k].pre:节点k的Suffix Links,例如图中pre[7] = pre[3] = 8,pre[9] = 0,pre[8] = 5, 
  • tre[k].len:longest(k)的长度,例如图中len[4] = 4, len[6] = 5, len[7] = 6
  • cnt:当前新建节点的编号
  • last:假设当前添加到第i个字符,前缀S[1..i-1]所在的节点,例如图中当cnt = 6时,last = 4

 

二、每新增一个字符后,让SAM额外识别子串S[1..i], S[2..i], …, S[i]的方法(三种情况)

先设sp(k)表示k节点到S的Suffix Links路径,例如sp(7) = 7→8→5→0,sp(2) = 2→1→0

当前要插入第i个字符,且下一个新节点为now

每个节点的endpos()表

(1)最简单的一种情况(新子串S[1..i], S[2..i], …, S[i]的endpos都相同,旧子串不同的endpos集合个数不会改变):

对于sp(u)路径上的任意节点v,都有trans[v][S[i]] = NULL。这时我们只要令trans[v][S[i]] = now,并且令pre[pnew] = 0即可

对应着字符串"aabbabd",插入最后一个字符'd'的情况,显然这个时候所有以'd'结尾的新后缀endpos都相同

→因此你只需要trans[7]['d'] = trans[8]['d'] = trans[5]['d'] = trans[0]['d'] = 9,pre[9] = 0即可,非常简单

对应代码如下

while(p!=-1 && tre[p].Next[ch-'a']==0)
{
	tre[p].Next[ch-'a'] = now;
	p = tre[p].pre;
}
if(p==-1)
	tre[now].pre = 0;

(2)第二种情况(新子串S[1..i], S[2..i], …, S[i]的endpos存在不同,但是旧子串不同的endpos集合个数不会改变):

sp(u)路径上的有一个节点v,trans[v][S[i+1]]!=NULL,例如当插入第5个字符S[5] = 'a'时,步骤如下

  1. sp(4) = 4→5→0,因为trans[4]['a'] = trans[5]['a'] = NULL,所以trans[4]['a'] = trans[5]['a'] = 6,即连上图中的红色实线
  2. 之后发现trans[0]['a'] = 1,但此时1号节点的最长子串刚好就是0号节点的最长子串+S[5],也就是len[3] = len[0]+1,这样子的话,只需要pre[6] = 1即可,也就是连上图中的红色虚线

对应代码如下

if(p==-1)
	tre[now].pre = 0;
else
{
	q = tre[p].Next[ch-'a'];
	if(tre[q].len==tre[p].len+1)
		tre[now].pre = q;
	else
	{
		//……
	}
}

可见,对于节点u, v,如果节点v的最长子串刚好就是节点u的最长子串+S[i],也就是len[v] = len[u]+1,那么只需要pre[now] = v即可,但如果节点v的最长子串不是节点u的最长子串+S[i],也就是len[v] > len[u]+1怎么办呢?那就是第三种情况了,需要新建节点(如下)

(3)第三种情况(新子串S[1..i], S[2..i], …, S[i]的endpos存在不同,并且旧子串不同的endpos集合个数会发生改变):

仍然是sp(u)路径上的有一个节点v,trans[v][S[i+1]]!=NULL,例如当插入第4个字符S[4] = 'b'时,步骤如下

  1. sp(3) = 3→0,因为trans[3]['b'] = NULL,所以trans[3]['b'] = 4
  2. tran[0]['b] = 3,但是len[3] != len[0]+1,可以看出在新增字符前 endpos("aab") = endpos("b") = {3},而在新增字符后endpos("aab") = {3},endpos("b") = {3,4},显然旧子串"b"的endpos值发生了改变
  3. 考虑SAM中,每个节点的所有子串endpos集合都相同,而在新增字符后,原本相同的两个子串"aab"和"b"的endpos集合就不同了,并且endpos("b")为一个前面从未出现过的集合{3,4}
  4. 这样结果已经明显了,需要新建一个代表{3,4}的节点5,并让"b"重新连向节点5,而这个节点5可以理解为从节点3分裂出来的,所有的trans(p, ch)都和节点3一致如果看不懂或不知道为什么,可以翻一下前置博客中Suffix Links的性质),除此之外,所有"b"的后缀也都应该分配到节点5(当然"b"是个特例没有多余后缀了),即从左图修改成右图
  5. 搞定之后只需要让pre[3] = pre[4] = 5即可

总体来说:

  1. 在sp(u)这条路径上,从u开始有一部分连续的状态满足trans[u..][ch] = NULL,对于这部分状态我们只需增加trans[u..][ch] = now
  2. 紧接着有一部分连续的状态v..w满足trans[v..w][ch] = x,并且longest(v)+ch不等于longest(x)。这时我们需要从x拆分出新的状态y,并且把原来x中长度小于等于longest(v)+c的子串分给y,其余字串留给x。同时令trans[v..w][ch] = y,pre[x] = pre[now] = y

对应代码如下:

rev = cnt++;
tre[rev] = tre[q];
tre[rev].len = tre[p].len+1;
while(p!=-1 && tre[p].Next[ch-'a']==q)
{
	tre[p].Next[ch-'a'] = rev;
	p = tre[p].pre;
}
tre[q].pre = tre[now].pre = rev;

 

三、三种不同情况与Suffix Links内向树

前面有提到:所有的Suffix Links构成了一颗根节点为S的内向树,每个节点都代表着一个独一无二的endpos()集合,并且每个节点的所有儿子一定都是其父亲的子集

而每次新增字符的三种情况正好对应着3种简单的树操作

先看第(2)种:插入第5个字符S[5] = 'a'后,树的变化如下:

可以看出,假设当前新增第i个字符S[i]后,除了新增的节点now,原先树的结构并没有改变,只不过新节点now的所有祖先都会多出一个元素i

这样子的话,就可以推测出第(1)种情况下:插入的新节点就直接连向根

最后再看第(3)种:插入第4个字符S[4] = 'b'后,树的变化如下

可以看出,假设当前新增第i个字符S[i]后,需要在某个节点son与其父亲之间额外再新建一个节点p,使得endpos(p) = endpos(son)+i,再将新节点now连向该新节点p,最后将p所有的祖先全部插入元素i

四、完整程序:

题意:给你一个字符串,求出有多少个本质不同的子串

题解:“后缀自动机中子串个数 = 所有节点的 longest(k)-shortest(k)+1 之和”

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
#define LL long long
typedef struct Node
{
	int len, pre;			//pre:对应图片中的绿线(一个节点显然最多只有一条绿线)
	int Next[26];			//Next[]:对应图片中的蓝线
}Node;
Node tre[2000005];
int cnt, last;
char str[1000005];
void Init()
{
	cnt = last = 0;
	memset(tre, 0, sizeof(tre));
	tre[cnt++].pre = -1;
}
void Insert(char ch, int id)
{
	int p, q, now, rev;
	p = last, now = cnt++;
	tre[now].len = tre[last].len+1;
	while(p!=-1 && tre[p].Next[ch-'a']==0)			//每次跳到当前最长且endpos集合与当前不同的后缀上,对应图片中的绿线回退(例如7→8→5→S)
	{
		tre[p].Next[ch-'a'] = now;					//tran(st[p], ch)=now,对应图片中的蓝线连接
		p = tre[p].pre;
	}
	if(p==-1)
		tre[now].pre = 0;			//情况①,递归到了初始节点S(空子串)(例如图片中的9号节点pre[9]=0)
	else			//如果中途某个子串tran(st[p], ch)已经存在
	{
		q = tre[p].Next[ch-'a'];
		if(tre[q].len==tre[p].len+1)		//情况②:节点q的最长子串刚好就是节点p的最长子串+S[i],也就是len[q] = len[p]+1
			tre[now].pre = q;
		else						//情况③
		{
			rev = cnt++;
			tre[rev] = tre[q];
			tre[rev].len = tre[p].len+1;			//这三行就是对Suffix Links内向树的插点操作
			tre[q].pre = tre[now].pre = rev;
			while(p!=-1 && tre[p].Next[ch-'a']==q)
			{
				tre[p].Next[ch-'a'] = rev;
				p = tre[p].pre;
			}
		}
	}
	last = now;
}
int main(void)
{
	LL ans;
	int n, i;
	scanf("%s", str+1);
	n = strlen(str+1);
	Init();
	for(i=1;i<=n;i++)
		Insert(str[i], i);
	ans = 0;
	for(i=1;i<=cnt-1;i++)
		ans += tre[i].len-tre[tre[i].pre].len;
	printf("%lld\n", ans);
	return 0;
}

猜你喜欢

转载自blog.csdn.net/Jaihk662/article/details/82824469