后缀系列——后缀自动机

后缀自动机:

首先在理解kmp , ac自动机 , 后缀数组的基础上 , 我们再讨论后缀自动机

http://hihocoder.com/problemset/problem/1441

真的是太经典了,好好看看绝对理解。

下图字符串 S="aabbabd"

性质:

  1. 字符串的后缀都能到 终结态
  2. S的所有子串都能到达一个合法态(从某个节点转移到另一个节点)
  3. 不是S子串的 字符串 最终会没有路可走

子串结束集合——endpos字符集:

把所有子串的endpos 都求出来,两个子串的endpos相等 归为一类

每一个endpos节点都记录了 一个的原串后缀的前缀,它可以包含很多个子串,最长的用len[s] 表示,最短的是shortlen[s]表示。

我们知道一个 子串 的所有后缀不是在同一个endpos的,它还可能在别的endpos里,我们的link就让它其他的endpos后缀指向它本身。也就只说从前向后越来越长,但是在同一个后缀上。

匹配的时候如果找一个匹配串的T[i] 找不到,那么就需要在保持之前已经匹配的基础上,向主串的后面跳,看看是否能找到其他的子串 S", 且S" 的后缀和刚才我们匹配成功的后缀一样,如果找到了,就继续对比T[i]是不是能成功匹配。

link的作用:

新建一个cur状态节点

  • 当前插入一个字母c,变到cur状态,它通过link向前跳,直到空节点都没一个出现一个pre状态,pre状态的下一个字符是c,那么我们让link[cur] = 0
  • 如果有一个q状态,它是由前一个p状态通过c转移过来的就需要分两种情况
  1. q状态 和 前一个p状态的关系是 len[q] = len[p] + 1 , 我们就可以让link[cur] = q
  2. 否则就出现了len[q] > len[p] + 1,我们需要copy一个 q 的clone,让len[clone] = len[p] + 1,让cur指向clone,然后 开始走 p 的link 看是否有指向q的转移,如果有就让它指向clone
  • 最终停下后,我们更新last=cur

 

 

时空复杂度:

边和点 根据CLJ的 PPT上所说 , 都是O(n)的 , 时间复杂度 也是 O(n) 的 ,毕竟有fail 指针加速

我们这里所说的fail指针和link指针是同一个东西,只不过我们后缀自动机中的性质保证了我们的fail可以从后向前指的都是同一个endpos的后缀。

而AC自动机没有相同后缀这一性质,它是把匹配串建出字典树,不能保证相同后缀的len逐渐递减,只能在匹配失败后向前跳,保证它能匹配到不同的 待匹配串。

那么就先来个 最简单的 后缀自动机 跑两个字符串的 最长连续 公共子串 的大小

当个简单模板吧

题目:SPOJ - LCS

给两个字符串 求 最长公共子串

const int Maxn = 250007;
int N , M;
char s[Maxn];
struct SAM{
	struct node {
		int len,fail;
		//map<char,int> next;
		int next[26];
		void init() { len = 0 , fail = 0; }
	};
	node st[Maxn*4];
	int sz, last;
 
	void sa_init() {
		sz = last = 0;
		st[last].fail = -1;
	}
 
