后缀自动机再学习

后缀自动机再学习日记

看到学弟们正在学习后缀数组,颇有些感慨,于是决定摸鱼回顾一下后缀自动机,如果能帮助到学弟们,那真的是莫大的荣幸

什么是后缀自动机?

—— 一种可以在O(nlogn)时间复杂度下解决大部分字符串问题的数据结构

如何理解后缀自动机?

—— 理解后缀自动机的关键在于理解endpos集合和后缀链接的转移。

后缀自动机的结构是什么?

—— 后缀自动机上的每一个节点都代表endpos集合一致的一类字符串,后缀自动机的每一条边都和字典树的边的意义一致

什么是endpos集合?

—— endpos集合表示子串在原字符串中所有出现的位置的终止位置的集合,例如 子串’sa’在原串’sasasa’中的endpos为【2,4,6】

这里有几个推论,其实就是后缀自动机的性质,对后面状态的转移的理解很关键,一定要理解,否则对后缀自动机只能是一知半解

—— 推论1
满足endpos集合相同的两个字符串,其中一个一定是另一个的后缀(这一条很好理解,不是吗?)

—— 推论2
如果两个字符串a,b。其中a是b的后缀,那么a的endpos集合一定包含b的endpos集合(这个也不难理解,不是吗?)

—— 推论3
上面的两个推论结合起来就得到了我们最想要的一个推论,同一个endpos集合中的字符串长度连续,短字符串是长字符串的后缀。

证明如下:

由推论1我们知道,同一个endpos集合中的字符串一定满足短字符串是长字符串的后缀的关系,假设现在a和b的endpos集合相同,长度分别为size(a),size(b),并且size(a)>size(b),现在有字符串c,它满足b是c的后缀且c是a的后缀,那么根据推论2,我们很容易推出来,它和a,b的endpos相同。

什么是后缀链接?

—— 后缀链接是当前节点所对应的endpos集合所能被包含的最小父集合所对应的节点,根据endpos集合的定义,我们也很容易推出,其实就是指向了当前节点所代表的最长字符串在原字符串中存在的最长后缀所在的节点,例如:原串为’sasasa’ 那么串’sasa’对应节点的后缀链接对应的就是’sa’所对应的节点

为了构建后缀自动机我们需要什么辅助变量?

—— len数组:len[i]表示i号节点所能代表的字符串中长度最大的字符串的长度
link数组: link[i]表示i号节点后缀链接对应的节点
Tree二维数组:后缀自动机本体,类似于字典树
Last:上一个修改的状态
Tot:节点的总个数
其他的都不是构建后缀自动机的必须变量
常见的功能性(辅助解题)的变量有
Num数组:num[I]表示i号节点一共代表了几个字符串
Size数组:Size[I]表示i号节点代表的endpos集合的大小,一定要和Num数组区分开

我们如何通过上面的那些变量构建后缀自动机?


public class SAM {
    static private final int MAX = 3000;

    /**
     *
     *  这里默认字符集为全体小写字母,字符串长度没有超过1500
     */
    static private int[][] tree = new int[MAX][26];
    static private int[] len    = new int[MAX];
    static private int[] link   = new int[MAX];
    static private int last;
    static private int tot;
    /**
     *
     * 初始化后缀自动机其中初始节点为1号节点
     */
    public SAM(){
        tot=1;
        last=1;
        link[1]=-1;
    }

    /**
     *
     * @param k 代表传入的字符
     * off 代表字符的相对于'a'的偏移量
     * now 代表当前正在创建的新节点
     * p 代表沿着后缀链接向上找的索引
     * clash 表示可能出现冲突的节点
     * neww 表示因分割而产生的新节点
     */
    public void add(char k){
        //初始化节点

        int off  = k-'a';
        int p    = last;
        int now  = last = ++tot;
        len[now] = len[p]+1;
        //沿着后缀链接向上找,没有发现对应的节点就直接建边
        for (;p!=-1&&tree[p][off]==0;p=link[p]){
            tree[p][off] = now;
        }
        //最简单的情形——如果我们来到了空状态-1,向途中所有节点添加了字符c的转移。这就意味着字符c在字符串s中先前未曾出
        //现。我们成功地添加了所有的转移,只需要记下状态cur的后缀链接——它显然必须等于0,因为这种情况下cur匹配字符串s+c的一
        //切后缀。
        if(p==-1){
            link[now] = 1;
        }
        else{
            int clash = tree[p][off];
            //第二种情况,这意味着我们试图向字符串中添加字符x+c(其中x是字符串s的某一后缀,
            //长度为len(clash)),且该字符串先前已经被加入了自动机(即,字符串x+c已经作为子串包含在字符串s中)
            //这可能会导致原本在一个endpos集合中的子串变得不在同一个endpos中,也有可能什么变化都没有发生
            //所以我们就要通过判断len[p]和len[clash]之间的关系,如果len[clash]+1==len[now],那么就不会发生冲突,原因就是推论3
            //这里要明白len的含义,对推论3熟悉,并且要对后缀链接指向的东西的熟悉才能理解(加油,相信自己!)
            //这种情况下我们只需要简单的将link[now]指向clash就行
            if (len[clash]==len[p]+1){
                link[now] = clash;
            }
            //这个时候clash节点中的子串并不在同一个endpos集合中了,所以我们要对其进行分割
            //可以很自然的想到原来集合里面的一部分子串的endpos会多一个,但是是那部分的子串呢?很简单,根据推论3我们知道就是长度小于len[p]+1
            //的那部分,所以根据后缀链接的性质我们知道分裂出来的节点应该处于高位(因为endpos包含原来的节点),原本所有指向clash的节点现在应该指向neww
            //同时因为neww写点原本就是clash的一部分,所以neww节点指向的节点应该和clash一致
            //然后我们考虑后缀链接的事情,很容易知道link[neww]等于原本的link[clash],然而link[clash]应该修改为neww,link[now]自然应该等于neww
            else{
                int neww   = ++tot;
                len[neww]  = len[p]+1;
                link[neww] = link[clash];
                link[now]  = link[clash] = neww;
                for (int i=0;i<26;i++){
                    tree[neww][i]=tree[clash][i];
                }
                for(;p!=-1&&tree[p][off]==clash;p=link[p]){
                    tree[p][off]=neww;
                }
            }
        }
        //至此,后缀自动机就构建完毕了
    }


}

后缀自动机有什么用?

应用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值