后綴自動機


后綴自動機,是一種線性的字符串處理工具:

引用一下陳立傑的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;
}

 


免責聲明!

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



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