后綴自動機,是一種線性的字符串處理工具:
引用一下陳立傑的PPT
有限狀態自動機的功能是識別字符串,令一個自動機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之后,所到達的狀態
S為自動機的節點,那么我們可以這樣定義S
struct S{ int c[26],fa,val;//26為字符集的大小,通常我們認為其為常數 }T[N];
那么自動機A能識別的字符串就是所有使得 trans(init,x) ⊂ end的字符串x。 令其為Reg(A)。
從狀態s開始能識別的字符串,就是所有使得 trans(s,x) ⊂ end的字符串x。
令其為Reg(s)。
給定字符串S
S的后綴自動機suffix automaton(以后簡記為SAM)是一個能夠識別S的所有后綴的自動機。
即SAM(x) = True,當且僅當x是S的后綴
同時后面可以看出,后綴自動機也能用來識別S所有的子串。
考慮字符串”aabbabd”
我們可以講該字符串的所有后綴 插入一個Trie中,就像右圖那樣。 那么初始狀態就是根,狀態轉移 函數就是這顆樹的邊,結 束狀態 集合就是所有的葉子。
注意到這個結構對於長度為N
的串,會有O(𝑁^2)的節點。
那么,我們就要尋找最簡狀態的后綴自動機:
假如我們得到了這個最簡狀態后綴自動機SAM。
我們令ST(str)表示trans(init,str)。就是初始狀態開始讀入字符串str之后,能到達的狀態。
令母串為S,它的后綴的集合為Suf,它的連續子串的集合為Fac。 從位置a開始的后綴為Suffix(a)。 S[l,r)表示S中[l,r)這個區間構成的子串。 下標從0開始。
對於一個字符串s,如果它不屬於Fac,那么ST(s) = null。因為s后面加上任何字符串都不可能是S的后綴了,沒有理由浪費空間。
同時如果字符串s屬於Fac,那么ST(s) ≠ null。因為s既然是S的子串,就可以在后面加上一些字符使得其變成S的后綴。我們既然要識別所有的后綴,就不能放過這種 可能性。
我們不能對每個s∈Fac都新建一個狀態,因為Fac的大小是O(𝑁2)的。
我們考慮ST(a)能識別哪些字符串,即Reg(ST(a))
字符串x能被自動機識別,當且僅當x∈Suf。
ST(a)能夠識別字符串x,當且僅當ax∈Suf。因為我們已經讀入了字符串a了。
也就是說ax是S的后綴。那么x也是S的后綴。Reg(ST(a))是一些后綴集合。
對於一個狀態s,我們唯一關心的是Reg(s)。
說句人話,后綴自動機的核心是縮點,一顆后綴樹上度為一的連續的點是沒有必要單獨存儲的,我們可以將其縮成一個點表示一段區間,那么我們可以證明點的個數是不會超過2n-1個的。那么最簡后綴自動機的節點數也是這樣的。
我們假設這樣一個節點a
如果a在S中的[l,r)位置出現,那么他就能識別S從r開始的后綴。
不妨令Right(a)=那么Reg(ST(a))就完全由Right(a)決定。
那么對於兩個子串𝑎,𝑏∈𝐹𝑎𝑐如果Right(a) = Right(b),那么ST(a) = ST(b)。
所以一個狀態s,由所有Right集合是Right(s)的字符串組成。
不妨令r∈𝑅𝑖𝑔𝑡(𝑠),那么只要給定子串的長度len,該子串就是S[r-len,r)。即給定Right集合后,再給出一個長度就可以確定子串了。
考慮對於一個Right集合,容易證明如果長度𝑙,𝑟合適,那么長度𝑙≤𝑚≤𝑟的m也一定合適。所以合適長度必然是一個區間。
不妨令s的區間是[Min(s),Max(s)]
我們考慮兩個狀態a,b。他們的Right集合分別為Ra,Rb。
假設Ra和Rb有交集,不妨設𝑟∈𝑅𝑎 ∩𝑅𝑏。
那么由於a和b表示的子串不會有交集,所以[Min(a),Max(a)]和[Min(b),Max(b)]也不會有交集。 不妨令Max(a)<Min(b)。那么a中所有長度都比b中短,由於都是由r往前,所以a中所有串都是b中所有串的后綴。因此a中某串出現的位置,b中某串也必然出現了。所以𝑅𝑎⊂𝑅𝑏。既Ra是Rb的真子集。
那么,任意兩個串的Right集合,要么不相交,要么一個是另一個的真子集。
我們可以看出Right集合實際上構成了一個樹形結構。不妨稱其為Parent樹。
在這個樹中,葉子節點的個數只有N個,同時每內部個節點至少有2個孩子,容易證明樹的大小必然是O(N)的。
令一個狀態s,我們令fa=Parent(s)表示上面那個圖中,它的父親。 那么𝑅𝑖𝑔𝑡𝑓𝑎⊃𝑅𝑖𝑔𝑡𝑠,並且Right(fa)的大小是其中最小的。
考慮長度,s的范圍是[Min(s),Max(s)],為什么長度Min(s)-1為什么不符合要求?可以發現肯定是因為出現的地方超出了Right(s)。同時隨着長度的變小,出現的地方越來越多,那么Min(s)-1就必然屬於fa的范圍。那么
Max(fa) = Min(s)-1
我們已經證明了狀態的個數是O(N)的,為了證明這是一個線性大小的結構,我們還需要證明邊的大小是O(N)的。
如果trans(a,c) == null,我們就沒有必要儲存這條邊,我們只需要儲存有用的邊。
不妨trans(a,c)=b的話,就看成有一條𝑎→𝑏的標號為c的邊。
我們首先求出一個SAM的生成樹(注意,跟之前提到的樹形結構沒有關系),以init為根。
那么令狀態數為M,,生成樹中的邊最多只有M-1條,接下來考慮非樹邊。對於一條非樹邊a→b標號為c。
我們構造:
生成樹中從根到狀態𝑎的路徑+(a->b)+b到任意一個end狀態。
可以發現這是一條從init到end狀態的路徑,由於這是一個識別所有后綴的后綴自動機,因此這必然是一個后綴。
那么一個非樹邊可以對應到多個后綴。我們對每個后綴,沿着自動機走,將其對應上經過的第一條非樹邊。
那么每個后綴最多對應一個非樹邊,同時一個非樹邊至少被一個后綴所對應,所以非樹邊的數量不會超過后綴的數量。
所以邊的數量也不會超過O(N)
由於每個子串都必然包含在SAM的某個狀態里。
那么一個字符串s是S的子串,當且僅當,ST(s)!=null
那么我們就可以用SAM來解決子串判定問題。
同時也可以求出這個子串的出現個數,就是所在狀態Right集合的大小
在一個狀態中直接保存Right集合會消耗過多的空間,我們可以發現狀態的Right就是它Parent樹中所有孩子Right集合的並集,進一步的話,就是Parent樹中它所有后代中葉子節點的Right集合的並集。
那么如果按dfs序排列,一個狀態的right集合就是一段連續的區間中的葉子節點的Right集合的並集,那么我們也就可以快速求出一個子串的所有出現位置了。
樹的dfs序列:所有子樹中節點組成一個區間。
我們的構造算法是Online的,也就是從左到右逐個添加字符串中的字符。依次構造SAM。
這個算法實現相比后綴樹來說要簡單很多,盡管可能不是非常好理解。
讓我們先回顧一下性質
狀態s,轉移trans,初始狀態init,結束狀態集合end。
母串S,S的后綴自動機SAM(Suffix Automaton的縮寫)。
Right(str)表示str在母串S中所有出現的結束位置集合。
一個狀態s表示的所有子串Right集合相同,為Right(s)。
Parent(s)表示使得Right(s)是Right(x)的真子集,並且Right(x)的大小最小的狀態x。
Parent函數可以表示一個樹形結構。不妨叫它Parent樹。
一個Right集合和一個長度定義了一個子串。
對於狀態s,使得Right(s)合法的子串長度是一個區間,為| [Min(s),Max(s)]
Max(Parent(s)) = Min(s)-1。
SMA的狀態數量和邊的數量,都是O(N)的。
不妨令trans(s,ch)==null表示從s出發沒有標號為ch的邊,
考慮一個狀態s,它的Right(s)=𝑟1,𝑟2,…,𝑟𝑛 ,假如有一條s→t標號為c的邊,考慮t的Right集合,由於多了一個字符,s的Right集合中,只有S[𝑟𝑖]==c的符合要求。那么t的Right集合就是{𝑟𝑖+1|S[ri]==c}
那么如果s出發有標號為x的邊,那么Parent(s)出發必然也有。
同時,對於令f=Parent(s),
Right(trans(s,c)) ⊆ Right(trans(f,c))。
有一個很顯然的推論是Max(t)>Max(s)
我們每次添加一個字符,並且更新當前的SAM使得它成為包含這個新字符的SAM。
令當前字符串為T,新字符為x,令T的長度為L
SAM(T) →SAM(Tx)
那么我們新增加了一些子串,它們都是串Tx的后綴。
Tx的后綴,就是T的后綴后面添一個x
那么我們考慮所有表示T的后綴(也就是Right集合中包含L)的節點𝑣1,𝑣2,𝑣3,…。
由於必然存在一個Right(p)={L}的節點p(ST(T))。那么𝑣1,𝑣2,…,𝑣k由於Right集合都含有L,那么它們在Parent樹中必然全是p的祖先。可以使用Parent函數得到他們。
同時我們添加一個字符x后,令np表示ST(Tx),則Right(np) ={L+1}
不妨讓他們從后代到祖先排為𝑣1=𝑝,𝑣2,…,𝑣k=root。
考慮其中一個v的Right集合=𝑟1,𝑟2,…,𝑟𝑛=𝐿。
那么在它的后面添加一個新字符x的話,形成新的狀態nv的話,只有S[𝑟𝑖] = x的𝑟𝑖那些是符合要求的。
同時在之前我們知道,如果從v出發沒有標號為x的邊(我們先不看𝑟𝑛),那v的Right集合內就沒有滿足這個要求的𝑟𝑖 。
那么由於𝑣1,𝑣2,𝑣3,… 的Right集合逐漸擴大,如果𝑣𝑖出發有標號為x的邊,那么𝑣𝑖+1出發也肯定有。
對於出發沒有標號為x的邊的v,它的Right集合內只有𝑟𝑛是滿足要求的,所以根據之前提到的轉移的規則,讓它連一條到np標號為x的邊。
令𝑣𝑝為𝑣1,𝑣2,…,𝑣k中第一有標號為x的邊的狀態。
考慮𝑣𝑝的Right集合=𝑟1,𝑟2,…,𝑟𝑛 ,令trans(𝑣𝑝,x)=q
那么q的Right集合就是{𝑟𝑖+1},S[𝑟𝑖 ]=x的集合(注意到這是更新之前的情況,所以𝑟𝑛是不算的)。
注意到我們不一定能直接在q的Right集合中插入L+1。
那么我們新建一個節點nq,使Right(nq) = Right(q) ∩𝐿+1
同時可以看出Max(nq) = Max(𝑣𝑝)+1。
那么由於Right(q)是Right(nq)的真子集,所以Parent(q) = nq。
同時Parent(np) = nq。
並且容易證明Parent(nq) = Parent(q) (原來的)
接下來,如果新建了節點nq我們還得處理。
回憶: 𝑣1,𝑣2,…,𝑣k是所有Right集合包含{L}的節點按后代到祖先排序,其中𝑣𝑝是第一個有標號為x的邊的祖先。x是這輪新加入的字符。
由於𝑣p,…,𝑣k都有標號為x的邊,並且到達的點的Right集合,隨着出發點Right集合的變大,也會變大,那么只有一段𝑣p,…,𝑣𝑒,通過標號為x的邊,原來是到結點q的。 回憶:q=Trans(𝑣p,x)。
那么由於在這里q節點已經被替換成了nq,我們只要把𝑣p,…,𝑣𝑒的Trans(*,x)設為nq即可。
代碼實現
inline int Sam(int x,int last){ int np=++tot; T[np].val=T[last].val+1; for(;last&&(!T[last].c[x]);last=T[last].fa) T[last].c[x]=np; if (!last) T[np].fa=1; else { int q=T[last].c[x]; if (T[last].val+1==T[q].val) T[np].fa=q; else { int nq=++tot; T[nq]=T[q]; T[nq].val=T[last].val+1; T[q].fa=T[np].fa=nq; for (;last&&T[last].c[x]==q;last=T[last].fa) T[last].c[x]=nq; } }return np; }
后附洛谷 【模板】后綴自動機的源代碼:
//#pragma optimize("-O2") #include<bits/stdc++.h> #define N 3000003 #define max(a,b) ((a)>(b)?(a):(b)) #define min(a,b) ((a)<(b)?(a):(b)) using namespace std; struct S{ int c[26],fa,val; }T[N]; int tot=1,tmp,n,len,c[N],id[N],now,ti,x,last,siz[N]; long long ans; char ch[N]; inline int Sam(int x,int last){ int np=++tot; T[np].val=T[last].val+1; for(;last&&(!T[last].c[x]);last=T[last].fa) T[last].c[x]=np; if (!last) T[np].fa=1; else { int q=T[last].c[x]; if (T[last].val+1==T[q].val) T[np].fa=q; else { int nq=++tot; T[nq]=T[q]; T[nq].val=T[last].val+1; T[q].fa=T[np].fa=nq; for (;last&&T[last].c[x]==q;last=T[last].fa) T[last].c[x]=nq; } } siz[np]=1; return np; } int main () { freopen("a.in","r",stdin); scanf("%s",ch); len=strlen(ch);last=1; for (int i=0;i<len;i++) last=Sam(ch[i]-'a',last); for (int i=1;i<=tot;i++) c[T[i].val]++; for (int i=1;i<=len;i++) c[i]+=c[i-1]; for (int i=1;i<=tot;i++) id[c[T[i].val]--]=i; for (int i=tot;i;i--) { int p=id[i]; siz[T[p].fa]+=siz[p]; if (siz[p]>1) ans=max(ans,1ll*siz[p]*T[p].val); } printf("%lld\n",ans); return 0; }