[后綴自動機]【學習筆記】


SAM ..................Smith ?


參考資料:

1.陳立傑課件 

2.一篇經典俄文的翻譯

3.https://huntzhan.org/suffix-automaton-tutorial/

4.http://codeforces.com/blog/entry/20861

說明:

花了晚上兩個小時+一上午(估計還要一下午寫筆記).....我主要看了clj的課件,算法過程主要依照課件上,其他兩篇文章作為輔助補充一些證明和性質,課件中有點錯誤讓我很糾結....

那些資料是非常好啦,然后我就亂寫一點加深一下印象

 


前置技能: 

有限狀態自動機的功能是識別字符串,令一個自動機A,若它能識別字符串S,就記為A(S)=True,否則A(S)=False。 自動機由五個部分組成,alpha:字符集,state:狀態集合,init:初始狀態,end:結束狀態集合,trans:狀態轉移函數。 不妨令trans(s,ch)表示當前狀態是s,在讀入字符ch之后,所到達的狀態。 如果trans(s,ch)這個轉移不存在,為了方便,不妨設其為null,同時null只能轉移到null。 null表示不存在的狀態。 同時令trans(s,str)表示當前狀態是s,在讀入字符串str之后,所到達的狀態。
DFA簡介


$Suffix\ Automaton$

-------$Direcged\ Acyclic\ Word\ Graph\ (DAWG)$

Defination:

suffix automaton A for a string s is a minimal finite automaton that recognizes the suffixes of s. This means that A is a directed acyclic graph with an initial node i, a set of terminal nodes, and the arrows between nodes are labeled by letters of s. To test whether w is a suffix of s it is enough to start from the initial node of i and follow the arrows corresponding to the letters of w. If we are able to do this for every letter of wand end up in a terminal node, w is a suffix of s. From:http://codeforces.com/blog/entry/20861

注意定義中的最簡狀態



 

Concept & Property:

$ST(str)$表示$trans(init,str)$

$Reg(s)$表示從狀態$s$開始能識別的$string$,可以發現這是一些$suffix$的集合

(因為能識別str表明當前+str組成了一個suffix,那么str也是一個suffix)

對於string a

 $Reg(ST(a))={suf(r_1),suf(r_2),...,suf(r_n)}$


 

 

$Right:$

$Right(a)={r_1,r_2,...,r_n}$

一個狀態的$Reg$由$Right$集合來決定

SAM中的一個狀態s表示了一個$Right-equivalence\ class$,也就是所有$Right集合=Right(s)$的子串

有了Right只需要一個長度就可以確定子串

對於$Right(s)$,適合他的子串的長度在一個范圍內,記作$[Min(s),Max(s)]$

對於任意兩個狀態$a,b$,$Right(a)$和$Right(b)$如果相交,設$Max(a)<Min(b)$,那么$Right(b)$是$Right(a)$的真子集  

//因為a是b的后綴啊,這里課件上寫反了QAQ//

否則不相交   


 

Right集合的包含關系形成的樹形結構叫做$Parent\ Tree$      

//其他文獻中叫做$Suffix\ Link$  這個邊是從孩子指向父親的,和Fail Tree有點像//

Parent樹從上往下Right集合變小,子串長度變長

$fa=Parent(s)\ \rightarrow\ Right(s) \subset Right(fa)$且$Right(fa)$最小

發現$Max(fa)=Min(s)-1$                                            

//Min(s)-1就多於Right(s)了,就到了Right(fa)//

Parent Tree的葉子節點數O(N),每個內部節點至少兩個孩子,所以總結點數O(N)      

//等比數列求和啊//

 

補充一點:

$Max(s)$也表示了SAM上root到s最多走幾步,從root到s的所有路徑范圍就是$[Min(s),Max(s)]$,因為一條路徑就是一個能轉移到s狀態的子串啊

Suffix Link有點像失配吧,當前狀態s走不了了就到Suffix Link指向的狀態fa上去,fa是s的后綴所以是可行的,並且有更多走的機會

 

其他性質:

1.可以證明,在SAM中節點數不超過$2n-2$,邊數不超過$3n-3$(包括轉移邊和Parent Tree的邊)

2.Suffix Link從父親指向兒子后就是Reverse(s)的Suffix Tree  反向字符串的后綴樹!后綴樹是一顆經過壓縮的字典樹

//隨便證一下:一個節點Right等價,一段后綴相同(相當於壓縮),然后Suffix Link連的點Right集合不同,也就是有不同的邊出去了,所以不能壓縮//

3.s有--c-->  Parent(s)也有  並且人家Right還大

4.s--c-->t   Max(t)>Max(s)

5.兩個串的最長公共后綴,位於這兩個串對應狀態在Parent樹上的最近公共祖先狀態

 

子串的性質:

從init開始走轉移邊可以得到所有子串 每個子串都必然包含在SAM的某個狀態里

一個狀態的Right集合就是他的子樹(葉子)的Right的並集



 

$SAM\ Online\ Construction:$

去看課件吧很詳細啦...... 

使用了Parent Tree的性質,保證了空間O(N),時間也是O(N),證明不管啦

