后缀自动机

听说后缀自动机是一个能帮助你轻松pku的神数据结构?既然这么神奇那没理由不学啊!可我盯着后缀自动机看了一个上午也没有头绪,(主要是看了后面忘了前面,结果后来直接手动模拟一遍就搞清楚怎么跑的了,当时一脸蒙bi啊),然而,我还是决定简要地证明一下后缀机!

参考资料:

https://blog.csdn.net/qq_35649707/article/details/66473069

2012noi冬令营clj讲稿

hzy大佬ppt

*****************************************************************//分界线,哼哼~

·后缀自动机的基本性质

对于一个串s,我们令r(zs)(zs为s的子串)表示zs子串在串s所有出现的末位置的集合,令n表示每个子串对应出现的个数,如:

aabbabac(1~8):

r(a)={1,2,5,7},n(a)=4;

r(ab)={3,6},n(ab)=2;

r(ba)={5,7},n(ba)=2;

r(aabba)={5},n(aabba)=1;

r(abba)={5},n(abba)=1……

引理1:对于两个子串s1,s2(length(s1)<=length(s2)),若r(s1)=r(s2),当且仅当s1在s中作为s2的后缀出现。

证明:略~(显然……吐舌头(其实还是很好想的,实在不懂请回复))

引理2:对于两个子串s1,s2(length(s1)<length(s2)),若s1为s2的后缀,则r(s2)⊆ r(s1),否则,r(s1)∩r(s2)=∅

证明:若s1为s2的后缀,即r(s1)与r(s2)至少有一个公共元素,意味着在其它r(s2)任意位置都出现过s1,但s1可能在其它地方出现,所以r(s2)⊆ r(s1);若s1不为s2的后缀,意味着它们末位置一定不会出现在同一点上,所以r(s1)∩r(s2)=∅

引理3:对于r集合相同的串,在原串s中出现一定是一段连续区间;但一段连续区间的子串的集合不一定属于同一集合

证明:由引理2得证,显然引理2与引理3是互补的

引理4:我们定义state为一个集合的状态(可以理解为树的节点编号,至于为什么,请往下看。如上例,串aabba与串abba在同一state里),则对于两个状态p,q,我们定义p为q的parent节点,当且仅当p是q的最小真包含的集合(在所有q⊆pi的集合中,n(pi)最小的一个),那么,对于任意一个状态q,有且仅有一个parent节点

证明:存在性显然,现在假设有p1,p2均为q的parent节点,则r(q)⊆ r(p1),r(q)⊆ r(p2),那么r(p1)与r(p2)有公共集合r(q),由引理2发现r(p1)与r(p2)可以合并,则最终只会存在1个parent节点,又因为每个状态有且仅有一个parent节点,所以这k个state状态构成了一颗树,我们姑且称这个树为fail树。

引理5:对于一颗状态树,它的总结点数不会超过2n-1个

证明:等比数列求和~

·fail树

首先,每个状态结点的fail指针指向它的parent

引理6:我们再定义maxl(k)为状态k的最长子串大小,同理minl(k)为其最小子串的大小,那么,对于状态p和q,若p为q的父亲,那么maxl(p)=minl(q)-1

证明:显然r(q)⊂ r(p),再次由引理2,我们可以证明minl(q)>maxl(p)……①,现在考虑一个minl(q)-1的串t,显然我们有r(q)⊂ r(t)⊆ r (p),由区间性质得到t∈{T|T=[minl(p),maxl(p)]},所以minl(q)-1<=maxl(p)……②,由①②得到maxl(p)=minl(q)-1

引理7:那么我们发现,一个串s[i],顺着它的fail指针走,能够补全它的所有后缀

证明:显然对于一个状态节点,它会包含长度递减的[minlen,maxlen]的所有串,而此节点的fail指针指向节点的最大长度恰好为此节点minlen-1,因此,最终我们可以不遗漏的走回到初始状态

恩,原理相信都懂了~

因为我是co的代码,并且对于构树网上都有很多讲解,就直接贴了

