最長迴文子串(馬拉車算法)

馬拉車算法:https://articles.leetcode.com/longest-palindromic-substring-part-ii/

 

 

Manacher 算法是時間、空間複雜度都爲 O(n) 的解決 Longest palindromic substring(最長迴文子串)的算法。迴文串是中心對稱的串,比如 'abcba'、'abccba'。那麼最長迴文子串顧名思義,就是求一個序列中的子串中,最長的迴文串。本文最後用 Python 實現算法,爲了方便理解,文中出現的數學式也採用 py 的記法。

在 leetcode 上用時間複雜度 O(n**2)、空間複雜度 O(1) 的算法做完這道題之後,搜了一下發現有 O(n) 的算法。可惜英文 wikipedia 上的描述太抽象,中文介紹又沒找到說的很明白的,於是就下決心自己寫一篇中文比較清楚的。我弄明白這個算法是通過 leetcode 上的一篇文章,也就是 wikipedia 詞條中第一個外部鏈接。鏈接在此(http://articles.leetcode.com/2011/11/longest-palindromic-substring-part-ii.html),圖文並茂,很容易懂(我就是沒通讀文章,主要看圖和圖的說明就弄懂了)。如果還是覺得讀英文費勁的話,那接着讀我這篇吧。

Manacher 算法的最終目的,是根據原串構造出一個新隊列,內容是以該點爲中心,最長的對稱長度。爲了解決對稱奇偶性的問題(比如 aba 和 abba,常規算法需要分成兩種情況),首先是構造一個輔助串,在首尾和任何兩字符中間插入一個相同的字符。比如串 ababa,構造成 #a#b#a#b#a#。接下來就是構造一個新隊列,裏面記錄以該點爲中心的最長對稱長度:

複製代碼

S1 = ababa
T1 = # a # b # a # b # a #
P1 = 0 1 0 3 0 5 0 3 0 1 0

S2 = abaaba
T2 = # a # b # a # a # b # a #
P2 = 0 1 0 3 0 1 6 1 0 3 0 1 0

複製代碼

那麼,具體該如何構造序列 P 呢?首先,去除串長不大於1的 corner case,我們總能得到 P 前兩個元素的值。

T = # ? # ...
P = 0 1 ? ...

然後,我們就可以根據已經知道的 P 元素和 T 中的元素一步一步求出後面的值了。問題分解成:已知 P[:i],求 P[i] 的問題。

接下來,就要討論一下回文子使 P 具有哪些性質。下面用 leetcode 文章中的例子,s = 'babcbabcbaccba'(len(s) == 14,t = '#b#a#b#c#b#a#b#c#b#a#c#c#b#a#',len(t) == len(s)*2+1 == 29)。


1.核心算法

如下圖,假設當我們已知 T、P[:8] 時,求 P[9]。

     0 1 2 3 4 5 6 7 8 9 0 1 2 3 4  5 6 7 8 9 0 1 2 3 4 5 6 7 8
T = [# b # a # b # c # b # a # b #] c # b # a # c # c # b # a #
P = [0 1 0 3 0 1 0 7 0 ? ...

觀察我用方括號包起來的部分,正是以 T[7] 爲中心,7爲長度構成的迴文。直觀來看,由於迴文是對稱的結構,P 中的元素值似乎也應該是根據中心對稱的,那麼 P[9] = P[5] = 1,從結果上來看也是正確的。那麼接下來往後填,很快你會發現這個結論有錯誤。

複製代碼

     0 1  2 3 4 5 6  7 8 9 0 1 2 3 4  5 6 7 8 9 0 1 2 3 4 5 6 7 8
T = {# b [# a # b #} c # b # a # b #] c # b # a # c # c # b # a #
P =  0 1  0 3 0 1 0  7 0 1 0 ? ...
i = 11
center = 7
right = 14
mirror = 3

複製代碼

當填到 P[11] 時,紅色字部分是 T[7] 爲中心7爲長度的迴文,而黃色背景色部分,是以 P[11] 爲中心9爲長度的迴文。按照上一段的結論,我們應該填入 P[7-(11-7)] = P[3] = 3,但實際上應該填入9。這是怎麼回事呢?

爲了說清楚這個問題,先來定義一些變量。首先 center 是已知的對稱點,right 是已知對稱點的最右端,當前求的 P 索引爲 i,i 關於 center 的對稱點索引是 mirror。上面在求 P[9] 和 P[11] 時,center == 7,right == 14。接下來,讓我們接着往後掃描,看一下另外一個情況:已知 center==11, right==20, 求 P[15]。

複製代碼

     0 1  2 3 4 5 6 7 8 9 0 1 2 3 4  5 6 7 8 9 0  1 2 3 4 5 6 7 8
T = {# b [# a # b # c # b # a # b #} c # b # a #] c # c # b # a #
P =  0 1  0 3 0 1 0 7 0 1 0 9 0 1 0  ? ...
i = 15
center = 11
right = 20
mirror = 7

複製代碼

觀察這兩個過程,不難發現,發生這種情況的原因,是 mirror 爲中心點的迴文(示例中用黃色背景標註,{}之間的迴文),其範圍超過了以 center 爲中心點的迴文的左端(紅字標註,[] 之間的迴文)。而凡是 P[i] == P[mirror] 的迴文,其 P[mirror] 的範圍都不超過 P[center] 的範圍。具體來說,就是 P[mirror] 的左端不超過 P[center] 的左端。

那麼怎麼來判斷呢?mirror 到 P[center] 左側的長度是 right-i,如果這段長度不小於 P[mirror] 的話,P[mirror] 就在 P[center] 的範圍內。如果沒想明白的話,下面是用笨方法來的數學推導:

複製代碼

PalindromeLeftOf(mirror) = mirror - P[mirror]
PalindromeLeftOf(center) = center - (right - center)
mirror = center - (i - center)

if leftOf(mirror) is inside P
PalindromeLeftOf(center) <= PalindromeLeftOf(mirror)  =>
center - (right - center) <= mirror - P[mirror]   =>
center - (right - center) <= center - (i- center) - P[mirror] =>
-right <= -i - P[mirror] =>
P[mirror] <= right - i

複製代碼

需要注意的是,由於 P[mirror] == right - i  情況的存在,也就是 P[mirror] 的左端與 P[center] 的左端重合,這也就意味着 P[i] 的當前右端爲 right,P[i] 也就有可能會比 P[mirror] 長,這種情況下仍需對 P[i] 進行擴展操作。

到目前爲止,通過已知的 p[:i] 用 O(1) 的操作計算得出 p[i] 的結果的過程就講完了。這個過程是 Manacher 算法的核心,也是最難理解的部分,剩下的過程就不復雜了。


2.擴展 P 的過程

現在再回到本文一開始,當我們拿到源數據時,馬上可以得到的是 P 的前兩項。

T = # b # a # b # c # b # a # b # c # b # a # c # c # b # a #
P = 0 1 ? ...
center = 1
right = 2

開始計算,i=2, mirror=0, P[mirror] == right-i。按照上一節的結論,需要對 P 進行擴展,也就是對 T[i+n] == T[i-n] 進行依次判斷。不幸的是,第一次就失敗了,於是我們進入下一步:

T = # b # a # b # c # b # a # b # c # b # a # c # c # b # a #
P = 0 1 0 ? ...
i = 3
center = ?
right = 2
mirror =?

不論是以 P[1] 爲中心,還是 P[2] 爲中心,right 的值總是2,而下一步 i > right,這時不論哪個是中心,都不存在 i 的對稱點了。那這種情況下,我們就從零開始吧:

複製代碼

    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8
T = # b # a # b # c # b # a # b # c # b # a # c # c # b # a #
P = 0 1 0 3 ? ...
i = 4
center = 3
right = 6
mirror = 2

複製代碼

這次我們得到了新的 center、right,並且 right 比原來的大,那麼就用新的來替換老的。

到現在爲止,i > right 和 P[mirror] <= right - i 兩種情況都已經討論完了,那麼還剩最後一種。


3.最後一步

現在還剩下的情況就剩 i <= right and P[mirror] > right - i 這一種了。既然是要擴展 P,那麼從零開始似乎也沒什麼不妥的。先考慮下面的情況:

複製代碼

    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8
T = # b # a # b # c # b # a # b # c # b # a # c # c # b # a #
P = 0 1 0 3 0 1 0 7 0 1 0 9 0 1 0 ? ...
i = 15
center = 11
right = 20
mirror = 7

複製代碼

這種情況前面已經見過一次了 P[mirror] > right-i。這時,我們需要擴展 P[15],但是需要從 n=0 開始判斷 P[15+n] == P[15-n] 成立嗎?因爲 P[mirror]=7 > right-i=5,由於當 0<=n<=7 時,有 P[mirror-n] == P[mirror+n],而當 0<=n<=5 時有 P[mirror-n] == P[i+n],所以不難得出當 0<=n<=5 時有 P[i+n] == P[i-n]。換句話說,由於 P[mirror] 的迴文範圍超過了 P[center] 的範圍,所以 P[i] 在不超過 P[center] 的範圍內一定是迴文。所以這時我們不需要從0開始判斷,而只要從 right+1 開始判斷就可以了。


 

代碼

再上代碼之前,首先對上述情況進行重分類和合並。

1. 直接用 O(1) 操作得到 P[i],不需要對 P 進行擴展,也不需要掃描 T,center、right 的值也不變。

2. 需要對 P 進行擴展,這時會從 T[right+1] 開始掃描,同時也需要更新 center、right 的值。

情況1的複雜度爲 O(1),情況2的複雜度雖然爲 O(n),但由於算法的實現,能夠保證從左到右掃描 T 時,每次都從 right+1 開始,並且掃描的最右端成爲 right,這樣就能夠保證從左到右訪問 T 中的每個元素不超過2次。所以,Manacher 算法的複雜度是 O(n)。

那麼按照我的分類,貼上一段 Python 代碼的實現,可能會跟你見到的標準實現長得不太一樣。

複製代碼

 1 # @param {string} s
 2 # @return {string}
 3 def longestPalindrome(s):
 4     if len(s) <= 1:
 5         return s
 6 
 7     THE_ANSWER = 42
 8     T = [THE_ANSWER]
 9     for c in s:
10         T.append(c)
11         T.append(THE_ANSWER)
12 
13     c, r, size = 1, 2, len(T)
14     P = [0, 1] + [None] * (size-2)
15     maxIndex, maxCount = 0, 1
16     for i in xrange(2, size):
17         m = c*2 - i     # mirror = center - (i - center)
18         if r > i and P[m] < r-i:
19             # case 1, just set P[i] <- P[m]
20             P[i] = P[m]
21             continue
22 
23         # case 2, expand P
24         count = min(i, size-i-1) # n's limit
25         # scan, from if r <= i then T[i+1] else T[right+1]
26         for n in xrange((1 if r <= i else r+1-i), count+1):
27             if T[i+n] != T[i-n]:
28                 count = n-1
29                 break
30                 
31         # update center and right, save P[i], compare with the max
32         c = i
33         r = i+count
34         P[i] = count
35         if count > maxCount:
36             maxCount = count
37             maxIndex = i-count
38 
39     maxIndex = maxIndex // 2
40     return s[maxIndex:maxIndex+maxCount]

 

轉自:http://www.cnblogs.com/egust/p/4580299.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章