后缀自动机的一点点理解

Posted 小蒟蒻yyb的博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了后缀自动机的一点点理解相关的知识,希望对你有一定的参考价值。

后缀自动机的一点点理解

前言

最近心血来潮,想学学SAM,于是花了一晚上+一上午
勉强打了出来(但是还是不理解)
虽说张口就讲我做不到
但是一些其他的东西还是有所感触的
索性,乱口胡点东西,谢谢关于SAM的一些简单的理解

资料

丽洁姐WC PPT
hihocoder上的后缀自动机

一些概念

这些概念都不读懂,接下来真的是步履维艰

本来我们要的是一个能够处理所有后缀的数据结构
但是我们发现,如果对于每一个后缀都要插入进Trie树
空间复杂度完全背不动(\(O(n^2)\)级别)
于是,后缀自动机出现了
后缀自动机相比于Trie树
在空间上有了很大的改善,他的空间复杂度是\(O(n)\)级别的
(详见丽洁姐的PPT)

杂七杂八的没有什么太多写的必要,网上一找一大堆
写写一些概念

right/endpos

hihocoder上写的是\(endpos\)集合
其他的大部分地方写的是\(right\)集合
这就是最基础的概念了
叫做\(endpos\)的话应该很好理解,所以我就写\(endpos\)
\(endpos\)就是一个子串结束位置组成的集合
对于所有结束位置相同的子串
也就是\(endpos\)相同的两个子串
他们一个一定是另一个的后缀

至于证明,简单的想一下,如果一个子串出现在了若干个位置
那么他的后缀也一定出现在了这些位置(只可能出现在更多未知,不可能更少)

同时,得到了一个推论:
两个字符串如果有一个是另一个的后缀,
那么,较长串的\(endpos\)一定是较短串的\(endpos\)的子集
(就是上面写的,只可能多,不可能少)
同样的,如果没有后缀的关系,那么它们的\(endpos\)的交集一定是空集

而后缀自动机的每个节点就是依照\(endpos\)来划分
对于\(endpos\)相同的子串,我们可以划分在一起
我们不难得出一点,对于一堆\(endpos\)相同的子串
他们一定互为后缀,并且他们长度连续

首先证明互为后缀,那就是上面的那个推论,
如果不是互为后缀的话,\(endpos\)就不可能相等
而长度连续?
既然互为后缀,那就一定有一个最长的串,不妨记为\(longest\)
那么,所有的其他串一定是他的后缀
随着后缀长度的减小,
那么从某一个后缀开始,就可能出现在了更多的位置
那么,这个后缀以及比它更短的后缀的\(endpos\)一定会变大
此时他们就会分到别的节点去了
因此,具有相同\(endpos\)的子串一定长度连续,互为后缀
另外一个简单的结论,确定了\(endpos\)和长度\(len\)就能确定唯一的子串

trans

