后缀自动机初探

定义

给定字符串S, S 的后缀自动机(SAM)是一个能够识别S 的所有后缀的自动机

一些记号

  • trans(s,x):状态s走x转移到达的状态
  • reg(s):状态s能接受的状态,即trans(s,str)属于end的所有str

功能

  • 识别后缀:trans(init,str)属于end
  • 识别子串:trans(init,str)!=null

朴素实现

将所有后缀加入Trie树即可,但这样节点数是n^2的。

一堆性质

  • 对于一个状态,其本质属性是他能接受哪些字符串,即reg(s)。
  • 考虑一个原串的一个子串a,trans(init,a)=X,则reg(X)是一堆后缀的集合,这些后缀的性质是:若a出现在区间[l1..r1-1],[l2..r2-2],…中,则reg(X)={Suffix(r1),Suffix(r2),…}
  • 定义right(X)={r1,r2,r3,…}其意义是串a在原串的这些位置出现,且仅在这些位置出现(右端点是这些位置),即出现位置的最大集合。
  • 对于两个子串a,b,right(a)=right(b)等价于trans(init,a)=trans(init,b)。所以一个状态s识别的子串,由所有Right 集合是Right(s) 的字符串组成。
  • 对于一个right集合Q,满足right(trans(init,str))=Q的str的长度是一段连续的区间[l..r],令Min(str)=l,Max(str)=r。因为如果长度越大,right集合就可能变小,长度越小,right集合就可能变大。
  • 考虑两个状态a, b,它们的right 集合分别为Ra,Rb,假设Ra 与Rb 有交集,不妨设r属于ra交rb,若ra交rb不为空,则ra,rb是包含关系。
  • 证明:不妨设a 中所有串都小于b 中所有串。因为都是由r 往前,所以a中串都是b 中串的后缀。所以Ra 是Rb 的真子集。
  • 那么任意两个串的Right 集合,要么不相交,要么一个是另一个的真子集。
  • 如果ra交rb不为空,则由于a 和b 表示的子串不会有交集,则[Min(a),Max(a)]与[Min(b),Max(b)] 不会有交集(一旦有交集,可证这两个集合全等)。
  • 于是我们可以看出Right 集合实际形成了一个树的结构。不妨称为parent 树,在这个树中,叶节点的个数只有N 个,所以树的大小必然是O(n)的(因为每次父节点相当于是子节点集合的并),随着深度的减小,Max(s)的值在减小。
  • 一个性质:Min(s)=Max(s->fa)+1,等价形式:Max(fa)=Min(s)-1
  • 可以这么想:fa集合的每个元素向左扩展1位,得到新的一些字符串,这些字符串会按是否相等分成几组。因为要满足fa是s的并,就要小于等于s的最小值,又因为区间不能相交,就必须要小于。
  • 另外注意:一个父节点的子节点的Min都是相等的(因为其子节点的最小值必须相等,不然就可以将最小值更大的作为最小值最小的子树,eg:len+2可以作为len+1的子节点)。Max值需要另求,暴力是O(n)的。
  • parent 树也是S 逆序的后缀树。可以求出这个子串出现的个数,就是所在状态Right 集合的大小。
  • parent 树里一个节点的Right 集合就是它所有儿子Right 集合的并集,也是dfs 序里连续的一段。同时在后缀自动机中某个点失配后,沿着parent指针可以到达一个新的状态,使得新的状态的后缀等于当前匹配的串的后缀。沿着后缀自动机的边走,right集合会变小,但并不是包含关系,因为位置向后移动了一个。
  • 维护一个串在前面插入字符转移到哪个状态:后缀数组维护,二分新加串的所在的后缀数组的位置

构造后缀自动机

增量法:每次在串的末尾添加一个字符。对于Trie,BFS,last指针指向父亲即可建立np=trans(last,x),则对于p=last的向上的路径,如果没有x转移,将其连接到x,这里相当于是其集合走x转移转到了right集合={len+1}的集合。因为一个状态有x转移等价于right(此状态)中至少有一个元素有x转移。
eg

  • aab???aab???aabx
  • 0010000200003
  • {1,2,3}{2,3}{3}可以进行x转移,而{1},{1,2}不行。
  • 如果找到了一个有x转移的节点p,令其trans(p,x)=q
  • 若Max(q)=Max(p)+1,则可以将np并入q:np->pa=q
  • 否则为保证right的正确性,需要建立新节点nq=right(q)并right(np),这样将q的转移信息复制给nq,将这一路向上的所有以q为x转移的节点,用nq代替即可。(只记录max值的原因:Max(fa)=Min(s)-1,其子节点的l都相等)

Code 建立后缀自动机

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;
const int MAXN=1000,INF=0x3f3f3f3f;
const int NS=26;
struct node{
    int ch[NS],pa,len;
        void init(int l=0){len=l;pa=0;memset(ch,0,sizeof(ch));}
};
struct SAM{
    node ns[MAXN+3];
    int root,last,tot;
    int newnode(int l){
        ns[++tot].init(l);
        return tot;
    }
    void init(){
        tot=0;root=last=newnode(0);
    }
    void append(int x){
        int p=last,np=newnode(ns[p].len+1);
        for(;p && ns[p].ch[x]==0;p=ns[p].pa)ns[p].ch[x]=np;
        if(p==0)ns[np].pa=root;
        else{
            int q=ns[p].ch[x];
            if(ns[q].len==ns[p].len+1)ns[np].pa=q;
            else{
                int nq=newnode(ns[p].len+1);
                memcpy(ns[nq].ch,ns[q].ch,sizeof(ns[q].ch));
                ns[nq].pa=ns[q].pa;
                ns[q].pa=ns[np].pa=nq;
                for(;p && ns[p].ch[x]==q;p=ns[p].pa)ns[p].ch[x]=nq;
            }
        }
        last=np;
    } 
    int trans(int *s,int len,int init){
        for(int i=0;i<len;i++){
            if(ns[init].ch[s[i]]==0)return -1;
            init=ns[init].ch[s[i]];
        }
        return init;
    }
}sam;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值