后缀自动机详解
后綴自動(dòng)機(jī)詳解
標(biāo)簽:?后綴自動(dòng)機(jī) 2017-03-26 11:34?2341人閱讀?評論(3)?收藏?舉報(bào) ?分類:版權(quán)聲明:本文為博主原創(chuàng)文章,未經(jīng)博主允許不得轉(zhuǎn)載。
后綴自動(dòng)機(jī)
后綴自動(dòng)機(jī)(單詞的有向無環(huán)圖)——是一種強(qiáng)有力的數(shù)據(jù)結(jié)構(gòu),讓你能夠解決許多字符串問題。
例如,使用后綴自動(dòng)機(jī)可以在某一字符串中搜索另一字符串的所有出現(xiàn)位置,或者計(jì)算不同子串的個(gè)數(shù)——這都能在線性
時(shí)間內(nèi)解決。
? ?直覺上,后綴自動(dòng)機(jī)可以被理解為所有子串的簡明信息。一個(gè)重要的事實(shí)是,后綴自動(dòng)機(jī)以壓縮后的形式包含了一個(gè)長度
為n的字符串的所有信息,僅需要O(n)的空間。并且,它能在O(n)時(shí)間內(nèi)被構(gòu)造(如果我們將字母表的大小k視作常數(shù),否則就
是O(n*logk))。
? ?歷史上,Blumer等人于1983年首次提出了后綴自動(dòng)機(jī)的線性規(guī)模,然后在1985-1986年,人們提出了首個(gè)線性時(shí)間內(nèi)構(gòu)建
后綴自動(dòng)機(jī)的算法(Crochemore,Blumer等)。在文末鏈接處查看更多細(xì)節(jié)。
? ?后綴自動(dòng)機(jī)在英文中被稱作“suffix automaton”(復(fù)數(shù)形式:suffix automata),單詞的有向無環(huán)圖——"direcged acyclic
word graph"(簡寫為“DAWG”)。
后綴自動(dòng)機(jī)的定義
定義.對給定字符串s的后綴自動(dòng)機(jī)是一個(gè)最小化確定有限狀態(tài)自動(dòng)機(jī),它能夠接收字符串s的所有后綴。
下面解釋這一定義:
·????????后綴自動(dòng)機(jī)是一張有向無環(huán)圖,其中頂點(diǎn)是狀態(tài),而邊代表了狀態(tài)之間的轉(zhuǎn)移。
·????????某一狀態(tài)t_0被稱作初始狀態(tài),由它能夠到達(dá)其余所有狀態(tài)。
·????????自動(dòng)機(jī)中的所有轉(zhuǎn)移——即有向邊——都被某種符號(hào)標(biāo)記。從某一狀態(tài)出發(fā)的諸轉(zhuǎn)移必須擁有不同的標(biāo)記。(另一方面,
狀態(tài)轉(zhuǎn)移不能在任何字符上)。
·????????一個(gè)或多個(gè)狀態(tài)被標(biāo)記為終止?fàn)顟B(tài)。如果我們從初始狀態(tài)t_0經(jīng)由任意路徑走到某一終止?fàn)顟B(tài),并順序?qū)懗鏊薪?jīng)過邊的
標(biāo)記,你得到的字符串必然是s的某一后綴。
·????????在符合上述諸條件的所有自動(dòng)機(jī)中,后綴自動(dòng)機(jī)有這最少的頂點(diǎn)數(shù)。(后綴自動(dòng)機(jī)并不被要求擁有最少的邊數(shù))
后綴自動(dòng)機(jī)的最簡性質(zhì)
最簡性——后綴自動(dòng)機(jī)的最重要性質(zhì)是:它包含了所有s的子串的信息。換言之,對于任意從初始狀態(tài)t_0出發(fā)的路徑,如果我們
寫出所經(jīng)過邊上的標(biāo)記,形成的子串必須是s的子串。相應(yīng)地,s的任意子串都對應(yīng)一條從初始狀態(tài)t_0出發(fā)的路徑。
為了簡化說明,我們稱子串“匹配”了從初始狀態(tài)出發(fā)的路徑,如果該路徑上的邊標(biāo)記組成了這一子串。相應(yīng)地,我們稱任意路徑
“匹配”某一子串,該子串由路徑中邊的標(biāo)記組成。
?后綴自動(dòng)機(jī)的每個(gè)狀態(tài)都引領(lǐng)一條或多條從初始狀態(tài)出發(fā)的路徑。我們稱這個(gè)狀態(tài)有若干匹配這些路徑的方法。
?
構(gòu)建后綴自動(dòng)機(jī)的實(shí)例
下面給出一些對簡單的字符串構(gòu)建后綴自動(dòng)機(jī)的例子。
?初始狀態(tài)被記作t0,終止?fàn)顟B(tài)用星號(hào)(*)標(biāo)記。
s=""
s="a"
s="aa"
s="ab"
s="aba"
s="abb"
s="abbb"
一個(gè)線性時(shí)間構(gòu)建后綴自動(dòng)機(jī)的算法
在我們描述構(gòu)建算法之前,有必要介紹一些新的概念和簡要的證明,它們對理解后綴自動(dòng)機(jī)的概念十分重要。
?
結(jié)束位置endpos,它們的性質(zhì)及與后綴自動(dòng)機(jī)的聯(lián)系:
考慮字符串s的任意非空子串t。我們稱終點(diǎn)集合endpos(t)為:s中所有是t出現(xiàn)位置終點(diǎn)的集合。
我們稱兩個(gè)子串t_1和t_2“終點(diǎn)等價(jià)”,如果它們的終點(diǎn)集合一致:endpos(t_1)=endpos(t_2)。因此,所有s的非空子串可
以根據(jù)終點(diǎn)等價(jià)性分成若干類。
?事實(shí)上對后綴自動(dòng)機(jī),終點(diǎn)等價(jià)字符串仍然保持相同性質(zhì)。換句話說,后綴自動(dòng)機(jī)中狀態(tài)數(shù)等價(jià)于所有子串的終點(diǎn)等價(jià)類
個(gè)數(shù),加上初始狀態(tài)。每個(gè)狀態(tài)對應(yīng)一個(gè)或多個(gè)擁有相同終點(diǎn)集合的子串。
? 我們將這一陳述作為假定,然后描述一個(gè)基于此假設(shè)的,線性時(shí)間構(gòu)建后綴自動(dòng)機(jī)的算法——正如我們不久后將會(huì)看到的,
所有后綴自動(dòng)機(jī)的必須性質(zhì),除最小性(即最少頂點(diǎn)數(shù)),都將被滿足(最小性由Nerode產(chǎn)生,見參考文獻(xiàn))。
?關(guān)于終點(diǎn)集合,我們給出一些簡單但重要的事實(shí)。
引理1.兩個(gè)非空子串u和v(length(u)<=length(v))是終點(diǎn)等價(jià)的,當(dāng)且僅當(dāng)u在字符串s中僅作為w的后綴出現(xiàn)。
?
證明是顯然的。
?
引理2.考慮兩個(gè)非空子集u,w(length(u)<=length(w))。它們的終點(diǎn)集合不相交,或者endpos(w)是endpos(u)的子集。進(jìn)一
步地,這取決于u是否是w的后綴:
證明.假設(shè)兩個(gè)集合endpos(u)和endpos(w)有至少一個(gè)公共元素,這就意味著字符串w和u在同一位置結(jié)束,即u是w的后綴。
因此,在字符串w的每次出現(xiàn)的終點(diǎn)u都會(huì)出現(xiàn),這就意味著endpos(w)包含于endpos(u)。
?
引理3.考慮一個(gè)終點(diǎn)等價(jià)類。將該等價(jià)類中的子串按長度遞減排序。排序后的序列中,每個(gè)子串將比上一個(gè)子串短,從而是
上一個(gè)字串的后綴。換句話說,某一終點(diǎn)等價(jià)類中的字符串互為后綴,它們的長度依次取區(qū)間[x,y]內(nèi)的所有數(shù)。
?
證明.考慮這個(gè)終點(diǎn)等價(jià)類。如果它只包含一個(gè)子串,那么引理3的正確性顯然。假設(shè)現(xiàn)在子串的個(gè)數(shù)多于一個(gè)。
?
根據(jù)引理1,兩個(gè)不同的終點(diǎn)等價(jià)子串總滿足一個(gè)是另一個(gè)的嚴(yán)格后綴。因此,在同一終點(diǎn)等價(jià)類中的子串不可能有相同的長
度。
?令w較長,u是等價(jià)類中的最短子串。根據(jù)引理1,u是w的嚴(yán)格后綴。考慮w任意一個(gè)長度為[length(u),length(w)]之間的后綴,
由引理1,顯然它在終點(diǎn)等價(jià)類中。
?
后綴鏈接
考慮一個(gè)狀態(tài)v≠t_0.就我們目前所知,有一個(gè)確定的子串集合,其中元素和v有著相同的終點(diǎn)集合。并且,如果我們記w是其
中的最長者,其余子串均是w的后綴。我們還知道w的前幾個(gè)后綴(按照長度降序)在同一個(gè)終點(diǎn)等價(jià)類中,其余后綴(至少包括
空后綴)在別的終點(diǎn)等價(jià)類中。令t是第一個(gè)這樣的后綴——對它我們建立后綴鏈接。
? 換言之,v的后綴鏈接link(v)指向在不同等價(jià)類中的w的最長后綴。
?在此我們假設(shè)初始狀態(tài)t_0在一個(gè)單獨(dú)的終點(diǎn)等價(jià)類中(僅包含空字符串),并且endpos(t_0)={-1,...,length(s)-1}。
?
引理4.后綴鏈接組成了一棵以t_0為根的樹。
?
證明.考慮任意狀態(tài)v≠t_0.后綴鏈接link(v)指向的狀態(tài)所對應(yīng)的字符串長度嚴(yán)格小于它本身(根據(jù)后綴鏈接的定義和引理3)。
因此,沿著后綴鏈接移動(dòng),我們將早晚到達(dá)t_0,它對應(yīng)一個(gè)空串。
?
引理5.如果我們將所有合法的終點(diǎn)集合建成一棵樹(使得孩子是父母的子集),這棵樹將和后綴鏈接構(gòu)成的樹相同。
?
證明.終點(diǎn)集合能構(gòu)成一棵樹這一事實(shí)由引理2得出(兩個(gè)終點(diǎn)集合要么不相交,要么一個(gè)包含另一個(gè))。
?
我們現(xiàn)在考慮任意狀態(tài)v≠t_0,及其后綴鏈接link(v)。根據(jù)后綴鏈接的定義和引理2得出:
endpos(v)?endpos(link(v))
這和上一引理證明了我們的斷言:后綴鏈接樹和終點(diǎn)集合樹相同。
?
這里是一個(gè)后綴鏈接的例子,表示字符串"abcbc":
小結(jié)
在學(xué)習(xí)具體算法之前,總結(jié)上面積累的知識(shí),并引入兩個(gè)輔助符號(hào)。
?
·????????s的所有子串可以按照它們的終點(diǎn)集合被分成等價(jià)類。
·????????后綴自動(dòng)機(jī)由一個(gè)初始狀態(tài)t_0和所有不同的終點(diǎn)等價(jià)類所對應(yīng)的狀態(tài)組成。
·????????每個(gè)狀態(tài)v對應(yīng)一個(gè)或多個(gè)字符串,我們記longest(v)是其中最長者,len(v)是其長度。我們記shortest(v)是這些字符串中
的最短者,其長度為minlen(v)。
·????????該狀態(tài)對應(yīng)的所有字符串是longest(v)的不同后綴,并且包括[minlen(v),len(v)]之間的所有長度。
·????????對每個(gè)狀態(tài)v≠t_0定義的后綴鏈接指向的狀態(tài)對應(yīng)longest(v)的長度為minlen(v)-1的后綴。后綴鏈接形成一棵以t_0為根的
樹,而這棵樹事實(shí)上是所有終點(diǎn)集合的樹狀包含關(guān)系。minlen(v)和link(v)的關(guān)系表示如下:minlen(v)=len(link(v))+1.
·????????如果我們從任意節(jié)點(diǎn)v_0開始沿后綴鏈接移動(dòng),我們早晚會(huì)到達(dá)初始狀態(tài)t_0.在此情況下,我們得到了一系列不相交的區(qū)
間[minlen(v_i),len(v_i)],其并集是一個(gè)連續(xù)區(qū)間。
?
一個(gè)構(gòu)建后綴自動(dòng)機(jī)的線性時(shí)間算法
我們下面描述這個(gè)算法。算法是在線的,即,逐個(gè)向s中加入字符,并適當(dāng)?shù)貙Ξ?dāng)前的自動(dòng)機(jī)進(jìn)行修改。
?為了達(dá)到線性空間的目的,我們將只存儲(chǔ)每個(gè)狀態(tài)的len,link的值,以及轉(zhuǎn)移列表。我們并不支持標(biāo)記終止?fàn)顟B(tài)(我們將
展示如果需要,如何在后綴自動(dòng)機(jī)構(gòu)建完畢后加上這些標(biāo)記)。
?最初自動(dòng)機(jī)由一個(gè)狀態(tài)t_0組成,我們稱之為0狀態(tài)(其余狀態(tài)將被稱作1,2,...)。對此狀態(tài),令len=0,為方便起見,將link
值設(shè)為-1(指向一個(gè)空狀態(tài))。
?因此,現(xiàn)在的任務(wù)就變成了實(shí)現(xiàn)向當(dāng)前字符串末尾添加一個(gè)字符c的操作。
下面我們描述這一操作:
·???????1.?令last為對應(yīng)整個(gè)字符串的狀態(tài)(最初last=0,在每次字符添加操作后我們都會(huì)改變last的值)。
·????????2.建立一個(gè)新的狀態(tài)cur,令len(cur)=len(last)+1,而link(cur)的值并不確定。
·???????3.?我們最初在last,如果它沒有字符c的轉(zhuǎn)移,那就添加字符c的轉(zhuǎn)移,指向cur,然后走向其后綴鏈接,再次檢查——如果沒
有字符c的轉(zhuǎn)移,就添加上去。如果在某個(gè)節(jié)點(diǎn)已有字符c的轉(zhuǎn)移,就停止,并且令p為這個(gè)狀態(tài)的編號(hào)。
·????????4.如果“某節(jié)點(diǎn)已有字符c的轉(zhuǎn)移”這一事件從未發(fā)生,而我們來到了空狀態(tài)-1(經(jīng)由t_0的后綴指針前來),我們簡單地令link(cur)=0,
跳出。
·????????5.假設(shè)我們停在了某一狀態(tài)q,是從某一個(gè)狀態(tài)p經(jīng)字符c的轉(zhuǎn)移而來。現(xiàn)在有兩種情況:len(p)+1=len(q)或不然。
·????????6.如果len(p)+1=len(q),那么我們簡單地令link(cur)=q,跳出。
·????????7.否則,情況就變得更加復(fù)雜。必須新建一個(gè)q的“拷貝”狀態(tài):建立一個(gè)新的狀態(tài)clone,將q的數(shù)據(jù)拷貝給它(后綴鏈接,以及
轉(zhuǎn)移),除了len的值:需要令len(clone)=len(p)+1.
·????????8.在拷貝之后,我們將cur的后綴鏈接指向clone,并將q的后綴鏈接重定向到clone。
·????????9.最終,我們需要做的最后一件事情就是——從p開始沿著后綴鏈接走,對每個(gè)狀態(tài)我們都檢查是否有指向q的,字符c的轉(zhuǎn)移,
如果有就將其重定向至clone(如果沒有,就終止循環(huán))。
·????????10.在任何情況下,無論在何處終止了這次添加操作,我們最后都將更新last的值,將其賦值為cur。
如果我們還需要知道哪些節(jié)點(diǎn)是終止節(jié)點(diǎn)而哪些不是,我們可以在構(gòu)建整個(gè)字符串的后綴自動(dòng)機(jī)之后找出所有終止節(jié)點(diǎn)。對此我們
考慮對應(yīng)整個(gè)字符串的節(jié)點(diǎn)(顯然,就是我們儲(chǔ)存在變量last中的節(jié)點(diǎn)),我們沿著它的后綴鏈接走,直到到達(dá)初始狀態(tài),并且將
途徑的每個(gè)節(jié)點(diǎn)標(biāo)記為終止節(jié)點(diǎn)。很好理解,如此我們標(biāo)記了字符串s所有后綴的對應(yīng)狀態(tài),也就是我們想要找出的終止?fàn)顟B(tài)。
?
在下一節(jié)中我們從細(xì)節(jié)上考慮算法的每一步,并證明其正確性。
這里我們僅注意一點(diǎn):每個(gè)字符的添加會(huì)導(dǎo)致向自動(dòng)機(jī)中添加一個(gè)或兩個(gè)狀態(tài)。因此,狀態(tài)數(shù)顯然是線性的。?
轉(zhuǎn)移數(shù)量的線性性,以及算法的線性時(shí)間復(fù)雜度較難理解,它們將在下面被證明,位于算法正確性的證明之后。
算法的正確性證明
·????????我們稱轉(zhuǎn)移(p,q)是連續(xù)的,如果len(p)+1=len(q)。否則,即len(p)+1<len(q)時(shí),我們稱之為不連續(xù)轉(zhuǎn)移。
·????????正如在算法描述中可以看到的那樣,連續(xù)轉(zhuǎn)移和不連續(xù)轉(zhuǎn)移導(dǎo)致了算法流程的不同分支。連續(xù)轉(zhuǎn)移)被如此命名是因?yàn)?#xff0c;自第
一次出現(xiàn)后,它們將保持不變。相反,不連續(xù)轉(zhuǎn)移可能會(huì)在向字符串中添加新字符的過程中被改變(可能會(huì)改變該邊指向的狀態(tài))。
·????????為了避免歧義,我們稱s是我們已經(jīng)構(gòu)建了自動(dòng)機(jī)的字符串,它正準(zhǔn)備添加當(dāng)前字符c。
·????????算法開始時(shí)我們創(chuàng)建了新狀態(tài)cur,它將匹配整個(gè)字符串s+c。我們之所以必須新建一個(gè)狀態(tài)的原因是顯然的——在添加新字
符后,出現(xiàn)了一個(gè)新的終點(diǎn)等價(jià)類——一類以新字符串s+c的末尾為結(jié)尾的子串。
·????????在創(chuàng)建新狀態(tài)后,算法從和整個(gè)字符串s匹配的狀態(tài)開始,沿著后綴鏈接移動(dòng),在途中試圖添加指向cur的,字符c的轉(zhuǎn)移。但
我們只會(huì)在不和已存在轉(zhuǎn)移沖突的情況下添加新的轉(zhuǎn)移,因此一旦我們遇到了一個(gè)字符c的轉(zhuǎn)移,我們就必須立刻停止。
·????????最簡單的情形——如果我們來到了空狀態(tài)-1,向途中所有節(jié)點(diǎn)添加了字符c的轉(zhuǎn)移。這就意味著字符c在字符串s中先前未曾出
現(xiàn)。我們成功地添加了所有的轉(zhuǎn)移,只需要記下狀態(tài)cur的后綴鏈接——它顯然必須等于0,因?yàn)檫@種情況下cur匹配字符串s+c的一
切后綴。
·????????第二種情況——當(dāng)我們進(jìn)入一個(gè)已存在的轉(zhuǎn)移(p,q)時(shí)。這意味著我們試圖向字符串中添加字符x+c(其中x是字符串s的某一后
綴,長度為len(p)),且該字符串先前已經(jīng)被加入了自動(dòng)機(jī)(即,字符串x+c已經(jīng)作為子串包含在字符串s中)。因?yàn)槲覀兗僭O(shè)字符
串s的自動(dòng)機(jī)已被正確構(gòu)建,我們并不應(yīng)該添加新的轉(zhuǎn)移。
然而,cur的后綴鏈接指向哪里有一定復(fù)雜性。我們需要將后綴鏈接指向一個(gè)長度恰好和x+c相等的狀態(tài),即,該狀態(tài)的len值必
須等于len(p)+1.但這樣一種情況可能并不存在:在此情況下我們必須添加一個(gè)“分割的”狀態(tài)。
·????????因此,一種可能的情形是,轉(zhuǎn)移(p,q)變得連續(xù),即,len(q)=len(p)+1.在這種情況下,事情變得簡單,不必再進(jìn)行任何分割,
我們只需要將cur的后綴鏈接指向q。
·????????另一種更復(fù)雜的情況——當(dāng)轉(zhuǎn)移不連續(xù)時(shí),即len(q)>len(p)+1.這意味著狀態(tài)q不僅僅匹配對我們必須的,長度len(p)+1的子串
w+c,它還匹配一個(gè)更長的子串。我們不得不新建一個(gè)“分割的”狀態(tài)q:將子串分割成兩段,第一段將恰在長度len(p)+1處結(jié)束。
如何實(shí)現(xiàn)這個(gè)“分割”呢?我們“拷貝”一個(gè)狀態(tài)q,將其復(fù)制為clone,但參數(shù)len(clone)=len(p)+1.我們將q的所有轉(zhuǎn)移復(fù)制給clone,
因?yàn)闊o論如何我們不想改變經(jīng)過p的路徑。從clone出發(fā)的后綴鏈接總是指向q原先的后綴鏈接,而且q的后綴鏈接將指向clone。
在拷貝之后,我們將cur的后綴鏈接指向clone——我們拷貝它就是為了干這個(gè)的。
?最后一步——重定向一些指向q的轉(zhuǎn)移,將它們改為指向clone。哪些轉(zhuǎn)移必須被重定向?只需要重定向那些匹配所有w+c的后
綴的。即,我們需要持續(xù)沿著后綴鏈接移動(dòng),從p開始,只要沒有到達(dá)空狀態(tài)-1或者沒有到達(dá)一個(gè)狀態(tài),其c的轉(zhuǎn)移指向不同于q的
狀態(tài)。
?
證明操作個(gè)數(shù)是線性的
首先,我們曾經(jīng)說過要保證字母表的大小是常數(shù)。否則,那么線性時(shí)間就不再成立:從一個(gè)頂點(diǎn)出發(fā)的轉(zhuǎn)移被儲(chǔ)存在B-樹中,
它支持按值的快速查找和添加操作。因此,如果我們記字母表的大小是k,算法的漸進(jìn)復(fù)雜度將是O(n*logk),空間復(fù)雜度O(n)。但
是,如果字母表足夠小,就有可能犧牲部分空間,不采用平衡樹,而對每個(gè)節(jié)點(diǎn)用一個(gè)長度為k的數(shù)組(支持按值的快速查找)和一
個(gè)動(dòng)態(tài)鏈表(支持快速遍歷所有存在的鍵值)儲(chǔ)存轉(zhuǎn)移。這樣O(n)的算法就能夠運(yùn)行,但需要消耗O(nk)的空間。
因此,我們假設(shè)字母表的大小是常數(shù),即,每個(gè)按字符查詢轉(zhuǎn)移的操作、添加轉(zhuǎn)移、尋找下一個(gè)轉(zhuǎn)移——所有這些操作我們都
認(rèn)為是O(1)的。
?如果我們觀察算法的所有部分,會(huì)發(fā)現(xiàn)其中三處的線性時(shí)間復(fù)雜度并不顯然:
?
·????????第一處:從last狀態(tài)開始,沿著后綴鏈接移動(dòng),并且添加字符c的轉(zhuǎn)移。
·????????第二處:將q復(fù)制給新狀態(tài)clone時(shí)復(fù)制轉(zhuǎn)移。
·????????第三處:將指向q的轉(zhuǎn)移重定向到clone。
我們使用眾所周知的事實(shí):后綴自動(dòng)機(jī)的大小(狀態(tài)和轉(zhuǎn)移的數(shù)目)是線性的。(對狀態(tài)個(gè)數(shù)是線性的證明來自算法本身,對于
轉(zhuǎn)移個(gè)數(shù)是線性的證明,我們將在下面給出,在實(shí)現(xiàn)算法之后。)。
? 那么顯然第一處和第二處是漸進(jìn)線性的,因?yàn)槊看尾僮鞫紩?huì)增加新的狀態(tài)和轉(zhuǎn)移。
? 仍然需要估算第三處總的線性復(fù)雜度——在每次添加字符時(shí)我們將指向q的轉(zhuǎn)移重定向至clone。
?我們不妨關(guān)注shortest(link(last))。注意到,在沿著后綴鏈接上溯的過程中,當(dāng)前節(jié)點(diǎn)的shortest的長度總是嚴(yán)格變小。
?顯然,在向s中添加新字符之前,shortest(link(last))的長度不小于shortest(p)的長度,因?yàn)閘ink(last)至多是p。爾后假設(shè)我們由q
拷貝得到了節(jié)點(diǎn)clone,并試圖從p沿后綴鏈接上溯,將所有通往q的轉(zhuǎn)移重定向?yàn)橥ㄍ鵦lone。設(shè)v是shortest(當(dāng)前節(jié)點(diǎn)),在clone剛
剛建立完成后,v=short(p)。然后,在每次沿后綴鏈接上溯時(shí),v的值都會(huì)變小,而如果當(dāng)前節(jié)點(diǎn)存在經(jīng)過字符c通往q的轉(zhuǎn)移,就意
味著q對應(yīng)的字符串集合中包含v+c,也意味著clone包含的字符串集合中包含v+c。換言之,我們?yōu)閏lone包含的字符串集合找到了一
個(gè)更短的元素,即減少了short(clone)的長度。
在“向s中添加新字符”的整個(gè)流程結(jié)束后,有l(wèi)ink(last)=link(cur)=clone。根據(jù)上面的討論,新的shortest(link(last))的長度變小(或
保持不變),而且這一長度減小的值和上溯的操作數(shù)同階。
?綜上,shortest(link(last))作為s一個(gè)后綴的起始位置在整個(gè)過程中不斷右移,而且每次沿后綴指針上溯都會(huì)導(dǎo)致該位置嚴(yán)格右移。
由于在程序結(jié)束時(shí)這一起始位置不超過n,所以這一過程的時(shí)間復(fù)雜度是線性的。
?(雖然沒什么用,但同樣的討論可以被用來證明第一處的線性性,以代替對狀態(tài)個(gè)數(shù)線性性的證明。)
?
算法的實(shí)現(xiàn)
首先我們描述一個(gè)數(shù)據(jù)結(jié)構(gòu),它儲(chǔ)存特定的一段信息(len,link,轉(zhuǎn)移列表)。如有必要,你可以增加表示終止?fàn)顟B(tài)的標(biāo)簽,以及其他需要
的信息。
我們用STL容器map存儲(chǔ)轉(zhuǎn)移列表,其空間復(fù)雜度為O(n),而處理整個(gè)字符串的時(shí)間復(fù)雜度為O(n*logk)。
[cpp]?view plaincopy
- struct?state?{??
- ????int?len,link;??
- ????map<char,int>?next;??
- };??
后綴自動(dòng)機(jī)本身將被儲(chǔ)存在一個(gè)state類型的數(shù)組中。正如下一節(jié)中將證明的那樣,如果程序中所處理字符串的最大可能長度是MAXN,
那么至多會(huì)占用2*MAXN-1個(gè)狀態(tài)。同時(shí),我們儲(chǔ)存變量last——當(dāng)前匹配整個(gè)字符串的狀態(tài)。
[cpp]?view plaincopy
- const?int?MAXLEN?=?100000;??
- state?st[MAXLEN*2];??
- int?sz,?last;??
我們給出初始化后綴自動(dòng)機(jī)的函數(shù)(新建一個(gè)初始狀態(tài)):
[cpp]?view plaincopy
- void?sa_init()?{??
- ???sz?=?last?=?0;??
- ???st[0].len?=?0;??
- ???st[0].link?=?-1;??
- ???++sz;??
- ????/*?
- ????//?若關(guān)于不同的字符串多次建立后綴自動(dòng)機(jī),就需要執(zhí)行這些代碼:?
- ????for?(int?i=0;?i<MAXLEN*2;?++i)?
- ????????st[i].next.clear();?
- ????*/??
- }??
最后,我們給出基礎(chǔ)函數(shù)的實(shí)現(xiàn)——向當(dāng)前字符串的尾部添加一個(gè)字符,并相應(yīng)地修改后綴自動(dòng)機(jī):
[cpp]?view plaincopy
- void?sa_extend?(char?c)?{??
- ????int?cur?=?sz++;??
- ????st[cur].len?=?st[last].len?+?1;??
- ????int?p;??
- ????for?(p=last;?p!=-1?&&?!st[p].next.count(c);?p=st[p].link)??
- ????????st[p].next[c]?=?cur;??
- ????if?(p?==?-1)??
- ????????st[cur].link?=?0;??
- ????else?{??
- ????????int?q?=?st[p].next[c];??
- ????????if?(st[p].len?+?1?==?st[q].len)??
- ????????st[cur].link?=?q;??
- ????????else?{??
- ????????int?clone?=?sz++;??
- ????????????st[clone].len?=?st[p].len?+?1;??
- ????????????st[clone].next?=?st[q].next;??
- ????????????st[clone].link?=?st[q].link;??
- ????????????for?(;?p!=-1?&&?st[p].next[c]==q;?p=st[p].link)??
- ????????????st[p].next[c]?=?clone;??
- ????????????st[q].link?=?st[cur].link?=?clone;??
- ????????}??
- ????}??
- ????last?=?cur;??
- }??
像前面提到的那樣,如果犧牲部分空間(空間復(fù)雜度增至O(nk),其中k是字母表大小),就能夠?qū)θ魏蝛實(shí)現(xiàn)O(n)構(gòu)建自動(dòng)機(jī)——但這將
會(huì)在每個(gè)狀態(tài)中建立一個(gè)長度為k的數(shù)組(用于快速按字符查詢轉(zhuǎn)移)和一個(gè)轉(zhuǎn)移鏈表(用于快速遍歷或者復(fù)制所有轉(zhuǎn)移)。
后綴自動(dòng)機(jī)的其他性質(zhì)
狀態(tài)的數(shù)量
由長度為n的字符串s建立的后綴自動(dòng)機(jī)的狀態(tài)個(gè)數(shù)不超過2n-1(對于n>=3)。
上面描述的算法證明了這一性質(zhì)(最初自動(dòng)機(jī)包含一個(gè)初始節(jié)點(diǎn),第一步和第二步都會(huì)添加一個(gè)狀態(tài),余下的n-2步每步至多由于需要分割,增加兩個(gè)狀態(tài))。
不過,即使不涉及算法,這一性質(zhì)也容易證明。注意到狀態(tài)個(gè)數(shù)等于不同的終點(diǎn)集合的個(gè)數(shù)。此外,終點(diǎn)集合按“子女是父節(jié)點(diǎn)的不同子集”這一原則構(gòu)成一棵樹。
考慮這棵樹,將其稍作擴(kuò)充:若一個(gè)內(nèi)部節(jié)點(diǎn)只有一個(gè)兒子,那就意味著該兒子的終點(diǎn)集合不包含其父親終點(diǎn)集合中的至少一個(gè)值;那么我們就創(chuàng)建一個(gè)虛擬節(jié)點(diǎn),
其終點(diǎn)集合為這個(gè)值。最終,我們得到了一棵樹,其內(nèi)部節(jié)點(diǎn)度數(shù)均>1,而葉子節(jié)點(diǎn)個(gè)數(shù)不超過n。因此,這棵樹的結(jié)點(diǎn)個(gè)數(shù)不超過2n-1.自然,原樹的結(jié)點(diǎn)個(gè)數(shù)也
不超過2n-1.
這樣我們就獨(dú)立于算法地證明了這一性質(zhì)。
有趣的是,這一上限無法被改善,即存在達(dá)到這一上限的例子:?
"abbbb..."
從第三次開始,每次添加字符時(shí)都會(huì)進(jìn)行分割,因此結(jié)點(diǎn)個(gè)數(shù)將達(dá)到2n-1.
轉(zhuǎn)移的數(shù)量
由長度為n的字符串s建立的后綴自動(dòng)機(jī)中,轉(zhuǎn)移的數(shù)量不超過3n-4(對于n>=3)。
?
證明.
?我們計(jì)算“連續(xù)的”轉(zhuǎn)移個(gè)數(shù)。考慮以t_0為初始節(jié)點(diǎn)的自動(dòng)機(jī)的最長路徑樹。這棵樹將包含所有連續(xù)的轉(zhuǎn)移,樹的邊數(shù)比結(jié)點(diǎn)個(gè)數(shù)小1,這意
味著連續(xù)的轉(zhuǎn)移個(gè)數(shù)不超過2n-2.
? ?我們再來計(jì)算不連續(xù)的轉(zhuǎn)移個(gè)數(shù)。考慮每個(gè)不連續(xù)轉(zhuǎn)移;假設(shè)該轉(zhuǎn)移——轉(zhuǎn)移(p,q),標(biāo)記為c。對自動(dòng)機(jī)運(yùn)行一個(gè)合適的字符串u+c+w,其
中字符串u表示從初始狀態(tài)到p經(jīng)過的最長路徑,w表示從q到任意終止節(jié)點(diǎn)經(jīng)過的最長路徑。一方面,對所有不連續(xù)轉(zhuǎn)移,字符串u+c+w都是不同
的(因?yàn)樽址畊和w僅包含連續(xù)轉(zhuǎn)移)。另一方面,每個(gè)這樣的字符串u+c+w,由于在終止?fàn)顟B(tài)結(jié)束,它必然是完整串s的一個(gè)后綴。由于s的非
空后綴僅有n個(gè),并且完整串s不能是某個(gè)u+c+w(因?yàn)橥暾畇匹配一條包含n個(gè)連續(xù)轉(zhuǎn)移的路徑),那么不連續(xù)轉(zhuǎn)移的總共個(gè)數(shù)不超過n-1.
? ?將這兩個(gè)限制加起來,我們就得到了總數(shù)限制3n-3.注意到雖然狀態(tài)個(gè)數(shù)限制可以被數(shù)據(jù)"abbbb..."達(dá)到,但這個(gè)數(shù)據(jù)并未達(dá)到3n-3的轉(zhuǎn)移個(gè)數(shù)
上限。它的轉(zhuǎn)移個(gè)數(shù)是3n-4,符合要求。
? ?有趣的是,仍然存在達(dá)到轉(zhuǎn)移個(gè)數(shù)上限的數(shù)據(jù):
"abbb...bbbc"。
與后綴樹的聯(lián)系,在后綴自動(dòng)機(jī)上建立后綴樹及反之
? ?我們證明兩個(gè)定理,它們能說明后綴樹和后綴自動(dòng)機(jī)之間的相互關(guān)系。
首先我們假定輸入字符串的每個(gè)后綴都在其后綴樹中對應(yīng)一個(gè)節(jié)點(diǎn)(對于任意字符串而言并不一定成立:例如,對于字符串 "aaa...")。在后綴
樹的典型實(shí)現(xiàn)中,我們通過在字符串的末尾加上一個(gè)特殊符號(hào)(例如"#"或"$")來保證這一點(diǎn)。
? ?方便起見,我們引入如下記號(hào):rev(s)——將字符串s反過來寫,DAWG(s)——這是由字符串s建立的后綴自動(dòng)機(jī),ST(s)——這是s的后綴樹。
? ?我們介紹“擴(kuò)展指針”的概念:對于樹節(jié)點(diǎn)v和字符c,ext[c,v]指向樹中對應(yīng)于字符串c+v的節(jié)點(diǎn)(如果路徑c+v在某邊的終點(diǎn)結(jié)束,那就將其指向該
邊的較低點(diǎn));如果這樣一條路徑c+v不在樹中,那么擴(kuò)展指針未定義。在某種意義上,擴(kuò)展指針的對立面就是后綴鏈接。
?
定理1.DAWG(s)中后綴鏈接組成的樹就是后綴樹ST(rev(s))。
?
定理2.圖DAWG(s)的邊都能用后綴樹ST(rev(s))的擴(kuò)展指針表示。另外,DAWG(s)中的連續(xù)轉(zhuǎn)移就是ST(rev(s))中反向的后綴指針。
?
這兩條定理允許使用兩個(gè)數(shù)據(jù)結(jié)構(gòu)之一在O(n)的時(shí)間內(nèi)構(gòu)建另外一個(gè)——這兩個(gè)簡單的算法將在下面定理3,4討論。
? ?出于說明需要,我們下面展示一個(gè)包含后綴鏈接的后綴自動(dòng)機(jī)的例子,以及其倒序字符串的相應(yīng)后綴樹。例如,令字符
串s="abcbc"。
? ?DAWG("abcbc")(出于簡便我們在每個(gè)狀態(tài)上標(biāo)出其識(shí)別的最長串):
ST("cbcba"):
引理.
對任意兩個(gè)子串u和w,如下三個(gè)陳述是等價(jià)的:
·????????在字符串s中endpos(u)=endpos(w)
·????????在字符串rev(s)中firstpos(rev(u))=firstpos(rev(w))
·????????在后綴樹ST(rev(s))中,rev(u)和rev(w)匹配從根開始的一段相同路徑。
證明十分顯然:如果兩個(gè)字符串的起始位置集合相同,那么一個(gè)字符串只作為另外一個(gè)的前綴出現(xiàn),這意味著在后綴樹中,二者之
間并沒有其他節(jié)點(diǎn)。
?因此,后綴自動(dòng)機(jī)中的狀態(tài)和后綴樹中的節(jié)點(diǎn)一一對應(yīng)。
?
定理1的證明.
?后綴自動(dòng)機(jī)中的狀態(tài)和后綴樹中的節(jié)點(diǎn)一一對應(yīng).
?
考慮任意后綴鏈接y=link(x)。根據(jù)后綴鏈接的定義,longest(y)是longest(x)的一個(gè)后綴,并且y是所有滿足條件(和x的終點(diǎn)集合不同)
的狀態(tài)中使len(y)最大者。
?在rev(s)中,這意味著link(x)指向x所對應(yīng)字符串的最長前綴,該前綴對應(yīng)一個(gè)不同的狀態(tài)y。換句話說,后綴鏈接link(x)指向后綴樹中節(jié)
點(diǎn)x的父親。
?
定理2的證明.
?
后綴自動(dòng)機(jī)中的狀態(tài)和后綴樹中的節(jié)點(diǎn)一一對應(yīng)。
?
考慮后綴自動(dòng)機(jī)DAWG(s)中的任意轉(zhuǎn)移(x,y,c)。這意味著y是包含子串longest(x)+c的終點(diǎn)集合等價(jià)類。對于rev(s),y對應(yīng)了一個(gè)子串,
該子串的firstpos(在文本rev(s)中)和c+rev(longest(x))的firstpos相同。
?這意味著:
rev(longest(y))=ext[c,rev(longest(x))].
(注:這里的用法并不嚴(yán)謹(jǐn)……請自行把字符串和節(jié)點(diǎn)對應(yīng))?
也就是該定理的第一部分,我們還需要證明第二部分:自動(dòng)機(jī)中的所有連續(xù)轉(zhuǎn)移對應(yīng)樹中的后綴指針。對于連續(xù)轉(zhuǎn)移,有l(wèi)ength(y)=length(x)+1,
即在標(biāo)識(shí)字符c后我們到達(dá)了一個(gè)狀態(tài),它是一個(gè)不同的等價(jià)類。這意味著在rev(s)的后綴樹中,x節(jié)點(diǎn)對應(yīng)的字符串恰好是y節(jié)點(diǎn)所對應(yīng)字符串的,長
度比它小1的后綴——也就是說,后綴樹中y的后綴指針指向x,(x,y)就是樹中的反向后綴指針。
?
定理得證。
?
定理3.
使用后綴自動(dòng)機(jī)DAWG(s),我們可以用O(n)的時(shí)間構(gòu)建后綴樹ST(rev(s))。
?
定理4.
使用后綴樹ST(rev(s)),我們可以用O(n)的時(shí)間構(gòu)建后綴自動(dòng)機(jī)DAWG(s)。
?
定理3的證明.
后綴樹ST(rev(s))的節(jié)點(diǎn)和DAWG(s)中的狀態(tài)一一對應(yīng)。樹中與自動(dòng)機(jī)中狀態(tài)v相對應(yīng)的節(jié)點(diǎn)表示一個(gè)長度為len(v)的字符串。
根據(jù)定理1,ST(rev(s))中的邊恰好是把DAWG(s)的后綴鏈接反向,而邊的標(biāo)記可以借助不同狀態(tài)的len計(jì)算(譯者注:從葉子開始,利用自動(dòng)
機(jī)中狀態(tài)的len值計(jì)算后綴樹中的節(jié)點(diǎn)對應(yīng)于哪個(gè)子串),或者更方便地,對自動(dòng)機(jī)中每個(gè)狀態(tài)我們都能知道其endpos集合中的一個(gè)元素(在構(gòu)建
后綴自動(dòng)機(jī)時(shí)維護(hù))。
?至于樹中的后綴指針,我們可以基于定理2構(gòu)建:查找自動(dòng)機(jī)中所有的連續(xù)轉(zhuǎn)移,對所有這樣的轉(zhuǎn)移(x,y)我們都在樹中添加一個(gè)后綴指針link[y]=x。
?因此,在O(n)時(shí)間內(nèi)我們就可以構(gòu)建一棵后綴樹及其中的后綴指針。
?(如果我們認(rèn)為字母表的大小k并非常數(shù),那么重建操作將花費(fèi)O(n*logk)的時(shí)間。)
?
定理4的證明.
后綴自動(dòng)機(jī)DAWG(s)包含的狀態(tài)和ST(rev(s))中的節(jié)點(diǎn)一一對應(yīng)。對每個(gè)狀態(tài)v,其對應(yīng)的最長字符串longest(v)都和后綴樹中從根到v的路徑翻轉(zhuǎn)后
形成的字符串相同。
?
根據(jù)定理2,為了構(gòu)建自動(dòng)機(jī)中的所有轉(zhuǎn)移,我們需要找到所有擴(kuò)展ext[c,v]的指針。
?首先,注意到其中的一些指針直接由樹中的后綴指針得到。事實(shí)上,如果對于樹中任意節(jié)點(diǎn)x,我們考慮其后綴指針y=link[x],那就意味著自動(dòng)機(jī)中
有一個(gè)從y指向x的連續(xù)轉(zhuǎn)移,標(biāo)記為樹節(jié)點(diǎn)x所對應(yīng)字符串的第一個(gè)字符。
?不過,只是這樣我們并不能找到所有的擴(kuò)展。額外地,有必要從葉子到根遍歷后綴樹,而且對于每個(gè)節(jié)點(diǎn)v都遍歷其所有兒子,對每個(gè)兒子觀察所有
擴(kuò)展指針ext[c,w],如果該指針上的字符c在節(jié)點(diǎn)v中還未發(fā)現(xiàn),就將其復(fù)制到v中:
ext[c,v]=ext[c,w],如果ext[c,w]=nil.
這一過程將在O(n)時(shí)間內(nèi)完成,如果我們認(rèn)為字母表的大小是常數(shù)。
最終,還需要建立后綴自動(dòng)機(jī)中的后綴鏈接。而根據(jù)定理1,后綴鏈接可以直接由后綴樹ST(rev(s))的邊獲得。
這樣,我們就得到使用從倒序字符串的后綴樹建立后綴指針的O(n)算法。
?(不過,若字母表的大小k是變量,那么漸進(jìn)復(fù)雜度就是O(n*logk))。
?
在解決問題中的應(yīng)用
下面看在后綴自動(dòng)機(jī)的幫助下我們能做什么。
?
簡便起見,我們假設(shè)字母表的大小k為常數(shù)。
?
存在性查詢
問題.給定文本T,詢問格式如下:給定字符串P,問P是否是T的子串。?
復(fù)雜度要求.預(yù)處理O(length(T)),每次詢問O(P)。
?
算法.我們對文本T用O(length(T))建立后綴自動(dòng)機(jī)。
現(xiàn)在回答單次詢問。假設(shè)狀態(tài)——變量v,最初是初始狀態(tài)T_0.我們沿字符串P給出的路徑走,因此從當(dāng)前狀態(tài)經(jīng)轉(zhuǎn)移來到新的狀態(tài)v
。如果在某時(shí)刻,當(dāng)前狀態(tài)沒有要求字符的轉(zhuǎn)移,那么答案就是"no"。如果我們處理了整個(gè)字符串P,答案就是"yes"。
顯然這一算法將在時(shí)間O(length(P))內(nèi)運(yùn)行完畢。并且,該算法實(shí)際上找出了P在文本中出現(xiàn)過的最長前綴——如果模式串使得這些前綴
都很短,算法將比處理全部模式串要快得多。
?
不同的子串個(gè)數(shù)
問題.給定字符串S,問它有多少不同的子串。
?復(fù)雜度要求.O(length(S))。
?
算法.我們將字符串S建立后綴自動(dòng)機(jī)。
?在后綴自動(dòng)機(jī)中,S的任意子串都對應(yīng)自動(dòng)機(jī)中的一條路徑。答案就是從初始節(jié)點(diǎn)t_0開始,自動(dòng)機(jī)中不同的路徑條數(shù)。
?已知后綴自動(dòng)機(jī)是一張有向無環(huán)圖,我們可以考慮用動(dòng)態(tài)規(guī)劃計(jì)算不同的路徑數(shù)量。
? 也就是,令d[v]為從狀態(tài)v開始的不同路徑條數(shù)(包括長度為零的路徑),則有轉(zhuǎn)移:
即d[v]是v所有后繼節(jié)點(diǎn)的d值之和加上1.
最終答案就是d[t_0]-1(減一以忽略空串)。
不同子串的總長
問題.給定字符串S,求其所有不同子串的總長度。
復(fù)雜度要求.O(length(S)).
?
算法.這一問題的答案和上一題類似,但現(xiàn)在我們必須考慮兩個(gè)狀態(tài):不同子串的個(gè)數(shù)d[v]和它們的總長ans[v].
? ?上一題已描述了d[v]的計(jì)算方法,而ans[v]的計(jì)算方法如下:
即取所有后繼節(jié)點(diǎn)w的ans值,并將它和d[w]相加。因?yàn)檫@是每個(gè)字符串的首字母。
?
字典序第k小子串
問題.給定字符串S,一系列詢問——給出整數(shù)K_i,計(jì)算S的所有子串排序后的第K_i個(gè)。
復(fù)雜度要求.單次詢問O(length(ans)*Alphabet),其中ans是該詢問的答案,Alphabet是字母表大小。
?算法.這一問題的基礎(chǔ)思路和上兩題類似。字典序第k小子串——自動(dòng)機(jī)中字典序第k小的路徑。因此,考慮從每個(gè)狀態(tài)出
發(fā)的不同路徑數(shù),我們將得以輕松地確定第k小路徑,從初始狀態(tài)開始逐位確定答案。
?
最小循環(huán)移位
問題.給定字符串S,找到和它循環(huán)同構(gòu)的字典序最小字符串。
?復(fù)雜度要求.O(length(S)).
?
算法.我們將字符串S+S建立后綴自動(dòng)機(jī)。該自動(dòng)機(jī)將包含和S循環(huán)同構(gòu)的所有字符串。
?從而,問題就簡化成了在自動(dòng)機(jī)中找出字典序最小的,長度為length(S)的路徑,這很簡單:從初始狀態(tài)開始,每一步都貪心地走
,經(jīng)過最小的轉(zhuǎn)移。
?
出現(xiàn)次數(shù)查詢
問題.給定文本T,詢問格式如下:給定字符串P,希望找出P作為子串在文本T中出現(xiàn)了多少次(出現(xiàn)區(qū)間可以相交)。
?復(fù)雜度要求.預(yù)處理O(length(T)),單次詢問O(length(P)).
?
算法.我們將文本T建立后綴自動(dòng)機(jī)。
?然后我們需要進(jìn)行預(yù)處理:對自動(dòng)機(jī)中的每個(gè)狀態(tài)v都計(jì)算cnt[v],等于其endpos(v)集合的大小。事實(shí)上,所有在T中對應(yīng)同一狀態(tài)的
字符串都在T中出現(xiàn)了相同次數(shù),該次數(shù)等于endpos中的位置數(shù)。
?不過,我們無法對所有狀態(tài)明確記錄endpos集合,所以我們只計(jì)算其大小cnt.
?為了實(shí)現(xiàn)這一點(diǎn),如下處理。對每個(gè)狀態(tài),如果它不是由“拷貝”而來,最初就賦值cnt=1.然后我們按長度len降序遍歷所有序列,并將
當(dāng)前的cnt[v]加給后綴鏈接:
?cnt[link(v)]+=cnt[v].
? 你可能會(huì)說我們并沒有對每個(gè)狀態(tài)計(jì)算出了正確的cnt值。
?為什么這是對的?不經(jīng)“拷貝”而來的狀態(tài)恰好有l(wèi)ength(S)個(gè),而且其中的第i個(gè)是我們添加第i個(gè)字符時(shí)得到的。因此,最初這些狀態(tài)的cnt=1,
其他狀態(tài)的cnt=0.
?然后我們對每個(gè)狀態(tài)v執(zhí)行如下操作:cnt[link(v)]+=cnt[v].其意義在于,如果某字符串對應(yīng)狀態(tài)v,曾在cnt[v]中出現(xiàn)過,那么它的所有后綴都
同樣在其中出現(xiàn)。
?這樣,我們就掌握了如何對自動(dòng)機(jī)中所有狀態(tài)計(jì)算cnt值的方法。
?在此之后,詢問的答案就變得平凡——只需要返回cnt[t],其中t是模式串P所對應(yīng)的狀態(tài)。
?
首次出現(xiàn)位置查詢
問題.給定文本T,詢問格式如下:給定字符串P,求P在文本中第一次出現(xiàn)的位置。
?復(fù)雜度要求.預(yù)處理O(length(T)),單次詢問O(length(P)).
?
算法.對文本T建立后綴自動(dòng)機(jī)。
?為了解決這一問題,我們需要預(yù)處理firstpos,找到自動(dòng)機(jī)中所有狀態(tài)的出現(xiàn)位置,即,對每個(gè)狀態(tài)v我們希望找到一個(gè)位置firstpos[v],代表
其第一次出現(xiàn)的位置。換句話說,我們希望預(yù)先找出每個(gè)endpos(v)中的最小元素(我們無法明確記錄整個(gè)endpos集合)。
? 維護(hù)這些firstpos的最簡單方法是在構(gòu)建自動(dòng)機(jī)時(shí)一并計(jì)算,當(dāng)我們創(chuàng)建新的狀態(tài)cur時(shí),一旦進(jìn)入函數(shù)sa_extend(),就確定該值:
?firstpos(cur)=len(cur)-1(如果我們的下標(biāo)從0開始)。
當(dāng)拷貝節(jié)點(diǎn)q時(shí),令:
?firstpos(clone)=firstpos(q),(因?yàn)橹挥幸粋€(gè)別的可能值——firstpos(cur),顯然更大)。
?這樣就得到了查詢的答案——firstpos(t)-length(P)+1,其中t是模式串P對應(yīng)的狀態(tài)。
?
所有出現(xiàn)位置查詢
問題.給定文本T,詢問格式如下:給定字符串P,要求給出P在T中的所有出現(xiàn)位置(出現(xiàn)區(qū)間可以相交)。
復(fù)雜度要求.預(yù)處理O(length(T))。單次詢問O(length(P)+answer(P)),其中answer(P)是答案集合的大小,即,要求時(shí)間復(fù)雜度和輸入輸出同階。
?
算法.對文本T建立后綴自動(dòng)機(jī),和上一個(gè)問題相似,在構(gòu)建自動(dòng)機(jī)的過程中對每個(gè)狀態(tài)計(jì)算第一次出現(xiàn)的終點(diǎn)firstpos。
?
假設(shè)我們收到了一個(gè)詢問——字符串P。我們找到了它對應(yīng)的狀態(tài)t。
?顯然應(yīng)當(dāng)返回firstpos(t)。還有哪些位置?我們考慮自動(dòng)機(jī)中那些包含了字符串P的狀態(tài),即那些P是其后綴的狀態(tài)。
?換言之,我們需要找出所有能通過后綴鏈接到達(dá)狀態(tài)t的狀態(tài)。
?因此,為了解決這一問題,我們需要對每個(gè)節(jié)點(diǎn)儲(chǔ)存指向它的所有后綴鏈接。為了找到答案,我們需要沿著這些翻轉(zhuǎn)的后綴鏈接進(jìn)行DFS/BFS,從狀
態(tài)t開始。
?這一遍歷將在O(answer(P))時(shí)間內(nèi)結(jié)束,因?yàn)槲覀儾粫?huì)訪問同一狀態(tài)兩次(因?yàn)槊總€(gè)狀態(tài)的后綴鏈接僅指向一個(gè)點(diǎn),因此不可能有兩條路徑通往同一
狀態(tài))。
?然而,兩個(gè)狀態(tài)的firstpos值可能會(huì)相同,如果一個(gè)狀態(tài)是由另一個(gè)拷貝而來。但這不會(huì)影響漸進(jìn)復(fù)雜度,因?yàn)槊總€(gè)非拷貝得到的節(jié)點(diǎn)只會(huì)有一個(gè)拷貝。
?此外,你可以輕松地除去那些重復(fù)的位置,如果我們不考慮那些拷貝得來的狀態(tài)的firstpos。事實(shí)上,所有拷貝得來的狀態(tài)都被其“母本”狀態(tài)的后綴鏈接
指向。因此,我們對每個(gè)節(jié)點(diǎn)記錄標(biāo)簽is_clon,我們不考慮那些is_clon=true的狀態(tài)的firstpos。這樣我們就得到了answer(P)個(gè)不重復(fù)地狀態(tài)。
?給出一個(gè)離線版本的實(shí)現(xiàn):
[cpp]?view plaincopy
- struct?state?{??
- ????...??
- ????bool?is_clon;??
- ????int?first_pos;??
- ????vector<int>?inv_link;??
- ????};??
- ??????
- ???????
- ...?后綴自動(dòng)機(jī)構(gòu)建完畢?...??
- for?(int?v=1;?v<sz;?++v)??
- ????st[st[v].link].inv_link.push_back?(v);??
- ...??
- ????
- ???
- //?回答詢問--返回所有的出現(xiàn)位置(出現(xiàn)區(qū)間可能有重疊)??
- void?output_all_occurences?(int?v,?int?P_length)?{??
- ???if?(!?st[v].is_clon)??
- ????????cout?<<?st[v].first_pos?-?P_length?+?1?<<?endl;??
- ???for?(size_t?i=0;?i<st[v].inv_link.size();?++i)??
- ????????output_all_occurences?(st[v].inv_link[i],?P_length);??
- }??
查詢不在文本中出現(xiàn)的最短字符串
問題.給定字符串S和字母表。要求找出一個(gè)長度最短的字符串,使得它不是S的子串。
復(fù)雜度要求.O(length(S)).
?算法.在字符串S的后綴自動(dòng)機(jī)上進(jìn)行動(dòng)態(tài)規(guī)劃。
?令d[v]為節(jié)點(diǎn)v的答案,即,我們已經(jīng)輸入了字符串的一部分,匹配到v,我們希望找出有待添加的最少字符數(shù)量,以到達(dá)一個(gè)不存在的轉(zhuǎn)移。
?計(jì)算d[v]非常簡單。如果在v處某個(gè)轉(zhuǎn)移不存在,那么d[v]=1:用這個(gè)字符來“跳出”自動(dòng)機(jī),以得到所求字符串。
?否則,一個(gè)字符串無法達(dá)到要求,因此我們必須取所有字符中的最小答案:
原問題的答案等于d[t_0],而所求字符串可以用記錄轉(zhuǎn)移路徑的方法得到。
?
求兩個(gè)字符串的最長公共子串
問題.給定兩個(gè)字符串S和T。要求找出它們的最長公共子串,即一個(gè)字符串X,它同時(shí)是S和T的子串。
? ?復(fù)雜度要求.O(length(S)+length(T)).
?
算法.我們對字符串S建立后綴自動(dòng)機(jī)。
?
我們按照字符串T在自動(dòng)機(jī)上走,查找它每個(gè)前綴在S中出現(xiàn)過的最長后綴。換句話說,對字符串T中的每個(gè)位置,我們都想找出S和T在該位置結(jié)束的最長公共子串。
?
為了實(shí)現(xiàn)這一點(diǎn),我們定義兩個(gè)變量:當(dāng)前狀態(tài)v和當(dāng)前長度l。這兩個(gè)變量描述了當(dāng)前的匹配部分:其長度和狀態(tài),對應(yīng)哪個(gè)字符串(如果不儲(chǔ)存長度就無法確定這一點(diǎn),因?yàn)橐粋€(gè)狀態(tài)可能匹配多個(gè)有不同長度的字符串)。
?
最初,p=t_0,l=0,即,匹配部分為空。
?
現(xiàn)在我們考慮字符T[i],我們希望找到這個(gè)位置的答案。
·????????如果自動(dòng)機(jī)中的狀態(tài)v有一個(gè)符號(hào)T[i]的轉(zhuǎn)移,我們可以簡單地走這個(gè)轉(zhuǎn)移,然后將長度l加一。
·????????如果狀態(tài)v沒有該轉(zhuǎn)移,我們應(yīng)當(dāng)嘗試縮短當(dāng)前匹配部分,為此應(yīng)當(dāng)沿著后綴鏈接走:
v=link(v).
在此情況下,當(dāng)前匹配長度必須被減少,但留下的部分盡可能多。顯然,應(yīng)令l=len(v):
l=len(v).
若新到達(dá)的狀態(tài)仍然沒有我們想要的轉(zhuǎn)移,那我們必須再次沿著后綴鏈接走,并且減少l的值,直到我們找到一個(gè)轉(zhuǎn)移(那就返回第一步),或者我們最終到達(dá)了空狀態(tài)-1(這意味著字符T[i]并未在S中出現(xiàn),所以令v=l=0然后繼續(xù)處理下一個(gè)i)。
原問題的答案就是l曾經(jīng)達(dá)到的最大值。
?
這種遍歷方法的漸進(jìn)復(fù)雜度是O(length(T)),因?yàn)樵谝淮我苿?dòng)中我們要么將l加一,要么沿著后綴鏈接走了若干次,每次都會(huì)嚴(yán)格減少l。因此,l總共的減少值之和不可能超過length(T),這意味著線性時(shí)間復(fù)雜度。
?
代碼實(shí)現(xiàn):
[cpp]?view plaincopy
- string?lcs?(string?s,?string?t)?{??
- ????sa_init();??
- ????for?(int?i=0;?i<(int)s.length();?++i)??
- ????????sa_extend?(s[i]);??
- ??????
- ????int?v?=?0,??l?=?0,??
- ????????best?=?0,??bestpos?=?0;??
- ????for?(int?i=0;?i<(int)t.length();?++i)?{??
- ????????while?(v?&&?!?st[v].next.count(t[i]))?{??
- ????????????v?=?st[v].link;??
- ????????????l?=?st[v].length;??
- ????????}??
- ????????if?(st[v].next.count(t[i]))?{??
- ????????v?=?st[v].next[t[i]];??
- ?????????++l;??
- ????????}??
- ????????if?(l?>?best)??
- ????????????best?=?l,??bestpos?=?i;??
- ????}??
- ????return?t.substr?(bestpos-best+1,?best);??
- }??
多個(gè)字符串的最長公共子串
問題.給出K個(gè)字符串S_1~S_K。要求找出它們的最長公共子串,即一個(gè)字符串X,它是所有S_i的子串。
? ?復(fù)雜度要求.O(∑length(S_I)*K).
?
算法.將所有S_i連接在一起成為一個(gè)新的字符串T,其中每個(gè)S_i后要加上一個(gè)不同的分隔符D_i(即加上K個(gè)額外的不同特殊字符D_1~D_K):
我們對字符串T構(gòu)建后綴自動(dòng)機(jī)。
?現(xiàn)在我們需要在后綴自動(dòng)機(jī)找出一個(gè)字符串,它是所有字符串S_i的子串。注意到如果一個(gè)子串在某個(gè)字符串S_j中出現(xiàn)過,那么后綴自動(dòng)機(jī)中存在一
條以這個(gè)子串為前綴的路徑,包含分隔符D_j,但不包含其他分隔符D_1,...,D_j-1,D_j+1,...,D_k。
?因此,我們需要計(jì)算“可達(dá)性”:對自動(dòng)機(jī)中的每個(gè)狀態(tài)和每個(gè)字符D_i,計(jì)算是否有一條從該狀態(tài)開始的路徑,包含分隔符D_i,但不包含別的分隔符。
很容易用DFS/BFS或者動(dòng)態(tài)規(guī)劃實(shí)現(xiàn)。在此之后,原問題的答案就是字符串longest(v),其中v能夠到達(dá)所有的分隔符。
?
OJ上的題目
可以用后綴自動(dòng)機(jī)解決的題目:
SPOJ#7258 SUBLEX"Lexicographical Substring Search"
BZOJ#2555 Substring
SPOJ#8222 NSUBSTR"Substrings"
SPOJ#1812 LCS2"Longest Common Substrings?II"
BZOJ#3998 弦論
參考文獻(xiàn)
我們首先給出和后綴自動(dòng)機(jī)有關(guān)的一些第一手研究:
·????????A. Blumer, J. Blumer, A. Ehrenfeucht, D. Haussler, R.McConnell.Linear Size Finite Automata for the Set of All Subwordsof a Word. An Outline of Results?[1983]
·????????A. Blumer, J. Blumer, A. Ehrenfeucht, D. Haussler.The SmallestAutomaton Recognizing the Subwords of a Text[1984]
·????????Maxime Crochemore.?OptimalFactor Transducers?[1985]
·????????Maxime Crochemore.?Transducersand Repetitions?[1986]
·????????A. Nerode.?Linear automatontransformations?[1958]
此外,還有一些當(dāng)代資源,這一主題在許多有關(guān)字符串算法的書籍中被提到:
·????????Maxime Crochemore, Wowjcieh Rytter.?Jewels ofStringology?[2002]
·????????Bill Smyth.?Computing Patterns in Strings?[2003]
·????????Билл Смит.?Методы и алгоритмы вычислений на строках?[2006]
總結(jié)
- 上一篇: 后缀数组
- 下一篇: 乌鲁木齐网络赛J题(最小费用最大流模板)