1. 程式人生 > 實用技巧 >Re:Manacher:: 老司機教會了女同學「馬拉車演算法」的正確姿勢

Re:Manacher:: 老司機教會了女同學「馬拉車演算法」的正確姿勢

Manacher‘s Algorithm

關於

馬拉車演算法(以下皆簡稱mnc)是用來 查詢一個字串的最長迴文子串的線性方法 ,由一個叫 Manacher 的人在 1975 年發明的,這個方法的牛逼之處在於將時間複雜度提升到了 線性

你可以理解為O(n).

中心思想及流程

使用“中心擴散法”,

“中心擴散法”的思想:迴文串可分為奇數迴文串和偶數迴文串,它們的區別是:奇數迴文串關於它的“中點”滿足“中心對稱”,偶數迴文串關於它“中間的兩個點”滿足“中心對稱”。

但為了避免奇偶套路,可以直接在字串每個字元間隙和頭尾間加上”分隔符

step 1:對原字串預處理

就如前文一樣,加入分隔符,如對於一個字串'abbaab',加入分隔符後為‘#a#b#b#a#a#b#’.

對這一點有如下說明:

1、分隔符是一個字元,種類也只有一個,並且這個字元一定不能是原始字串中出現過的字元,然後滿足這個條件後,這個分隔符如何處置就在你了╮(─▽─)╭;

2、加入了分隔符以後,使得“間隙”有了具體的位置,方便後續的討論,並且新字串中的任意一個迴文子串在原始字串中的一定能找到唯一的一個迴文子串與之對應,當然也要看位置,因此對新字串的迴文子串的研究就能得到原始字串的迴文子串;

3、新字串的迴文子串的長度一定是奇數)自己算

4、新字串的迴文子串一定以分隔符作為兩邊的邊界,因此分隔符起到“哨兵”的作用。

設原字串為s,新字串為s1,則

s.size()*2+1==s1.size();

floor(s1.size()/2)==s.size();

step 2:計算輔助陣列p

輔助陣列 p 記錄了新字串中以每個字元為中心的迴文子串的資訊。及迴文半徑

手動的計算方法仍然是“中心擴散法”,此時記錄以當前字元為中心,向左右兩邊同時擴散,記錄能夠擴散的最大步數。

以'abab'為例,列出如下表格

index 0 1 2 3 4 5 6 7 8
char # a # b # a # b #
p 1 2 1 4 1 2 1 2

1

發現 p 最大是4,則原字串最長迴文串長度為3,迴文串輸出可進一步實現,在此不過多贅述。

為什麼p_max就是最長迴文串長度呢?

將回文中心(包括迴文中心)一側的‘#’與緊挨的字元兩兩配對,迴文中心可以是分隔符。

不難看出 對數*2-1=迴文串長度。不論奇偶都一樣。

那麼下面我們就來看如何求p陣列,需要新增兩個輔助變數 mx 和 id,其中 id 為能延伸到最右端的位置的那個迴文子串的中心點位置,mx 是迴文串能延伸到的最右端的位置,需要注意的是,這個 mx 位置的字元不屬於迴文串,所以才能用 mx-i 來更新 p[i] 的長度而不用加1,由 mx 的更新方式 mx = i + p[i] 也能看出來 mx 是不在迴文串範圍內的,這個演算法的最核心的一行如下:

p[i] = mx > i ? min(p[2 * id - i], mx - i) : 1;

可以這麼說,這行要是理解了,那麼馬拉車演算法基本上就沒啥問題了,那麼這一行程式碼拆開來看就是

如果 mx > i, 則 p[i] = min( p[2 * id - i] , mx - i )

否則,p[i] = 1

當 mx - i > P[j] 的時候,以 S[j] 為中心的迴文子串包含在以 S[id] 為中心的迴文子串中,由於 i 和 j 對稱,以 S[i] 為中心的迴文子串必然包含在以 S[id] 為中心的迴文子串中,所以必有 P[i] = P[j],其中 j = 2*id - i,因為 j 到 id 之間到距離等於 id 到 i 之間到距離,為 i - id,所以 j = id - (i - id) = 2*id - i

當 P[j] >= mx - i 的時候,以 S[j] 為中心的迴文子串不一定完全包含於以 S[id] 為中心的迴文子串中,但是基於對稱性可知,下圖中兩個綠框所包圍的部分是相同的,也就是說以 S[i] 為中心的迴文子串,其向右至少會擴張到 mx 的位置,也就是說 P[i] = mx - i。至於 mx 之後的部分是否對稱,就只能老老實實去匹配了,這就是後面緊跟到 while 迴圈的作用。

對於 mx <= i 的情況,無法對 P[i] 做更多的假設,只能 P[i] = 1,然後再去匹配了。

程式碼如下

#include <vector>
#include <iostream>
#include <string>

using namespace std;

string Manacher(string s) {
    // Insert '#'
    string t = "$#";
    for (int i = 0; i < s.size(); ++i) {
        t += s[i];
        t += "#";
    }
    // Process t
    vector<int> p(t.size(), 0);
    int mx = 0, id = 0, resLen = 0, resCenter = 0;
    for (int i = 1; i < t.size(); ++i) {
        p[i] = mx > i ? min(p[2 * id - i], mx - i) : 1;
        while (t[i + p[i]] == t[i - p[i]]) ++p[i];
        if (mx < i + p[i]) {
            mx = i + p[i];
            id = i;
        }
        if (resLen < p[i]) {
            resLen = p[i];
            resCenter = i;
        }
    }
    return s.substr((resCenter - resLen) / 2, resLen - 1);
}

int main() {
    string s1 = "12212";
    cout << Manacher(s1) << endl;
    string s2 = "122122";
    cout << Manacher(s2) << endl;
    string s = "waabwswfd";
    cout << Manacher(s) << endl;
}
點選展開