簡單一說:

  • 當前T,長度L,加入x
  • p=ST(T) ,則Right(p)={L}
  • 新建np=ST(Tx)
  • p的Parent祖先的Right里都有L
  • 對於沒有--x-->的祖先v,trans(v,x)=np
  • IF 一直到root之后也沒有這樣的祖先,Parent(np)=root
  • ELSE p=第一個有的祖先,令q=trans(p,x)  //注意這里的Right(q)={ri+1|s[ri]==c}不包括rn
  •   IF Max(q)==Max(p)+1,說明強行加入L+1不會使Max(q)變小,直接Parent(np)=q
  •   ELSE 新建nq復制q,用nq代替trans(v,x)=q的q, Parent(nq)=Parent(q),Parent(q,np)=nq

會使變小:Right(q)的Max可能更長一點,最后加上x還是沒他長



Code:

實現上:

1.last保存當前的ST(T) , val保存MAX(s) 也就是到root的最遠距離

2.和寫過的其他數據結構不一樣,root和last要新開節點,因為即使走到root還是可以走的,Parent(root)=0

3.好短啊 感覺比SA還好寫

4.注意iniSAM和走之前u=root

int c[N],a[N];
int s[N];
struct State{
    int ch[26],par,val;
}t[N];
int sz,root,last;
inline int nw(int _){t[++sz].val=_;return sz;}
inline void iniSAM(){sz=0;root=last=nw(0);}
void extend(int c){
    int p=last,np=nw(t[p].val+1); 
    for(;p&&!t[p].ch[c];p=t[p].par) t[p].ch[c]=np;
    if(!p) t[np].par=root;
    else{
        int q=t[p].ch[c];
        if(t[q].val==t[p].val+1) t[np].par=q;
        else{
            int nq=nw(t[p].val+1);
            memcpy(t[nq].ch,t[q].ch,sizeof(t[q].ch));
            t[nq].par=t[q].par;
            t[q].par=t[np].par=nq;
            for(;p&&t[p].ch[c]==q;p=t[p].par) t[p].ch[c]=nq;
        }
    }
    last=np;
}
void RadixSort(){
    for(int i=0;i<=n;i++) c[i]=0; 
    for(int i=1;i<=sz;i++) c[t[i].val]++;
    for(int i=1;i<=n;i++) c[i]+=c[i-1];
    for(int i=sz;i>=1;i--) a[c[t[i].val]--]=i; 
}

SAM
SAM

 

struct node{
    int ch[26],par,val;
}t[N];
int sz=1,root=1,last=1;
void extend(int c){
    int p=last,np=++sz;
    t[np].val=t[p].val+1;
    for(;p&&!t[p].ch[c];p=t[p].par) t[p].ch[c]=np;
    if(!p) t[np].par=root;
    else{
        int q=t[p].ch[c];
        if(t[q].val==t[p].val+1) t[np].par=q;
        else{
            int nq=++sz;
            t[nq]=t[q];t[nq].val=t[p].val+1;
            t[q].par=t[np].par=nq;
            for(;p&&t[p].ch[c]==q;p=t[p].par) t[p].ch[c]=nq;
        }
    }
    last=np;
}
Another SAM

 

 



$Summary$

一定要時刻把握這幾條性質:

1.走 子串

2.Parent Tree的祖先 Right集合變大,字符串變短(路徑長度變短),並且是后代的后綴哦

3.出現次數向父親(Parent邊)傳遞,接收串數從兒子(仍然Parent邊)獲取

4.拓撲排序/對val用基數排序 , 然后可以轉移邊/Parent邊 DP ,可以倒着遞推出|Right|

5.

 


 


 

Generalized SAM

研究了兩節多課廣義后綴自動機是什么,還看了2015國家隊論文,然后發現,廣義后綴自動機不就是把很多串的SAM建到了一個SAM上,建每個串的時候都從root開始(last=root)就行了........
廣義后綴自動機是Trie樹的后綴自動機,可以解決多主串問題
這樣的在線構造算法復雜度為O(G(T)),G(T)為Trie樹上所有葉子節點深度和,發現G(T)<=所有主串總長度
還有一種離線算法,復雜度O(|T||A|) ,不學了吧
 
一個基本應用是求出每個狀態出現次數(同一個串算一次)
根據接收串數從兒子獲取,就是子樹中有多少主串經過
方法是:
先建出SAM
然后跑每個主串,狀態維護cou和cur分別為出現次數及上一次出現是哪個串
出現次數向父親傳遞,所以要沿着Parent向上跑更新,遇到cur=當前串的就不用繼續跑了
如果題目規定串總長L,N個串
這樣最壞情況下復雜度為O(L^3/2),發生在N=每個串長度 的時候(均值不等式啊)
 
 
對Trie建廣義后綴自動機:
從根dfs中保存last
解釋
直接對Trie建SAM與原本一個串建SAM唯一的不同是last可能已經有--c-->q了,我們有兩種選擇來處理
如果t[q].val==t[last].val+1
第一種是直接不管,這樣t[np].par=q,np和q可以看作一個點,不受影響
第二種是管,直接讓last走到q,也沒關系
如果t[q].val!=t[last].val+1
這時會新建節點nq,然后t[q].par=t[np].par=nq 注意np和nq的Right是一樣的(因為本來last有--c-->這個轉移啊,所有的r都可以r+1),但沒關系,依舊看作一個點
 


免責聲明!

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



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