	void sa_extend (char c) {
		c = c - 'a';
		/*
		//建两次树,重复的就要跳过
		if(st[last].next[c] && st[last].len + 1 == st[st[last].next[c]].len){
			last = st[last].next[c];
			return;
		}
		*/
 
		int p = last;
		int np = last = ++sz;
		st[np].len = st[p].len + 1;
		for (; p != -1 && !st[p].next[c]; p=st[p].fail)
			st[p].next[c] = np;
		if (p == -1){
			st[np].fail = 0;
		} else {
			int q = st[p].next[c];
			if (st[p].len + 1 == st[q].len){
				st[np].fail = q;
			} else {
				int nq = ++sz;
				//st[nq].next = st[q].next;
				for(int i = 0 ; i < 26 ; i++)	st[nq].next[i] = st[q].next[i];
				st[nq].len = st[p].len + 1;
				st[nq].fail = st[q].fail;
				st[q].fail = st[np].fail = nq;
				for (; p != -1 && st[p].next[c]==q; p=st[p].fail)
					st[p].next[c] = nq;
			}
		}
	}
	int suffix_Automaton(){
		int p = 0 , len = 0;
		int ans = 0;
		for(int i = 0 ; s[i] ; i++){
			char c = s[i] - 'a';
			while(p != -1 && st[p].next[c] == 0){
				p = st[p].fail;
			}
			if(p == -1){
				p = len = 0;
			} else {
				len = min(len , st[p].len);
				p = st[p].next[c];
				len++;
			}
			ans = max(ans , len);
		}
		return ans;
	}
}sam;
 
int main()
{
	scanf(" %s",s);
	int len = strlen(s);
	sam.sa_init();
	for(int i = 0 ; i < len ; i++){
		sam.sa_extend(s[i]);
	}
	scanf(" %s",s);
	int ans = 0;
	ans = sam.suffix_Automaton();
	printf("%d\n",ans);
	return 0;
}

这道题由于是 250000 的字符串 , 所以我用了map超时 了 , 虽然感觉每个节点只有26个 , map用起来和 int数组一样快 , 但是冒有办法。

 

题目:SPOJ - LCS2

给  t  (1 < t<= 10)个字符串 , 求最长公共子串

topo部分:

topo是用来求一个字符串匹配成功以后,我们把整个图拓补出来,我们在上面的endpos字符集里面发现匹配的 len长度 可能会偏长,所以 拓补 之后如果找到了匹配成功的,那么将之前的 爷爷fail指针 的len向后 传递 给 父亲fail节点

这样从(sz ~~ 1) 的过程 中就能维护处 每次匹配 成功维护出来的 最长公共子串的 最小值。

 

这里 就不能 像之前那样 直接建树 然后跑一遍s a匹配就求出答案的。

之前做了一个 Codeforces 427D (给出两个字符串,求最小公共子串长度,使这个子串仅在两个串均出现一次), 被里面的两次建树思想带跑了,写成了多次建树,然后更新里面的一个新变量sum , 但是不能保证 单独一个SS1串 里面 不多次出现的子串str 。如果第一个串里出现了 >= 10次 , 后面2 ~ 8个串没出现,最后一个串出现了一次str ,那么匹配的时候就会算错。

const int Inf = 1e9 + 7;
const int Maxn = 100007;
int N , M;
int fc[Maxn<<2] , fmn[Maxn<<2];
char s[Maxn];
struct SAM{
	int a[Maxn<<2] , b[Maxn<<2];
	struct node {
		int len,fail;
		int sum;
		//map<char,int> next;
		int next[26];
		void init() { len = 0 , fail = 0; }
	};
	node st[Maxn << 1];
	int sz, last;
 
	void sa_init() {
		sz = last = 0;
		st[last].fail = -1;
		fill(fc , fc+Maxn*2 , Inf);
	}
 
	void sa_extend (char c) {
		c = c - 'a';
		/*
		//建多次树,重复的就要跳过
		if(st[last].next[c] && st[last].len + 1 == st[st[last].next[c]].len){
			last = st[last].next[c];
			//
			st[last].sum++;
			return;
		}
		*/
 
		int p = last;
		int np = last = ++sz;
		st[np].len = st[p].len + 1;
		for (; p != -1 && !st[p].next[c]; p=st[p].fail)
			st[p].next[c] = np;
		if (p == -1){
			st[np].fail = 0;
		} else {
			int q = st[p].next[c];
			if (st[p].len + 1 == st[q].len){
				st[np].fail = q;
			} else {
				int nq = ++sz;
				//st[nq].next = st[q].next;
				for(int i = 0 ; i < 26 ; i++)	st[nq].next[i] = st[q].next[i];
				st[nq].len = st[p].len + 1;
				st[nq].fail = st[q].fail;
				st[q].fail = st[np].fail = nq;
				for (; p != -1 && st[p].next[c]==q; p=st[p].fail)
					st[p].next[c] = nq;
			}
		}
	}

