1. 程式人生 > 實用技巧 >Manacher演算法(馬拉車)

Manacher演算法(馬拉車)

轉載於:https://blog.csdn.net/qq_43152052/article/details/100784978

馬拉車的解決的問題:
給定字串S,求S中的最長迴文子串
解釋:迴文串就是正讀反讀都一樣的字串,比如奇迴文串(bab)、偶迴文串(noon)。


馬拉車演算法步驟:

  • 1)由於迴文串存在奇迴文串和偶迴文串,馬拉車演算法第一步就是:預處理字串,做法是在每一個字元的左右都加上一個特殊字元(前提是這個字元在字串沒有出現過),使這兩種迴文串都變成偶迴文串。比如加上’#’,這樣奇迴文串(bab)還是會變成奇迴文串(#b#a#b#),偶迴文串(noon)會變成奇迴文串(#n#o#o#n#)。

  • 2)然後我們定義一個輔助陣列p用來表示經過與處理過的新字串t,其中p[i]表示以字元t[i]為半徑的迴文子串長度,例如:
index 0 1 2 3 4 5 6 7 8 9 10 11 12 13
char $ # 1 # 2 # 2 # 1 # 2 # 2 #
R 1 2 1 2 5 2 1 6 1 2 3 2 1

  • 3)找規律

規律①:最大半徑減1等於最長迴文串的長度
看上面那個例子,以中間的 ‘1’ 為中心的迴文子串 “#2#2#1#2#2#” 的半徑是6,而未新增#號的迴文子串為 “22122”,長度是5,為半徑減1。這是個普遍的規律麼?我們再看看之前的那個 “#b#o#b#”,我們很容易看出來以中間的 ‘o’ 為中心的迴文串的半徑是4,而 "bob"的長度是3,符合規律。再來看偶數個的情況 “noon”,新增#號後的迴文串為 “#n#o#o#n#”,以最中間的 ‘#’ 為中心的迴文串的半徑是5,而 “noon” 的長度是4,完美符合規律。所以我們只要找到了最大的半徑,就知道最長的迴文子串的字元個數了。只知道長度無法定位子串,我們還需要知道子串的起始位置。


規律②:最長迴文字元的起始位置是中間位置減去半徑在除以2
我們還是先來看中間的 ‘1’ 在字串 “#1#2#2#1#2#2#” 中的位置是7,而半徑是6,貌似 7-6=1,剛好就是迴文子串 “22122” 在原串 “122122” 中的起始位置1。那麼我們再來驗證下 “bob”,“o” 在 “#b#o#b#” 中的位置是3,但是半徑是4,這一減成負的了,肯定不對。所以我們應該至少把中心位置向後移動一位,才能為0啊,那麼我們就需要在前面增加一個字元,這個字元不能是#號,也不能是s中可能出現的字元,所以我們暫且就用美元號吧,畢竟是博主最愛的東西嘛。這樣都不相同的話就不會改變p值了,那麼末尾要不要對應的也新增呢,其實不用的,不用加的原因是字串的結尾標識為 ‘\0’,等於預設加過了。那此時 “o” 在 "$#b#o#b#"

中的位置是4,半徑是4,一減就是0了,貌似沒啥問題。我們再來驗證一下那個數字串,中間的 ‘1’ 在字串 "$#1#2#2#1#2#2#" 中的位置是8,而半徑是6,這一減就是2了,而我們需要的是1,所以我們要除以2。之前的 “bob” 因為相減已經是0了,除以2還是0,沒有問題。再來驗證一下 “noon”,中間的 ‘#’ 在字串 "$#n#o#o#n#" 中的位置是5,半徑也是5,相減併除以2還是0,完美。所以,最長迴文字元的起始位置是中間位置減去半徑在除以2。


  • 4)p陣列求解

關於p陣列的求解,需要建立兩個輔助變數mx和id,id表示迴文串的中心位置下標,mx表示迴文串右邊最大半徑下標,所以mx = id + p[id]

接下來就是求p[i],當然這也是演算法中最重要的部分:

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

注: 2 * id - i表示 i 關於 id 對稱的座標點j。因為 j 到 id 之間到距離等於 id 到 i 之間到距離(id - j = i - id),所以j = 2 * id - i

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

①: 在mx > i的前提下,若p[j] < mx - i,表示以 S[j] 為中心的迴文子串包含在以 S[id] 為中心的迴文子串中,由於 i 和 j 對稱,以 S[i] 為中心的迴文子串必然包含在以 S[id] 為中心的迴文子串中,所以必有 P[i] = P[j]。

②: 在mx > i的前提下,若 p[j] >= mx - i表示以 S[j] 為中心的迴文子串不一定完全包含於以 S[id] 為中心的迴文子串中,也就是說p[j]表示的迴文串半徑超過mx對稱點的座標了,那麼此時不能利用對稱性了,但我們一定可以擴充套件到 mx 的,至於mx之後的部分我們任然需要匹配了。

③: 在mx <= i的前提下,p[i] = 1,因為此時我們需要通過中心擴充套件法一步一步擴充套件半徑就行了。


關於p[i] = mx > i ? min(p[2 \* id - i], mx - i) : 1;的更好理解,大家可看:一文讓你徹底明白馬拉車演算法此文中的三種特殊情況!


  • 5)馬拉車演算法程式碼如下:
#include <vector>
#include <iostream>
#include <string>

using namespace std;

string Mannacher(string s){
    //插入"#"
    string t="$#";
    for(int i=0;i<s.size();++i)
    {
        t+=s[i];
        t+="#";
    }
    
    vector<int> p(t.size(),0);
    //mx表示某個迴文串延伸在最右端半徑的下標,id表示這個迴文子串最中間位置下標
    //resLen表示對應在s中的最大子迴文串的半徑,resCenter表示最大子迴文串的中間位置
    int mx=0,id=0,resLen=0,resCenter=0;

     //建立p陣列
    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];

        //半徑下標i+p[i]超過邊界mx,需要更新
        if(mx<i+p[i]){
            mx=i+p[i];
            id=i;
        }

        //更新最大回文子串的資訊,半徑及中間位置
        if(resLen<p[i]){
            resLen=p[i];
            resCenter=i;
        }
    }

    //最長迴文子串長度為半徑-1,起始位置為中間位置減去半徑再除以2
    return s.substr((resCenter-resLen)/2,resLen-1);
}

int main(){
    cout<<Mannacher("12212")<<endl;
    cout<<Mannacher("122122")<<endl;
    cout<<Mannacher("noon")<<endl;
    return 0;
}