1. 程式人生 > >最長迴文字串演算法-Manacher’s Algorithm-馬拉車演算法

最長迴文字串演算法-Manacher’s Algorithm-馬拉車演算法

除了翻譯之外,其中還加入了個人的理解的部分,將其中沒有詳細說明的部分進行了解釋。

時間複雜度為O(n)的演算法

首先,我們需要講輸入的字串 S 進行一下轉換得到 T,轉換的方法就是通過在每兩個字元之間插入一個字串“#”,你馬上就能知道為什麼要這麼做。

例如 輸入字串 S = “abaaba”, 轉換之後得到了 T = “#a#b#a#a#b#a#.

為了找到最長迴文字串,我們的做法是在從T中的第 i 元素開始往字串的兩邊進行擴充套件,例如T[i -d] 到 T[i +d]表示的是以T[i]為中心的迴文字串,你看到這個表示應該就能理解實際上回文字串的長度是 2 * d.

為了找到最長的迴文子串,那麼必不可少的就是找出以每個字元為中心的最長迴文子串,然後比較之後才能找出最長的。在這裡我們使用陣列P來儲存以T中每個字元為中心的最長迴文子串的半徑(既長度的一半)。

接著使用上面的例子,
T = # a # b # a # a # b # a #
P = 0 1 0 3 0 1 6 1 0 3 0 1 0

通過檢視陣列P,找到其中最大的數字,我們可以馬上看出最長的迴文字串是”abaaba”.

不知道你是否注意到了我們插入的字元# ,通過插入這個字元,我們可以一次性的處理當輸入字串為奇數或者偶數的情況,而不需要分情況討論。

不知道你又沒有注意一個很重要的事情,那就是觀察P中元素,他們是關於中心對稱的,不僅僅是這個,在迴文串“aba”中,他們在P中對應的數字(1, 3 , 1})也是對稱的。這僅僅是一個巧合麼?答案可以說是也可以說不是。這種對稱的情況僅僅在某種情況下才成立,但是無論如何我們都獲得了一個重大的發現。通過這個發現,我們可以減少P中元素的重複計算。

現在讓我們來看一個更加複雜的例子,其中包含了一些重疊的迴文子串。
S = “babcbabcbaccba”.

這裡寫圖片描述

上面的圖片是從S轉換之後得到的T,假設你已經完成了P陣列的部分數值的計算,圖中的實線表示當前迴文串 “abcbabcba”的中心(center C ),兩條虛線分別表示以C為中心的迴文串的邊界。你先需要計算的是 i 的值,圖中 i’ 表示的是i 關於 C 對稱的點。那麼問題來了,怎麼計算更有效率的算出P[i] 值。

假設當前 i 的值是 13,我們需要得到 P[13]的值,我們首先看下 i 關於 C對稱的 i’ = 9的值。

這裡寫圖片描述

兩條綠色實線覆蓋的區域分表表示的是 以 i’ 和 i 為中心的最長迴文子串。我們已經知道了 P[i’] = P[9] = 1, 那麼非常明顯,我們可以推斷出 P[i] = P[i’] = 1, 因為迴文子串的對稱性。(這裡稍微解釋一下: 因為我們已經知道 a )

正如你在上一段看到的一樣,P[i] = P[i’] = 1,這個是由迴文子串關於其中心的對稱性所決定的。實際上在C之後的三個元素都遵循了這種對稱性(既 P[12] = P[10] = 0, P[13] = P[9] =1, P[14] = P[8] = 0 ).

這裡寫圖片描述

現在我們到了 i = 15 這個點,i 關於 C對稱的點是 i’ = 7 ,那麼這裡 P[15] = P[7] = 7 還會不會成立呢?

很不幸,這個時候如果認為P[15] 那就錯了,如果我們以T[15] 為中心開始兩邊擴充套件,那麼得到的最長迴文子串是”a#b#c#b#a”, 這比我們根據對稱性所得到的值 7 要小,那麼原因是什麼?

