1. 程式人生 > 其它 >馬拉車演算法,其實並不難!!!

馬拉車演算法,其實並不難!!!

要說馬拉車演算法,必須說說這道題,查詢最長迴文子串,馬拉車演算法是其中一種解法,狠人話不多,直接往下看:

題目描述

給你一個字串 s,找到 s 中最長的迴文子串。

例子

示例 1:
輸入:s = "babad"
輸出:"bab"
解釋:"aba" 同樣是符合題意的答案。

示例 2:
輸入:s = "cbbd"
輸出:"bb"

示例 3:
輸入:s = "a"
輸出:"a"

示例 4:
輸入:s = "ac"
輸出:"a"

馬拉車演算法

這是一個奇妙的演算法,是1957年一個叫Manacher的人發明的,所以叫Manacher‘s Algorithm,主要是用來查詢一個字串的最長迴文子串,這個演算法最大的貢獻是將時間複雜度提升到線性,前面我們說的動態規劃的時間複雜度為 O(n2

)。

前面說的中心拓展法,中心可能是字元也可能是字元的間隙,這樣如果有 n 個字元,就有 n+n+1 箇中心:

為了解決上面說的中心可能是間隙的問題,我們往每個字元間隙插入”#“,為了讓拓展結束邊界更加清晰,左邊的邊界插入”^“,右邊的邊界插入 "$":

S 表示插入"#","^","$"等符號之後的字串,我們用一個數組P表示S中每一個字元能夠往兩邊拓展的長度:

比如 P[8] = 3,表示可以往兩邊分別拓展3個字元,也就是迴文串的長度為 3,去掉 # 之後的字串為aca

P[11]= 4,表示可以往兩邊分別拓展4個字元,也就是迴文串的長度為 4,去掉 # 之後的字串為caac

假設我們已經得知陣列P,那麼我們怎麼得到迴文串?

P 的下標 index ,減去 P[i](也就是迴文串的長度),可以得到迴文串開頭字元在拓展後的字串 S 中的下標,除以2,就可以得到在原字串中的下標了。

那麼現在的問題是:如何求解陣列P[i]

其實,馬拉車演算法的關鍵是:它充分利用了迴文串的對稱性,用已有的結果來幫助計算後續的結果。

假設已經計算出字元索引位置 P 的最大回文串,左邊界是PL,右邊界是PR

那麼當我們求因為一個位置 i 的時候,i 小於等於 PR,其實我們可以找到 i 關於 P 的對稱點 j:

那麼假設 j 為中心的最長迴文串長度為 len,並且在 PL 到 P 的範圍內,則 i 為中心的最長迴文串也是如此:

以 i 為中心的最長迴文子串長度等於以 j 為中心的最長迴文子串的長度

但是這裡有兩個問題:

  • 前一個迴文字串P,是哪一個?
  • 有哪些特殊情況?特殊情況怎麼處理?

(1) 前一個迴文字串 P,是指的前面計算出來的右邊界最靠右的迴文串,因為這樣它最可能覆蓋我們現在要計算的 i 為中心的索引,可以儘量重用之前的結果的對稱性。

也正因為如此,我們在計算的時候,需要不斷儲存更新 P 的中心和右邊界,用於每一次計算。

(2) 特殊情況其實就是當前 i 的最長迴文字串計算不能再利用 P 點的對稱,例如:

  1. i 的迴文串的右邊界超出了 P 的右邊界 PR:

這種情況的解決方案是:超過的部分,需要按照中心拓展法來一一拓展。

  1. i 不在 以 P 為中心的迴文串裡面,只能按照中心拓展法來處理。

具體的程式碼實現如下:

    // 構造字串
    public String preProcess(String s) {
        int n = s.length();
        if (n == 0) {
            return "^$";
        }
        String ret = "^";
        for (int i = 0; i < n; i++)
            ret = ret + "#" + s.charAt(i);
        ret = ret + "#$";
        return ret;
    }

    // 馬拉車演算法
    public String longestPalindrome(String str) {
        String S = preProcess(str);
        int n = S.length();
        // 儲存迴文串的長度
        int[] P = new int[n];
        // 儲存邊界最右的迴文中心以及右邊界
        int center = 0, right = 0;
        // 從第 1 個字元開始
        for (int i = 1; i < n - 1; i++) {
            // 找出i關於前面中心的對稱
            int mirror = 2 * center - i;
            if (right > i) {
                // i 在右邊界的範圍內,看看i的對稱點的迴文串長度,以及i到右邊界的長度,取兩個較小的那個
                // 不能溢位之前的邊界,否則就得中心拓展
                P[i] = Math.min(right - i, P[mirror]);
            } else {
                // 超過範圍了,中心拓展
                P[i] = 0;
            }

            // 中心拓展
            while (S.charAt(i + 1 + P[i]) == S.charAt(i - 1 - P[i])) {
                P[i]++;
            }

            // 看看新的索引是不是比之前儲存的最右邊界的迴文串還要靠右
            if (i + P[i] > right) {
                // 更新中心
                center = i;
                // 更新右邊界
                right = 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 start = (centerIndex - maxLen) / 2;
        return str.substring(start, start + maxLen);
    }

至於演算法的複雜度,空間複雜度藉助了大小為n的陣列,為O(n),而時間複雜度,看似是用了兩層迴圈,實則不是 O(n2),而是 O(n),因為絕大多數索引位置會直接利用前面的結果以及對稱性獲得結果,常數次就可以得到結果,而那些需要中心拓展的,是因為超出前面結果覆蓋的範圍,才需要拓展,拓展所得的結果,有利於下一個索引位置的計算,因此拓展實際上較少。

【作者簡介】
秦懷,公眾號【秦懷雜貨店】作者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。個人寫作方向:Java原始碼解析JDBCMybatisSpringredis分散式劍指OfferLeetCode等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花裡胡哨,大多寫系列文章,不能保證我寫的都完全正確,但是我保證所寫的均經過實踐或者查詢資料。遺漏或者錯誤之處,還望指正。

劍指Offer全部題解PDF

2020年我寫了什麼?

開源程式設計筆記