什么是馬拉車算法(Manacher's Algorithm)?


提出問題

最長回文子串問題:給定一個字符串,求它的最長回文子串長度。

如果一個字符串正着讀和反着讀是一樣的,那它就是回文串。如a、aa、aba、abba等。

暴力解法

簡單粗暴:找到字符串的所有子串,遍歷每一個子串以驗證它們是否為回文串。一個子串由子串的起點和終點確定,對於一個長度為n的字符串,共有n^2個子串。這些子串的平均長度大約是n/2,因此這個解法的時間復雜度是 \(O(n^3)\)。明顯不可取。

方法改進

回文子串是連續的,而且是對稱的。長度為奇數回文串以最中間字符的位置為對稱軸左右對稱,而長度為偶數的回文串的對稱軸在中間兩個字符之間的空隙。可否利用這種對稱性來提高算法效率呢?答案是肯定的。我們知道整個字符串中的所有字符,以及字符間的空隙,都可能是某個回文子串的對稱軸位置。可以遍歷這些位置,在每個位置上同時向左和向右擴展,直到左右兩邊的字符不同,或者達到邊界。對於一個長度為n的字符串,這樣的位置一共有 n+n-1=2n-1 個,在每個位置上平均大約要進行 n/4 次字符比較,於是此算法的時間復雜度是 \(O(n^2)\)

另外一種改進方法是利用動態規划,DP[i][j]定義成子串[i, j]是否是回文串。外循環 i從 n−1 往 0 遍歷,內循環 j 從 i 往 n−1 遍歷,若s[i]==s[j]:

  • 若i==j,則dp[i][j]=true;
  • 若i和j是相鄰的,則dp[i][j]=true;
  • 若i和j中間只有一個字符,則dp[i][j]=true;
  • 否則,檢查dp[i+1][j-1]是否為true,若為true,那么dp[i][j]就是true。

前三條可以合並,即 j−i≤2。求得dp[i][j]真值后,也可快速解決問題。時間復雜度:\(O(n^2)\)

Manacher's Algorithm

對於 \(O(n^2)\) 的復雜度,或許還不滿足,是否可以再優化一些呢?

先分析改進方法中的缺陷,利用回文中心需要分奇偶兩種情況討論,兩種改進都會重復訪問子串,降低效率。

Manacher's Algorithm正是針對這兩個問題進行進一步的改進,將時間復雜度降到了神奇的 \(O(n)\)

問題一:回文長度奇偶性問題

為了不區分奇偶兩種情況,對字符串作預處理,在所有字符之間(包括首尾)插入相同字符如'#',處理之后所有的子串都是奇數長度的。如aba→#a#b#a#。

插入的是同樣的符號,且符號不存在於原串,因此子串的回文性不受影響,原來是回文的串,插完之后還是回文的,原來不是回文的,依然不會是回文。

問題二:重復訪問問題

定義回文半徑:回文串中最左或最右位置的字符與其對稱軸的距離。算法中定義回文半徑數組 \(RL\)\(RL[i]\) 表示以第i個字符為對稱軸的回文串的回文半徑。

定義 \(MaxRight\),表示當前訪問到的所有回文子串,所能觸及的最右一個字符的位置。另外還要記錄下 \(MaxRight\) 對應的回文串的對稱軸所在的位置,記為 \(pos\)

核心代碼:RL[i] = i < MaxRight ? min(RL[2*pos-i], MaxRight-i) : 1;

理解了這行代碼,可以說就理解了這個算法。我們來分情況討論:

(1)i < MaxRight時,可以再分兩種情況:

①MaxRight - i > RL[2*pos-i],如下圖,以S[j]為中心的回文子串包含在以S[id]為中心的回文子串中,由於 i 和 j 對稱,以S[i]為中心的回文子串必然包含在以S[id]為中心的回文子串中,所以必有 P[i] = P[j]。

②MaxRight - i < RL[2*pos-i],如下圖,以S[j]為中心的回文子串不一定完全包含於以S[id]為中心的回文子串中,但是基於對稱性可知,下圖中兩個綠框所包圍的部分是相同的,也就是說以S[i]為中心的回文子串,其向右至少會擴張到mx的位置,也就是說 P[i] >= mx - i。

(2)i > MaxRight時,無法對 P[i]做更多的假設,只能P[i] = 1,然后再去慢慢匹配了。

代碼實現

返回最長的回文子串。代碼中resLen為處理后字符串的最大回文半徑,對應到原來的字符串中時,只需-1即是整個回文串的長度。

string Manacher(string s) {
    //預處理
    string t = "#";
    for (int i = 0; i < s.size(); ++i) {
        t += s[i];
        t += "#";
    }

    vector<int> RL(t.size(), 0);
    int MaxRight = 0, pos = 0;
    int resLen = 0, resCenter = 0;
    for (int i = 0; i < t.size(); ++i) {
        RL[i] = MaxRight > i ? min(RL[2 * pos - i], MaxRight - i) : 1;

        while (i-RL[i] >=0 && i+RL[i] < t.size() && t[i + RL[i]] == t[i - RL[i]])//擴展,注意邊界
            ++RL[i];
        //更新最右端及其中心
        if (MaxRight < i + RL[i] -1) {
            MaxRight = i + RL[i] -1;
            pos = i;
        }
        if (resLen < RL[i]) {
            resLen = RL[i];
            resCenter = i;
        }
    }
    return s.substr((resCenter - resLen + 1) / 2 , resLen - 1);
}

時間復雜度:\(O(n)\)。在參考鏈接中有比較詳細的證明過程。

參考鏈接:https://segmentfault.com/a/1190000003914228

參考鏈接:http://www.cnblogs.com/grandyang/p/4475985.html


免責聲明!

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



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