首页 > 其他 > 详细

[学习笔记]后缀自动机

时间:2018-11-17 17:21:50      阅读:166      评论:0      收藏:0      [点我收藏+]

解决大部分字符串问题的大杀器

给一下clj课件:戳我

 

一、前言

字符串常见问题:各种匹配

1.LCP

2.子串匹配

3.模式串和主串匹配

4.回文串?也算匹配吧。。。

 

经典例子:

给两个串,求最长公共子串长度。

用hash表可以做到nlogn

 

后缀自动机可以做到O(n)

 

二、概念

我们需要一个可以识别某个串是否为某个主串的子串、处理主串的子串信息、串和串之间进行匹配的问题。

朴素的做法是,对每个子串建立一个节点,然后连边,O(n^2)

 

考虑的优化是,发现一些子串会出现很多次,一些子串出现所有的出现位置的集合是同一个集合!

对这个进行优化

1.Right集合

对于某个串s,所有出现的位置的集合是:{r1,r2,...,rm}

那么,这个集合就是Right(s)

由于许多的串si,可能Right集合都是Right(s)

如果我们给定集合Right(t)

再给定属于这个集合的字符串的长度len

随便选择一个r,那么这个字符串就已经唯一确定了。

一个Right集合会包含若干个字符串

那么,其len的范围一定是一个区间[max,min]

 

那么,为了处理方便,我们会把这些串压缩在一起。

保留这个Right集合的max

 

我们就会定义Right(s)为一个“状态”

这个“状态”,包含了所有的可能匹配到的节点末尾的位置ri,以及可能已经匹配出来的字符串集合si

 

*性质:两个Right集合要么不相交,要么一个是另一个的真子集

证明:

首先易证,一个字符串属于且仅属于一个Right集合。

设两个集合s1,s2相交,r是交集的某个元素

那么对于一个属于s1的字符串string1

一个属于s2的字符串string2

由于两个字符串都可以以r作为末尾出现一次,由于一个字符串属于且仅属于一个集合

所以,string1!=string2

不妨设len(string1)=l1<len(string2)=l2

那么,显然地,string2出现的位置,string1必然也会出现。

所以,s2中有的元素ri,s1中必然也有

并且,必然存在一个元素r0,满足s1中有,但是s2中没有(否则s1,s2其实是一个集合)

那么s2就是s1的真子集。

证毕。

 

2.Parent树

发现,Right集合的两个性质非常给力。

真子集的包含关系可以看做父子关系

Right集合之间的不相交关系可以看做非祖宗关系。

图示:from clj

技术分享图片

可以发现,对于集合字符串长度,有mx[fa[i]]+1=min[i]

进而可以这样理解:

father集合的结束位置更多,所以满足的字符串长度必然更短。

而且,有mx[fa[i]]+1=min[i]

叶子节点只有一个r

那么,如果从某个叶子节点往上找,那么所遍历的长度,就是以r结尾的所有子串。

 

三、SAM的构造

 

数组:(节点及$Right$集合,即所谓的“状态”)

$fa[i]$——节点$parent$树上的$father$

$ch[i][∑]$——节点的出边(类比$trie$树),指向添加字符$x$后到达的下一个节点($s[ri+1]==x$的会继续匹配上,这些$ri$就构成了目标的节点。如果没有,那么就指向$NULL$(实际不会操作,即$0$号节点就是$NULL$节点))。$∑$是字符集大小

$len[i]$——某个节点的所有字符串中最长的长度。

$nd$——当前匹配的最后一个节点$p$。若当前是$L$,则$Right(p)={L}$

$cnt$——节点个数。


我令$rt$节点为$1$

开始$nd=rt$

增量法构造,即每次插入一个字符$x$。设当前已经插入的字符串长度是$L$

我们要做的,就是新加入这个节点之后,维护好所有的后缀(使得后缀都能走出来)。

需要维护$fa$指针,和$ch[∑]$指针。


(以下祖先指$parent$树上的祖先)

建立一个$np$节点,其实有$Right(np)={L+1}$

赋值:$len[np]=L+1$

对于$p$的祖先,$p_1,p_2,...p_k$

存在一段$p_1,p_2,...p_{v-1}$

满足都没有$ch[*][x]$的边(也即都指向$NULL$)

对于$p_v$存在一个指向$x$的边

设$q=ch[p_v][x]$,

存在两种情况

$I$

$len[q]==len[p_v]+1$