	void Topo(){
		int os = 1;
		for(int i = os ; i <= sz ; i++){
			++a[st[i].len];
		}
		for(int i = os ; i <= sz ; i++){
			a[i] += a[i-1];
		}
		for(int i = os ; i <= sz ; i++){
			b[a[st[i].len]--] = i;
		}
	}

	void suffix_Automaton(){
		Clear(fmn , 0);
		int p = 0 , len = 0;
		int ans = 0;
		for(int i = 0 ; s[i] ; i++){
			char c = s[i] - 'a';
			while(p != -1 && st[p].next[c] == 0){
				p = st[p].fail;
			}
			if(p == -1){
				p = len = 0;
			} else {
				len = min(len , st[p].len);
				p = st[p].next[c];
				len++;
			}
			fmn[p] = max(fmn[p] , len);
		}
		for(int i = sz ; i >= 1 ; i--){
			int pos = b[i];
			fc[pos] = min(fc[pos] , fmn[pos]);
			if(fmn[pos]){
				//之前的会出现重复的子串,所以要将之前的len向后移
				//它的len并没有被改变
				fmn[st[pos].fail] = st[st[pos].fail].len;
			}
		}
	}
}sam;
 
int main()
{
	sam.sa_init();
	scanf(" %s",s);
	for(int i = 0 ; s[i] ; i++){
		sam.sa_extend(s[i]);
	}
	sam.Topo();
	while(~scanf(" %s",s)){
		sam.suffix_Automaton();
	}
	int ans = 0;
	for(int i = 1 ; i <= sam.sz ; i++){
		ans = max(ans , fc[i]);
	}
	printf("%d\n",max(0,ans));
	return 0;
}

感谢大佬列出的例题:(https://www.cnblogs.com/xzyxzy/p/9186759.html

  • [hihocoder1441]后缀自动机一·基本概念 http://hihocoder.com/problemset/problem/1441
  • [hihocoder1445]后缀自动机二·重复旋律5 http://hihocoder.com/problemset/problem/1445
  • [hihocoder1449]后缀自动机三·重复旋律6 http://hihocoder.com/problemset/problem/1449
  • [hihocoder1457]后缀自动机四·重复旋律7 http://hihocoder.com/problemset/problem/1457
  • [hihocoder1465]后缀自动机五·重复旋律8 http://hihocoder.com/problemset/problem/1465
  • [HihoCoder1413]Rikka with String
  • [Luogu3804]【模板】后缀自动机 https://www.luogu.org/problemnew/show/P3804
  • [BZOJ4516][SDOI2016]生成魔咒
  • [BZOJ3998][TJOI2015]弦论 https://www.luogu.org/problemnew/show/P3975
  • [BZOJ3277]串 https://www.lydsy.com/JudgeOnline/problem.php?id=3277
  • [BZOJ3926][ZJOI2015]诸神眷顾的幻想乡
  • [BZOJ2806][CTSC2012]熟悉的文章(Cheat) https://www.luogu.org/problemnew/show/P4022
  • [BZOJ1396&2865]识别子串
  • [HEOI2015]最短不公共子串 https://www.luogu.org/problemnew/show/P4112
  • [BZOJ2555]SubString
  • [BZOJ5137][Usaco2017 Dec]Standing Out from the Herd
  • [BZOJ2780][SPOJ8093]Sevenk Love Oimaster https://www.luogu.org/problemnew/show/SP8093
  • [CF700E]Cool Slogans https://www.luogu.org/problemnew/show/CF700E
  • [CF666E]Forensic Examination
  • [BZOJ5408][HN省队集训6.25T3]string https://www.lydsy.com/JudgeOnline/problem.php?id=5408
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值