馬拉車算法(Manacher's Algorithm)


這是悅樂書的第343次更新,第367篇原創

Manacher's Algorithm,中文名叫馬拉車算法,是一位名叫Manacher的人在1975年提出的一種算法,解決的問題是求最長回文子串,神奇之處在於將算法的時間復雜度精進到了O(N),下面我們來詳細介紹下這個算法的思路。

01 算法由來

在求解最長回文子串的問題時,一般的思路是以當前字符為中心,向其左右兩邊擴展尋找回文,但是這種解法的時間復雜度是O(N^2),那么能不能將時間復雜度再降低一點?做到線性?馬拉車算法就完美地解決了這個問題。

02 預處理

回文字符串以其長度來分,可以分為奇回文(其長度為奇數)、偶回文(其長度為偶數),一般情況下需要分兩種情況來尋找回文,馬拉車算法為了簡化這一步,對原始字符串進行了處理,在每一個字符的左右兩邊都加上特殊字符(肯定不存在於原字符串中的字符),讓字符串變成一個奇回文。例如:

原字符串:abba,長度為4
預處理后:#a#b#b#a#,長度為9

原字符串:aba,長度為3
預處理后:#a#b#a#,長度為7

03 計算最長回文子串長度

以字符串"cabbaf"為例,將預處理后的新字符串"#c#a#b#b#a#f#"變成一個字符數組arr,定義一個輔助數組int[] pp的長度與arr等長,p[i]表示以arr[i]字符為中心的最長回文半徑,p[i]=1表示只有arr[i]字符本身是回文子串。

i       0 1 2 3 4 5 6 7 8 9 10 11 12
arr[i]  # c # a # b # b # a #  f  #
p[i]    1 2 1 2 1 2 5 2 1 2 1  2  1

我們來比對分下一下最長回文半徑和原字符串之間的關系。在上面例子中,最長回文子串是"#a#b#b#a#",它以arr[6]為中心,半徑是5,其代表的原始字符串是"abba",而"abba"的長度為4,可以通過5減去1得到,是字符串"cabbaf"中的最長回文子串,那么我們是不是可以得出最長回文半徑和最長回文子串長度之間的關系?

讓我們再多看幾個例子,如"aba",轉換后是"#a#b#a#",以字符'b'為中心的回文,半徑是4,減1得到3,3是原字符串的最長回文子串長度。

再例如"effe",轉換后是"#e#f#f#e#",以最中間的'#'為中心的回文,半徑是5,減1得到4,4是原字符串的最長回文子串長度。

因此,最后我們得到最長回文半徑和最長回文子串長度之間的關系:int maxLength = p[i]-1maxLength表示最長回文子串長度。

04 計算最長回文子串起始索引

知道了最長回文子串的長度,我們還需要知道它的起始索引值,這樣才能截取出完整的最長回文子串。

繼續以第三步中的字符串"cabbaf"為例,p[6]=5,是最長半徑,用6(i)減去最長半徑5(p[i])得到1,而1恰好是最長回文子串"abba"的起始索引。

我們再來看一個奇回文的例子。例如"aba",轉換后是"#a#b#a#",p[3]=4,最長半徑是4,i為3,用i減去4得到-1,數組下標越界了。

在偶回文的情況下,可以滿足i減最長半徑,而奇回文卻會下標越界,我們需要在轉換后的字符串前面再加一個字符,解決下標越界的問題,不能是'#',那就加個'$'字符吧,但是加過一個字符后,字符串的長度不是奇數了,只能在尾部再加一個不會重復出現的字符,比如'@',這樣字符串的長度依舊是奇數了,滿足前面第三部分的條件。

加多一個字符后,奇回文可以正常做減法了,偶回文呢?

i       0 1 2 3 4 5 6 7 8 9 10 11 12 13
arr[i]  $ # c # a # b # b # a  #  f  #
p[i]      1 2 1 2 1 2 5 2 1 2  1  2  1

在補上字符'$'后,p[7]=5,用i減去最長半徑,7-5=2,而理想的結果應該是1,那就再除以2吧,這樣就能得到1了。而奇回文"aba"在用i減去最長半徑后得到的是0,除以2后還是0,可以完美解決下標越界的問題。

結論:最長回文子串的起始索引int index = (i - p[i])/2

05 計算p數組

在第三步和第四步中我們都用到了一個關鍵對象p數組,存放的是最長回文子串半徑,那么它是怎么來的呢?
還是以上面的例子配合着看,

i       0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
arr[i]  $ # c # a # b # b # a  #  f  #  @ 
p[i]      1 2 1 2 1 2 5 2 1 2  1  2  1

設置兩個變量id和mx,id是所有回文子串中,能延伸到最右端位置的那個回文子串的中心點位置,mx是該回文串能延伸到的最右端的位置。

當i等於7時,id等於7,p[id] = 5,在以位置7為中心的回文子串中,該回文子串的右邊界是位置12。

當i等於12時,id等於12,p[id] = 2,在以位置12為中心的回文子串中,該回文子串的右邊界是位置14。

由此我們可以得出回文子串右邊界和其半徑之間的關系:mx = p[id]+id

因為回文字符串是中心對稱的,知道中心點位置id,如果一個位置的回文子串以i為中心,並且包含在以id為中心的回文子串中,即mx > i,那么肯定會存在另外一個以j為中心回文子串,和以i為中心的回文子串相等且對稱,即p[j] = p[i],而i和j是以id為中心對稱,即i+j=2*id,如果知道了i的值,那么j = 2*id - i

但是我們需要考慮另外一種情況,如果存在一個以i為中心的回文子串,依舊有mx > i,但是以i為中心的回文子串右邊界超過了mx,在i到mx的這段回文子串中,與另一端對稱的以j為中心的回文子串還是相等的,此時p[i] = mx - ip[j] = [pi],至於右邊界mx之外的子串,即以i為中心的回文子串超出的部分是否還是滿足上述條件就需要遍歷比較字符了。

因此,在mx > i的情況下,p[i] = Math.min(p[2*id - i], mx - i)
另外如果i大於mx了,也即是邊界mx后面的子串,依舊需要去比較字符計算。

public static String Manacher(String s) {
    if (s.length() < 2) {
        return s;
    }
    // 第一步:預處理,將原字符串轉換為新字符串
    String t = "$";
    for (int i=0; i<s.length(); i++) {
        t += "#" + s.charAt(i);
    }
    // 尾部再加上字符@,變為奇數長度字符串
    t += "#@";
    // 第二步:計算數組p、起始索引、最長回文半徑
    int n = t.length();
    // p數組
    int[] p = new int[n];
    int id = 0, mx = 0;
    // 最長回文子串的長度
    int maxLength = -1;
    // 最長回文子串的中心位置索引
    int index = 0;
    for (int j=1; j<n-1; j++) {
        // 參看前文第五部分
        p[j] = mx > j ? Math.min(p[2*id-j], mx-j) : 1;
        // 向左右兩邊延伸,擴展右邊界
        while (t.charAt(j+p[j]) == t.charAt(j-p[j])) {
            p[j]++;
        }
        // 如果回文子串的右邊界超過了mx,則需要更新mx和id的值
        if (mx < p[j] + j) {
            mx = p[j] + j;
            id = j;
        }
        // 如果回文子串的長度大於maxLength,則更新maxLength和index的值
        if (maxLength < p[j] - 1) {
            // 參看前文第三部分
            maxLength = p[j] - 1;
            index = j;
        }
    }
    // 第三步:截取字符串,輸出結果
    // 起始索引的計算參看前文第四部分
    int start = (index-maxLength)/2;
    return s.substring(start, start + maxLength);
}

06 小結

馬拉車算法將求解最長回文子串的時間復雜度降低到了O(N),雖然也犧牲了部分空間,其空間復雜度為O(N),但是其算法的巧妙之處還是值得學習和借鑒的。

算法專題目前已連續日更超過六個月,算法題文章211+篇,公眾號對話框回復【數據結構與算法】、【算法】、【數據結構】中的任一關鍵詞,獲取系列文章合集。

以上就是全部內容,如果大家有什么好的解法思路、建議或者其他問題,可以下方留言交流,好看、留言、轉發就是對我最大的回報和支持!


免責聲明!

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



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