那么意味着,之前所有的状态$q$代表的位置都可以加一个$x$匹配上将要加入的字符

那么可以直接把$q$当做$np$的$father$

(这个时候,其实$q$的$Right$集合发生了变化,已经加入了一个位置$L+1$)

就可以结束这次插入。


$II$

$len[q]>len[p_v]+1$

这是什么意思?

图示:(第一个串有一个B错了)
技术分享图片

 


那么意味着,$q$抛弃了一些$p_v$的存在位置,并且使得长度增加了。

那么,$q$一定抛弃了以$L$结尾的位置。

否则,会存在一个离p更近的祖先,满足存在指向$x$的边。

所以,这样的$q$匹配上$x$之后,一定不会包含$L+1$位置。


那么,这个$q$,一定不能作为$np$的$father$(因为不包含$L+1$这个位置)

所以,我们新建一个节点$nq$

这个$nq$的$Right$集合,其实是:$Right(q)∪\{L+1\}$

我们还要对$nq$进行一些处理。

$fa[nq]=fa[q]$

$fa[q]=nq$

$fa[np]=nq$

$len[nq]=len[p_v]+1$

对于$nq$,所有$q$的出边,都是$nq$的出边

对于$ch[p_i][x]==q$的$p$的祖先(一定是连续一段),把出边$ch[p_i][x]$都设置为$nq$


然后就可以结束了。


代码:

luoguP3804 【模板】后缀自动机

(注意,点数是2倍)

#include<bits/stdc++.h>
#define reg register int
#define il inline
#define numb (ch^‘0‘)
using namespace std;
typedef long long ll;
il void rd(int &x){
    char ch;x=0;bool fl=false;
    while(!isdigit(ch=getchar()))(ch==-)&&(fl=true);
    for(x=numb;isdigit(ch=getchar());x=x*10+numb);
    (fl==true)&&(x=-x);
}
namespace Miracle{
const int N=3e6+6;
int n,m;
ll ans;
char s[N];
struct SAM{
    int ch[N][30],cnt;
    int fa[N],nd;int sz[N];
    int hd[N],tot;
    int len[N];
    struct node{
        int nxt,to;
    }e[2*N];
    void init(){nd=1;cnt=1;}
    void add(int x,int y){
        e[++tot].nxt=hd[x];
        e[tot].to=y;
        hd[x]=tot;
    }
    void ins(int c,int pos){
        int p=nd;sz[nd=++cnt]=1;
        len[nd]=pos;
        for(;p&&ch[p][c]==0;p=fa[p]) ch[p][c]=nd;
        if(p==0) {fa[nd]=1;return;}
        int q=ch[p][c];
        if(len[q]==len[p]+1){fa[nd]=q;return;}
        len[++cnt]=len[p]+1;
        for(reg i=1;i<=26;++i) ch[cnt][i]=ch[q][i];
        fa[cnt]=fa[q];fa[q]=cnt;fa[nd]=cnt;
        for(;p&&ch[p][c]==q;p=fa[p]) ch[p][c]=cnt;
    }
    void build(){
        for(reg i=2;i<=cnt;++i) add(fa[i],i);
    }
    void dfs(int x){
        for(reg i=hd[x];i;i=e[i].nxt){
            dfs(e[i].to);sz[x]+=sz[e[i].to];
        }
        if(sz[x]!=1) ans=max(ans,(ll)sz[x]*len[x]);
    }
}sam;
int main(){
    scanf("%s",s+1);
    n=strlen(s+1);sam.init();
    for(reg i=1;i<=n;++i) sam.ins(s[i]-a+1,i);
    sam.build();sam.dfs(1);
    printf("%lld",ans);return 0;
}
 
}
int main(){
    Miracle::main();
    return 0;
}

/*
   Author: *Miracle*
   Date: 2018/11/16 21:54:31
*/

 

 

 

四、证明

 

维护了所有的后缀,所以维护了所有的子串。

并且Parent指针类似于AC自动机的fail指针,转到了可能可以继续匹配的某个出现过的、更短的字符串。

跳一次Parent,匹配长度至少-1

所以,能失配后,快速利用保留信息,所以也可以支持匹配。

 

五、应用

SP1811 LCS - Longest Common Substring

 

P3804 【模板】后缀自动机

 

SP8222 NSUBSTR - Substrings

 

P1368 工艺

 

六、理解

1.和AC自动机比较

 

2.

[学习笔记]后缀自动机

原文:https://www.cnblogs.com/Miracevin/p/9974743.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!