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


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

題目描述

給你一個字符串 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年我寫了什么?

開源編程筆記


免責聲明!

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



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