這裡寫圖片描述

在上圖中,綠色實線表示的是以C為中心的最長迴文子串所覆蓋的區域,紅色實線表示的是與綠色區域不匹配的部分,左邊的紅色實線是以i’為中心的最長迴文子串所覆蓋區域超出綠色實線的部分。右邊的紅色實線是以i為中心的最長迴文子串所覆蓋區域超出綠色實線的部分。而綠色的虛線部分則是分別以 i 和 i’ 為中心最長迴文子串與 綠色實線部分的重疊部分。

從圖中我們可以明顯的看出被兩條綠色實線所覆蓋的區域中,C兩邊的子串是完全相同的。同時綠色虛線的那部分也是關於中心對稱的。但是需要注意的是 P[i’] = 7,此時以T[i’]為中心的最長迴文子串超出了以C為中心的最長迴文子串的左邊界(圖中L, 左邊紅色實線),正是因為這個原因,這個時候無法再按照迴文子串的對稱性來處理了。

現在我們知道的是P[i] >= 5 ,為了找出P[i]最終的值,我們只有繼續以P[i]為中心向兩邊擴充套件進行比較,在這個例子中P[21] 不等於 P[1],那麼我們最終推斷是P[i] = 5.

那麼總結下來就是下面的偽碼

if P[ i’ ] ≤ R – i,
then P[ i ] ← P[ i’ ]
else P[ i ] ≥ P[ i’ ]. (Which we have to expand past the right edge (R) to find P[ i ].

這個表示式是否足夠明瞭,如果你能掌握上面的公式,那麼說明你已經知道了這個演算法中最精髓也是最難的部分。

接下來的部分就什麼時候該去移動當前迴文子串的中心C和右邊界R的位置,這個相對簡單:

如果以i為中心的迴文子串的右邊界,向右擴充套件的時候超過了當前的R,那麼就將C更新為 i,並且更新R到新的以i為中心的迴文子串的右邊界。

在每一步中,都存在兩種可能,如果P[i’] <= R - i ,我們將P[i]的值設定為P[i],否則我們會嘗試以P[i]為中心並且從 R 開始去擴充套件迴文子串,在擴充套件的過程中最多執行N步,移動和測試每一箇中心(P[i])一共需要耗費N步,那麼總的時間複雜度是O(n).

下面是Java程式碼的實現

    private static String preProcess (String s){

        int n = s.length();

        if (n == 0 ) return "^$" ;

        String result = "^" ;

        for ( int i = 0 ; i < n; i++) {

            result += "#" + s.charAt(i);

        }

        result += "#$" ;

        return result;

    }


// the original implementation in C/C++

    public static String getLongestPalindrome (String s){

        char [] T = preProcess(s).toCharArray();

        int n = T.length;

        int [] P = new int [n];

        int C = 0 , R = 0 ;

        for ( int i = 1 ; i < n- 1 ; i++) {

            int i_mirror = 2 *C - i; // equals to i' = C - (i-C)

            if ( i < R ) {

                P[i] = Math.min(R-i, P[i_mirror]);

            } else {

                P[i] = 0 ;

            }

// Attempt to expand palindrome centered at i

            while ( T[i+ 1 +P[i]] == T[i- 1 -P[i]] )

                P[i]++;

// if palindrome centered at i expand past R

// adjust center based on expanded palindrome

            if ( i + P[i] > R) {

                C = i;

                R = i + P[i];

            }

        }

        int maxLen = 0 ;

        int centerIndex = 0 ;

        for ( int i = 1 ; i < n- 1 ; i++) {

            if (P[i] > maxLen) {

                maxLen = P[i];

                centerIndex = i;

            }

        }

        int beginIndex = (centerIndex- 1 -maxLen)/ 2 ;

        return s.substring(beginIndex, beginIndex+maxLen);

    }