const int N = 2e5+10;
#define FOR(i,L,R) for(register int i=(L);i<=(R);++i)
struct Suffix{
	#define MAXNODE N*2//最大结点数
	char s[N];//原串 
	int rt,cnt,last,fal[MAXNODE],maxl[MAXNODE],trans[MAXNODE][130];
	//编号,上一个字符,值,fail指针,最大长度,转移(转移状态参照文本标准) 
	inline void insert(int f){
		int v=++cnt;//新建结点
		maxl[v]=maxl[last]+1;
		int p=last;last=v;//更新 
		while(p&&!trans[p][f])trans[p][f]=v,p=fal[p];
		if(!p)return fal[v]=rt,void();//所有结点均无f转移
		int q=trans[p][f];
		if(maxl[p]+1==maxl[q])return fal[v]=q,void();//接一个可以做到
		int newnode=++cnt;//拆点
		maxl[newnode]=maxl[p]+1;//最短的一个 
		memcpy(trans[newnode],trans[q],sizeof(trans[q]));
		fal[newnode]=fal[q];
		fal[q]=fal[v]=newnode;
		while(p&&trans[p][f]==q)trans[p][f]=newnode,p=fal[p];//如果我有此转移,我的所有fail指针均有此转移
	}
	inline void build(){
		gets(s);//防止有空格 
		rt=cnt=last=1;
		int len=strlen(s)-1;
		FOR(i,0,len)insert(s[i]);
	}
}

·SAM的应用

1)统计right集合

统计right集合有什么用呢?事实上,我们知道right集合(即上文的r集合)表示的是结尾字符的前缀,如果我们以正确的顺序统计right,我们能表示出以此字符串结尾的出现次数,这样,我们就可以求出以i为长度字符串的最大出现次数。

SPOJ8222重复子串

Description

给定字符串s,定义F(x)表示s的所有长度为x的子串中,重复出现次数最多子串在s中的出现的次数,两次出现可以有部分重叠。 
现给定字符串s,求F(1),F(2),...,F(length(s)).

Input

一行,即一个字符串s。注意均为小写字母

Output

共length(s)行,每行一个数。第i行为F(i)。

Sample Input

ababa

Sample Output

3

2

2

1

1

Hint

1<=length(s)<=250000

思路上文已经给出来了,直接给代码理解:

#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+5e4+10;
#define FOR(i,L,R) for(register int i=(L);i<=(R);++i)
#define REP(i,R,L) for(register int i=(R);i>=(L);--i)
struct Suffix_Automata{
	#define MAXNODE N*2
	char S[N];
	int n,rt,cnt,last;
	int fal[MAXNODE],maxl[MAXNODE],s[MAXNODE][26];
	int f[N],t[MAXNODE],rk[MAXNODE],sz[MAXNODE];
	inline void solv(){
		FOR(i,1,cnt)++rk[maxl[i]];
		FOR(i,1,n)rk[i]+=rk[i-1];//排名 
   		REP(i,cnt,1)t[rk[maxl[i]]--]=i;
		REP(i,cnt,1)sz[fal[t[i]]]+=sz[t[i]];//长的先加,怎么说?有点像拓扑序? 
		FOR(i,1,cnt)f[maxl[i]]=max(f[maxl[i]],sz[i]);
		//我们一定表示不完,所以用maxl[i]表示,之后用i+1更新i,简单dp 
		REP(i,n,1)f[i]=max(f[i],f[i+1]);
		//事实上,我们知道,既然我出现了k次,那么比我短的我的后缀一定出现了k次 
		FOR(i,1,n)cout<<f[i]<<'\n';
	}
	inline void insert(int f){
		int v=++cnt;
		maxl[v]=maxl[last]+1;
		sz[v]=1;//主串节点 
		int p=last;last=v;
		while(p&&!s[p][f])s[p][f]=v,p=fal[p];
		if(!p)return fal[v]=rt,void();
		int q=s[p][f];
		if(maxl[p]+1==maxl[q])return fal[v]=q,void();
		int New=++cnt;//拆点
		maxl[New]=maxl[p]+1;
		memcpy(s[New],s[q],sizeof(s[q]));
		fal[New]=fal[q];
		fal[q]=fal[v]=New;
		while(p&&s[p][f]==q)s[p][f]=New,p=fal[p];
	}
	inline void init(){
		scanf("%s",S+1);
		n=strlen(S+1);
		rt=cnt=last=1;
		FOR(i,1,n)insert(S[i]-'a');
	}
}z;
int main(){
	z.init();
	z.solv();
	return 0;
}

POJ3415公共子串Common Substrings

Description

