后綴自動機的一點點理解


后綴自動機的一點點理解

前言

最近心血來潮,想學學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\)強行插入一個\(np\)
但是,我們發現,如果強行插入進去
這個\(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;
        }
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM