迴文樹

  對於文本T,設T’是T的逆序文本,若T'與T相同,那麼稱T爲迴文。好比aba、abba都是迴文。node

  迴文樹是用於組織和統計文本T中全部迴文的數據結構,能夠優雅地解決大量回文有關的問題。如同AC自動機,後綴自動機等處理文本的數據結構同樣,迴文樹的創建也擁有着線性的時間複雜度,而且其創建過程是在線的。算法

  下面咱們來描述迴文樹的定義和創建過程。數據結構

定義

  在文本T上創建的迴文樹中的結點表示文本T的某段子迴文串,且文本T中的每一個子迴文串都對應迴文樹中的一個結點,咱們將結點n所表明的迴文記做n.repr。結點與其孩子之間經過軌跡聯繫,若某個結點u經過字符c標記的軌跡抵達到結點v(即存在c標記的邊(u,v)),那麼v所表明的迴文等同於u所表明的迴文的兩端加上c,即v.repr = c + u.repr + c。函數

  對於每一個結點,咱們記錄結點對應迴文的長度。對於結點n,咱們記n.len爲其迴文長度,即n.len=|n.repr|。spa

  如同AC自動機和後綴自動機同樣,迴文樹結點也有失敗指針fail。對於結點x,設y=x.fail,那麼y.repr是x.repr的後綴,且是最長後綴(|y.len|<|x.len|)。對於從某個結點x出發,沿着fail鏈能訪問到的結點序列,咱們稱爲x的fail鏈。顯然全部x的迴文後綴都出如今了x的fail鏈上(根據定義)。指針

  迴文樹中初始有兩個結點,even和odd。其中even.len = 0,即even表示的是空串(按照定義空串固然也是迴文)。而odd.len=-1,這裏可能會感受比較奇怪,可是看到後面就知道用處了。咱們順便將even.fail設爲odd,而odd.fail設爲空NIL便可。實際上咱們考慮到孩子的存在,而孩子是在迴文兩端加上相同的字符,即對於父親father和孩子child,必定有father.len+2=child.len,那麼even的孩子是偶數長度的迴文串,而odd的孩子均爲奇數長度的迴文串,將even.len設爲0,能夠保證T中全部偶數串均可以掛在even或even的後代結點上,而將odd設爲-1,能夠幫助全部奇數長度的迴文掛在其下。而且因爲空串even的存在,咱們能保證每一個非空迴文的fail指針均能指向一個結點(空串是全部串的後綴,且是迴文,所以知足fail指針的要求)。code

  迴文樹中還須要有一個指針last,初始時其指向even和odd都可。last會在每次向樹中加入新的字符後,指向當前讀取的文本(T的某段前綴)的能造成最長迴文的後綴(因爲單個字符也是文本,所以last始終能指向一個有效結點)。blog

創建

  好的,上面就是迴文樹中的定義,下面咱們要講述迴文樹的創建過程。假設咱們已經利用文本T的前綴T[1...k-1]創建了一株合法的迴文樹(此時全部的上面的定義都對於文本T[1...k-1]是知足的)。那麼如今讀入新的字符c=T[k]。ast

  如今咱們須要作的工做是將在T[1...k-1]創建的迴文樹Tree[k-1]轉變爲在T[1...k]上創建的迴文樹Tree[k]。先觀察兩株迴文樹有什麼區別,區別在於Tree[k]可能擁有比Tree[k-1]更多的迴文,而這個迴文必定是以新加入的字符T[k]做爲結尾的。咱們考慮須要加入多少個結點,等同於考慮T[1...k]較T[1...k-1]多了哪些迴文。設T[l...k]是T[1...k]全部迴文後綴中最長的迴文,那麼咱們能夠保證T[1...k]只可能比T[1...k-1]多了迴文T[l...k](固然也可能兩者擁有相同的迴文子串)。那麼對於r>l,若T[r...k]也是迴文,咱們如何保證T[1...k-1]中包含迴文T[r...k]呢?這源於迴文的性質,因爲T[r...k]是T[l...k]的後綴,且兩者都是迴文,所以T[l...(l+k-r)]=T[r...k],而(l+k-r)<k,所以T[r...k]是T[1...k-1]的某個子串。class

  好了,根據上一段的說明,咱們瞭解到最多隻須要向Tree[k-1]中加入一個結點就可使得與Tree[k]有相同的結點。固然也有可能T[l...k]已經存在於T[1...k-1],這時候咱們就不須要追加結點。不管哪一種狀況,咱們都須要先找到T[l...k]在Tree[k-1]中的父結點。T[l...k]的父結點必然是T[l+1...k-1](若是l=k,那麼表明的就是odd)。而咱們注意到last記錄了T[1...k-1]中的最長後綴迴文,而T[l+1...k-1]也是T[1...k-1]的某個迴文後綴,所以T[l+1...k-1]必然出如今了last的fail鏈上。而根據T[l...k]是T[1...k]的最長迴文後綴,所以T[l+1...k-1]必然是last的fail鏈上最長的某個符合下面條件的結點x:T[k-x.len-1]=T[k]。因爲fail鏈上的結點的長度是遞減的,所以T[l+1...k-1]是last的fail鏈上首個知足該條件的結點。寫做代碼就以下:

x = last;
for(; k - x.len >= 0; x = x.fail);
for(; T[k - x.len - 1] != T[k]; x = x.fail);

  咱們已經成功找到了父親,以後利用父親是否存在c標記的軌跡,就能夠判斷T[l...k]是否已經存在於Tree[k-1]中了,若是已經存在天然就不須要增長了。可是根據last的定義,咱們須要將last調整爲x.next[c]才能保證上面的定義不被破壞,即last指向文本的最長迴文後綴。

if(x.next[c] != NIL)
    last = x
    return

  固然還有一種是T[l...k]不存在於Tree[k-1]。咱們須要加入新的結點now,來保證能建立出Tree[k]。很顯然now.len = x.len + 2。而且咱們還須要創建now與x的父子聯繫:

now = new-node
now.len = x.len + 2
x.next[c] = now

  可是作完了這些就OK了嗎,看看咱們新建立的Tree[k]還違反了上面提出的哪些定義。是的,now的fail指針尚未正確設置。因爲咱們說過結點的fail指針指向的是該結點的最長迴文後綴,而因爲now和now.fail均爲迴文,所以now.fail也是now的迴文前綴,即在now加入以前,now.fail已經存在於原來的迴文樹中了,這也說明了now.fail永遠能指向正確的結點,而且不會由於後面新的字符的加入而改變。咱們接下來聊一聊如何找到now.fail。

  因爲now=c+x+c,而now.fail是now的後綴,所以now.fail在剔除兩端的c以後獲得的結點y(即y是now.fail的父親),必然是x的後綴迴文(注意因爲x沒有c標記的軌跡,所以x不多是y,故y只多是x的後綴迴文)。而x的後綴迴文均落在x的fail鏈上,因此咱們能夠在fail鏈上找到y,而y也就是x的fail鏈上最長(換言之首個)知足下面條件的結點:T[k-y.len-1]=T[k]。固然這個過程當中,若x是odd,那麼now實際上只有一個字符c,咱們上面所說的尋找fail指針指向的y.next[c]的算法找到的fail長度至少爲1,所以沒法找到x的fail,咱們能夠特判,並將其fail指針設置爲空串,即even。

if(x.len == 1)
    now.fail = even
else
    y = x.fail
    for(; T[k-y.len-1] != T[k]; y = y.fail)
    now.fail = y.next[c]

  固然也不要忘記須要將last設置爲正確值now。

last = now

  將上面幾部分代碼合併起來,咱們就獲得了從Tree[k-1]到Tree[k]的轉移函數。

分析

  時間複雜度的分析以下:

  按照算法,每次讀入一個新的結點,咱們最多將循環結束後的last的len增長2,同時每次沿着fail鏈尋找新結點的父親x的時候,每次循環都將會使得last的len減小1。在下一次讀入新字符後,咱們先找到x(last的某個後綴),以後必然會從x.fail開始沿着其fail鏈移動尋找y,而x.fail的len是必然要不可能大於last.fail.len的。所以咱們發現now.fail.len<=last.fail.len+2。每次從x.fail開始沿着其fail鏈移動尋找y,都會使得last.fail.len減小至少1,而每讀入一個字符最多使得last.fail.len增長2,由last.fail.len>=0能夠推出最多沿着從x.fail開始沿着其fail鏈移動尋找y共計2|T|次。其它每次讀入一個字符的時間複雜度均爲常數O(1),所以時間複雜度爲O(|T|)+O(|T|)+|T|*O(1)=O(|T|)。
  空間複雜度的分析以下:

  每次讀入一個字符最多建立1個結點,加上初始時建立的even和odd,總計最多建立|T|+2個結點,所以空間複雜度爲O(|T|)。同時這也說明一段文本T中最多有|T|種不一樣的迴文。

相關文章
相關標籤/搜索