题目大意:你有两个串A,B,给你一个数k,求A与B的公共子串里大于等于k的个数,注意位置不同另算

Input

1 ≤ |A|, |B| ≤ 10^5

1 ≤ K ≤ min{|A|, |B|}

Output

For the case, output an integer |S|.

Sample Input

2

aababaa

abaabaa

Sample Output

22

通过我和ljr大佬的激烈讨论,我们终于有了思路!这应该算我第一道自己做出来的后缀自动机的题?(别逗了,明明是ljr大佬带你飞的)有大写字母是真的坑!!!

一会儿写思路,先贴代码(因为oj数据水,我们就打纯暴力,听说poj要挂?不过暴力也只是nlog2n?):

//思路见下文 
#include<bits/stdc++.h>
#define max(a,b) ((a)<(b)?(b):(a))  
using namespace std;
const int N = 1e5+10;
#define FOR(i,L,R) for(register int i=(L);i<=(R);++i)
#define REP(i,R,L) for(register int i=(R);i>=(L);--i)
struct Suffix_Automata{
	#define MAXNODE N*2
	char S1[N],S2[N];
	int k,n1,n2,rt,cnt,last;
	int fal[MAXNODE],maxl[MAXNODE],s[MAXNODE][130];
	int t[MAXNODE],rk[MAXNODE],sz[MAXNODE];
	inline void Sort(){//桶排序,O(n)复杂度 
		FOR(i,1,cnt)++rk[maxl[i]];
		FOR(i,1,n1)rk[i]+=rk[i-1];
   		REP(i,cnt,1)t[rk[maxl[i]]--]=i;
		REP(i,cnt,1)sz[fal[t[i]]]+=sz[t[i]];
	}
	inline void solv(){
		Sort();
		long long ans=0;
		for(int p=rt,i=1,tmp=0;i<=n2;i++){//暴力,纯模拟了 
			int f=S2[i],cop=p;
			bool flag=1;
			if(!s[cop][f])flag=0;
			while(cop&&!s[cop][f])cop=fal[cop];
			if(!cop)cop=rt,tmp=0;
			if(!flag)tmp=maxl[cop];
			if(s[cop][f])cop=s[cop][f],++tmp;
			p=cop;
			while(cop)ans+=(long long)max((min(maxl[cop],tmp)-max(maxl[fal[cop]]+1,k)+1),0)*(long long)sz[cop],cop=fal[cop];
			//感觉会不会头晕目眩?
			//第一次去max是为了防止负数
			//取min是因为你到cop之后,实际匹配数tmp不一定会到maxl[cop]
			//去max也是同理 
		}cout<<ans<<"\n";
	}
	inline void insert(int f){
		int v=++cnt;
		maxl[v]=maxl[last]+1;
		sz[v]=1;
		int p=last;last=v;
		while(p&&!s[p][f])s[p][f]=v,p=fal[p];
		if(!p)return fal[v]=rt,void();
		int q=s[p][f];
		if(maxl[p]+1==maxl[q])return fal[v]=q,void();
		int New=++cnt;//拆点
		maxl[New]=maxl[p]+1;
		memcpy(s[New],s[q],sizeof(s[q]));
		fal[New]=fal[q];
		fal[q]=fal[v]=New;
		while(p&&s[p][f]==q)s[p][f]=New,p=fal[p];
	}
	inline void init(){
		scanf("%d%s%s",&k,S1+1,S2+1);
		n1=strlen(S1+1);
		n2=strlen(S2+1);
		rt=cnt=last=1;
		FOR(i,1,n1)insert(S1[i]);
	}
}z;
int main(){
	z.init();
	z.solv();
	return 0;
}

这道题主要是加深我们对于r集合的理解,因为不同位置要另算的缘故,我们走到一个状态之后,自然它r集合的所有元素都应该算进去,我们能感性地认识此方法是正确的。

因为对于一个状态,你的每一个转移状态是唯一的,也就是说,当你新匹配了一个字母之后,公共串增加了,我们就得到了一个新的串,显然是要加进去的,这样每一次转移,我们都能得到一个唯一的匹配,并且能将它完整地拼起来,因此不会漏也不会重复。不知道我说清楚没有?如果还不了解请回复

持续更新ing~

猜你喜欢

转载自blog.csdn.net/dancingz/article/details/80542598
今日推荐