SAM ..................Smith ?
參考資料:
1.陳立傑課件
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之后,所到達的狀態。
$Suffix\ Automaton$
-------$Direcged\ Acyclic\ Word\ Graph\ (DAWG)$
Defination:
A 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

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; }
$Summary$
一定要時刻把握這幾條性質:
1.走 子串
2.Parent Tree的祖先 Right集合變大,字符串變短(路徑長度變短),並且是后代的后綴哦
3.出現次數向父親(Parent邊)傳遞,接收串數從兒子(仍然Parent邊)獲取
4.拓撲排序/對val用基數排序 , 然后可以轉移邊/Parent邊 DP ,可以倒着遞推出|Right|
5.