\(trans\)不难理解是转移的意思
\(trans(s,c)\)表示当前在\(s\)状态,接受一个字符\(c\)之后所到达的状态
一个状态\(s\)表示若干\(endpos\)相同的连续子串
那么,此时相当于在后面加上了一个字符\(c\)
那么,我们对于任意一个串直接加上一个字符\(c\)之后
组成的串的\(endpos\)还是相同的
所以\(trans(s,c)\)就会指向这个状态
换句话说,随便在当前状态\(s\)中找一个串(比如\(longest\)
然后在后面接上一个\(c\)
那么,就指向包含这个新字符串的状态

本质上也是一个东西,不同的地方写的不一样而已
不妨设一个状态中包含的最短的串叫做\(shortest\)
那么,我们就知道\(shortest\)的任意一个非自己的后缀一定就会出现在了更多位置
他的最长的那个后缀,也就是减去了第一个字符后的串
就会出现在另外一个状态里面,并且是那个状态的\(longest\)
为什么?因为出现在了更多的位置,我们还是知道他是连续的子串
如果存在一个更长的串
那么,只可能是当前状态的\(shortest\)
但是\(shortest\)属于当前状态,而没有出现在更多的位置
因此,\(longest\)一定是当前状态的\(shortest\)减去最前面字符形成的串

那么,当前位置的\(parent\)就会指向那个状态

当然,还是有几个很有趣的性质
假设当状态是\(s\)
\(s.shortest.len=parent.longest.len+1\)
这个就是前面所说的东西,所以,对于每个状态,就没有必要记录\(shortest\)
因为你只要知道\(parent\)就可以算出来了

其次,\(s\)\(endpos\)\(parent\)的子集
这个不难证明,因为\(parent\)包含了更多的位置

如果\(trans(s,c)\neq NULL\)
那么,\(trans(parent,c)\neq NULL\)
因为如果\(trans(s,c)\)存在这个状态
那么\(parent\)的串加上\(c\)之后,一定还是\(s+c\)后的后缀
所以也一定存在\(trans(parent,c)\)
所以,你可以认为\(parent\)是一个完全包含了\(s\)的状态
也正因为如此,\(parent\)\(endpos\)就是所有儿子\(endpos\)的并集

将所有的\(parent\)反过来,我们就得到了\(parent\)
如果要处理什么,就需要\(parent\)树的拓扑序
(因为\(parent\)相当于包含了所有的他的子树,都需要更新上去)
其实不需要拓扑排序
我们知道\(s\)\(endpos\)完全被\(parent\)\(endpos\)包含
\(s.longest\)一定长于\(parent.longest\)
所以,一个状态的\(longest\)越长,它一定要被更先访问
所以,按照\(longest\)的长度进行桶排序就可以解决拓扑序了

extend

对于一个\(SAM\)的构造
我们当然在线了(因为我只会这个)
我们依次加入字符\(c\),来进行构造

假设原来的字符串是\(T\)
首先,一定会有一个新节点
因为新加入了一个字符后,一定出现了这个新的字符串\(T+c\)
此时\(endpos\)一定是新的位置
同时,原来的\(T\)的最后一个位置也可以通过\(+c\)变到这个新位置
设原来的最后一个位置的状态是\(last\),新的状态是\(np\)
所以\(trans(last,c)=np\)
根据前面的东西,我们知道\(last\)的祖先们一定也会有这个\(trans\)
我们要怎么解决他呀

\(p=last\)
一直沿着\(parent\)往前跳,也就是不断令\(p=p.parent\)
所以\(p\)代表的,就是越来越短的\(T\)的后缀
因为要更新的是最后的位置,
只有当存在\(T\)的最后一个位置时才能更新

如果\(trans(p,c)=NULL\),直接令\(trans(p,c)=np\)
很显然是可以直接在后面添加一个\(c\)到达\(np\)
如果跳完后发现没有\(parent\)了,直接把\(np.parent\)指向\(1\)
也就是空串所代表的状态

如果某个\(trans(p,c)\)不为\(NULL\)
那么,设\(q=trans(p,c)\)
如果有\(longest(p)+1=longest(q)\)
什么意思?
\(p\)的串后面添上一个\(c\)之后就是\(q\)状态
没有任何问题,直接在作为\(T\)的后缀的那一个子串上
直接添加一个\(c\)显然也可以到达\(q\)状态
又因为\(np\)所代表的\(endpos\)更小,
所以\(np.parent=q\)

在否则的话
也就是\(longest(q)>longest(p)+1\)
具体的反例看丽洁姐PPT第\(35\)
如果直接插入的话(也就是\(np.parent=q\)
相当于给\(q\)\(endpos\)强行插入一个\(q\)
但是,我们发现,如果强行插入进去
这个\(T+c\)的后缀会出现在更多的位置,应该属于另外一个状态
然后就\(GG\)
此时,我们新建一个点\(nq\)
相当于把\(q\)拆成两部分:
一部分是\(T+c\)的那个后缀,一个是\(longest(p)+c\)
也就是\(longest(nq)=longest(p)+1\)
显然\(T+c\)的后缀是包含了状态较少的,
拆分出来的一部分\(q\)是长度较长的
所以\(q.parent=np.parent=nq\)
同时,继续沿着\(p\)\(parent\)往上走
把所有的\(q\)都替换成\(nq\)

看起来很有道理,但是我也是似懂非懂的感觉

End

这就是我自己的一些没有什么用的总结了
我觉得题目才能真正反映SAM的作用
到时候再补点题目上去

补一份后缀自动机\(extend\)的代码

int tot=1,last=1;
struct Node
{
    int son[26];
    int ff,len;
}t[MAX<<1];
void extend(int c)
{
    int p=last,np=++tot;last=np;
    t[np].len=t[p].len+1;
    while(p&&!t[p].son[c])t[p].son[c]=np,p=t[p].ff;
    if(!p)t[np].ff=1;
    else
    {
        int q=t[p].son[c];
        if(t[p].len+1==t[q].len)t[np].ff=q;
        else
        {
            int nq=++tot;
            t[nq]=t[q];t[nq].len=t[p].len+1;
            t[q].ff=t[np].ff=nq;
            while(p&&t[p].son[c]==q)t[p].son[c]=nq,p=t[p].ff;
        }
    }
}

以上是关于后缀自动机的一点点理解的主要内容,如果未能解决你的问题,请参考以下文章

[后缀自动机]SAM的一点理解

康复计划#1 再探后缀自动机&后缀树

后缀自动机感性理解

后缀数组

每天进步一点点之中缀表达式转后缀表达式

后缀自动机总结