后缀自动机(探索)Codeforces 427D

想学后缀自动机的 弱鸡 表示真自闭啊

开局一道题,内容全靠水 :Codeforces 427D

有题目才能更好地学 算法

题意很简单 :给两个字符串,求最短公共子串 的长度

后缀自动机 模板很多 ,给了各种写法 , 匡斌的版本 , 一个俄罗斯人的版本 , clj的版本 ,还有一种全数组(不用结构体)的版本

可真是太开心了(???我一个小白,到底哪个好理解??)

 

 ● 如果你是一个论文爱好者可以看看这个:https://blog.csdn.net/qq_35649707/article/details/66473069

全是俄文翻译,全是概念

 ● 而民间大神版本,看了这么多,对原理   和    应用    所做的中文解释  能看懂  且没歧义的版本:

https://blog.csdn.net/doyouseeman/article/details/52245413

 

各路大神是很厉害 , 可是冗长的一句话,断句阅读  给个空格  可好,逗号根本不顶事啊(很难受)

经过切身实践,我表示  数组 版本的最好理解,问我为什么?

(问我数组版本在哪学到的?下文讲解里有个链接,在那看到的)

(注:节点只表示状态,而边表示字母)

你看这通俗的数组名称

  1. son 当前状态节点 通过字母x  推往下一个状态节点 ( 每一个状态节点都有一个角标last,这个角标一直在增加 )
  2. pre 当前状态节点   前推上一个  可更新状态的  角标
  3. step[i]  从空串0  到  状态i 的最长子串 长度
  4. total 总共有多少个状态节点 (数量)

对应结构体版本

  • len - step
  • link - pre
  • son - next 用map写或者next[26]都行
  • last - last 还是当前最后一个状态节点
  • total - sz 状态节点总数

所以构造方式。。。我们还是用结构体(真香)

struct state {
	int len,link;
	map<char,int> next;
};
state st[Maxn*2];
int sz, last;

要注意的一点是 , 如果只是一个单独的字符串建立后缀自动机 , 大佬们说只要用Maxn * 2 个节点就够了

那么首先我们要进行初始化操作

void sa_init() {
	sz = last = 1;
}

状态节点 以后简称 节点

一开始什么都没有,我们把 1节点 定义为 一个空串  的状态节点

 

然后就是在线O(n)建树的过程

因为我们要一个一个字母的往树里加,所以下面的sa_extend 函数 就是一个字母  往里加  过程

里面有一些注释:帮助理解,后面的AC代码 ,除新加的注释 , 当前帮助学习的注释都会去掉

void sa_extend (char c) {
	int p = last;
    //一个用来  推  状态的节点下标
	int np = last = ++sz;
    //更新last ,同事新建一个 经过  边c  的  np新状态
	st[np].len = st[p].len + 1;
	for (; p && !st[p].next[c]; p=st[p].link)
		st[p].next[c] = np;
	if (p == 0){
		st[np].link = 1;
	} else {
		int q = st[p].next[c];
		if (st[p].len + 1 == st[q].len){
			st[np].link = q;
		} else {
			int nq = ++sz;
			st[nq].next = st[q].next;
			st[nq].len = st[p].len + 1;
			st[nq].link = st[q].link;
			st[q].link = st[np].link = nq;
			for (; p && st[p].next[c]==q; p=st[p].link)
				st[p].next[c] = nq;
		}
	}
}

np 是当前 即将 加到  树里的 新节点

所以接下来到此结束

直接讲一下为什么这么建立后缀树

首先我们要了解

  • 说是后缀树  ,但是它从根节点  遍历边  按一条线路  顺序输出的   还是  某个子串的  正顺序,并不是逆顺序  

 惊不惊喜,但是它实际是某一个 节点   掌管    其所有之前  能控制的  节点

什么叫能控制? 从源点走到之前的所有状态  都没出现过   c   字符的时候 , c就能控制之前所有的状态

那么如果出现了,某一个状态节点 ,它的下一条边已经出现了 c ,这就是到了 当前的新节点 所不能掌控的 节点了

那么分两种情况

