想学后缀自动机的 弱鸡 表示真自闭啊
开局一道题,内容全靠水 :Codeforces 427D
有题目才能更好地学 算法
题意很简单 :给两个字符串,求最短公共子串 的长度
后缀自动机 模板很多 ,给了各种写法 , 匡斌的版本 , 一个俄罗斯人的版本 , clj的版本 ,还有一种全数组(不用结构体)的版本
可真是太开心了(???我一个小白,到底哪个好理解??)
● 如果你是一个论文爱好者可以看看这个:https://blog.csdn.net/qq_35649707/article/details/66473069
全是俄文翻译,全是概念
● 而民间大神版本,看了这么多,对原理 和 应用 所做的中文解释 能看懂 且没歧义的版本:
https://blog.csdn.net/doyouseeman/article/details/52245413
各路大神是很厉害 , 可是冗长的一句话,断句阅读 给个空格 可好,逗号根本不顶事啊(很难受)
经过切身实践,我表示 数组 版本的最好理解,问我为什么?
(问我数组版本在哪学到的?下文讲解里有个链接,在那看到的)
(注:节点只表示状态,而边表示字母)
你看这通俗的数组名称
- son 当前状态节点 通过字母x 推往下一个状态节点 ( 每一个状态节点都有一个角标last,这个角标一直在增加 )
- pre 当前状态节点 前推上一个 可更新状态的 角标
- step[i] 从空串0 到 状态i 的最长子串 长度
- 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;
}