看了这么多后缀自动机博客 , 这一部分讲的最好的 ,最容易懂得 就是

但是在这里面 , np 实际是用cur来表示的 , 而且新建的 节点是 clone 代替上面代码里的 q

http://blog.sina.com.cn/s/blog_70811e1a01014dkz.html#cmt_533F66DF-7F000001-75D82355-8B8-8A0

这位大佬的讲解。(后文还有新的东西)

欢迎回来。。

接下来我们回到题目上来 , 应用后缀自动机

还记得吧

这里面我们应对两个字符串建立的 一个后缀自动机

但是建立自动机的时候有一个问题,第二个字符串 再次从 1节点   开始创建

那么一些重复的 会再次被创建怎么办

//因为要建两次树,重复的就要跳过
if(st[last].next[c] && st[last].len + 1 == st[st[last].next[c]].len){
	last = st[last].next[c];
	return;
}

这一段就是检验  相邻的两个连续节点  是不是可以取代 新创建的过程(其实看代码更容易懂)

思路中用到了  拓补  我一个小白怎么知道拓补? 我看了一堆(真的是一堆)博客才发现的,用这个的人没说明这一步是干什么的

他从拓补序从后往前推同时更新了其中的长度 , endpos是拓补数组标记

什么时候更新答案?建立自动机的时候会标记这个  状态节点是  s1串s2串 或者 两个经过的 节点,如果这个节点两个串都更新到了,那么就可以和ans 去个min ,更新到ans。

大佬们还是大佬,写的代码都不解释,直接上代码

反正我的第一道后缀自动机就这么自闭的结束了 , 还要做这类型的题才能真的融会贯通吧

AC代码

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <iostream>
#include <cmath>
#include <map>
#include <queue>
#include <algorithm>
#include <set>
#include <vector>
#include <stack>
#define Clear( x , y ) memset( x , y , sizeof(x) );
#define Qcin() std::ios::sync_with_stdio(false);
using namespace std;
typedef long long LL;
const int Inf = 1e9 + 7;
const int Maxn = 5007;
int N , M;
char s[Maxn];
struct SAM{
	int endpos[2][Maxn*4];
	int a[Maxn*4] , b[Maxn*4];
	struct state {
		int len,link;
		map<char,int> next;
		void init()
		{
			len = 0 , link = 0;
		}
	};
	state st[Maxn*4];
	int sz, last;

	void sa_init() {
		sz = last = 1;
	}

	void sa_extend (char c) {
		//因为要建两次树,重复的就要跳过
		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 && !st[p].next[c]; p=st[p].link)
			st[p].next[c] = np;
		if (p == 0){
			st[np].link = 1;
		} else {
			int q = st[p].next[c];
			if (st[p].len + 1 == st[q].len){
				st[np].link = q;
			} else {
				int nq = ++sz;
				st[nq].next = st[q].next;
				st[nq].len = st[p].len + 1;
				st[nq].link = st[q].link;
				st[q].link = st[np].link = nq;
				for (; p && st[p].next[c]==q; p=st[p].link)
					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 solve(){
		int ans = 9999;
		//cout << "# sz = " << sz << endl;
		for(int i = sz ; i > 1 ; i--){
			int e = b[i];
			//if(st[e].link == -1)	continue;
			if(endpos[0][e] == 1 && endpos[1][e] == 1){
				ans = min(ans , st[st[e].link].len + 1);
			}
			//cout << "ans = " << ans << endl;
			endpos[0][st[e].link] += endpos[0][e];
			endpos[1][st[e].link] += endpos[1][e];
		}
		if(ans == 9999)	printf("-1\n");
		else	printf("%d\n",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]);
		sam.endpos[0][sam.last] = 1;
	}
	//cout << sam.sz << " " << sam.last << endl;
	sam.last = 1;
	scanf(" %s",s);
	len = strlen(s);
	for(int i = 0 ; i < len ; i++){
		sam.sa_extend(s[i]);
		sam.endpos[1][sam.last] = 1;
	}
	sam.Topo();
	sam.solve();
	return 0;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值