Manacher (馬拉車) 算法:解決最長迴文子串的利器

最長迴文子串

迴文串就是原串和反轉字符串相同的字符串。好比 abaacca。前一個是奇數長度的迴文串,後一個是偶數長度的迴文串。php

最長迴文子串就是一個字符串的全部子串中,是迴文串且長度最長的子串。html

Brute Force 作法

枚舉全部子串,判斷是不是迴文串,而後尋找最大長度。尋找全部子串要兩重循環,判斷是不是迴文要一重循環,整體時間複雜度 \(O(n^3)\)ios

稍微優化一下,能夠枚舉對稱中心,而後向兩邊擴展,直到遇到兩個不一樣的字符,枚舉下一個對稱中心,尋找其中的最大長度,時間複雜度 \(O(n^2)\)c++

還可使用 DP 解決,求原串與反轉字符串的最長公共子序列 (LCS),時間複雜度 \(O(n^2)\)算法

Manacher 算法

接下來就是重點了,Manacher 算法,在1975年由一個叫 Manacher 的人發明的。可以在 O(n) 的時間求得最長迴文子串。c#

前面提到,迴文串有奇數長度的和偶數長度的,分類討論有些複雜,能夠參考這裏。爲了不分類討論,可使用一個技巧:在字符串首尾以及每兩個字符之間插入一個 '#'。好比 abaacca,轉換後就是 #a#b#a#a#c#c#a#。那麼無論是奇迴文 aba 仍是偶迴文 acca,轉換後都是奇迴文 (#a#b#a##a#c#c#a#)。數組

string init(string s) {
    string res;
    res += '@';  // 在開頭加入哨兵防止越界
    for(int i = 0; i < s.size(); ++i) {
        res += '#';
        res += s[i];
    }
    res += '#';
    res += '$';  // 結尾一樣加入哨兵防止越界
    return res;
}

\(Manacher\) 算法的思想來自於上述枚舉對稱中心的思想。該算法須要維護一個 \(len\) 數組,\(len[i]\) 表明 \(i\) 爲中心的最長迴文子串的長度。優化

\(s\) 爲原字符串,\(mx\) 爲以前計算的迴文串中右端點的最大值,這個迴文串的中心位置爲 \(id\),也就是 \(mx=id+len[id]\)spa

每次計算的時候,id 的右邊和左邊是對稱的,所以計算右邊的時候不須要用從對稱中心向兩邊擴展的思想,而是隻用一行代碼解決:len[i] = min(mx - i, len[2 * id - i]);,這也是 Manacher 中最關鍵的一行代碼。3d

以下圖所示,\(id\) 右邊到 \(mx\) 之間的子串與 \(id\) 左邊是對稱的,因此右邊的 \(len[i]\) 最大長度爲左邊與之對稱的 \(len[2×id−i]\),因爲右邊的迴文串不能超過 \(mx\) (緣由見第 2 張圖),因此 len[i] = min(mx - i, len[2 * id - i]);

\(id\) 右邊的迴文串長度不能超過 \(mx−i\) 的緣由是,若是 \(len[2∗id−i]\) 更長,以下圖的黃色部分,那麼右邊的黃色部分與左邊的黃色部分相同,那麼黑色部分應該能夠更長,產生矛盾。

理解了上面的內容基本上就理解了 \(Manacher\) 算法了。

代碼實現以下:

若是以爲這個代碼解釋的不太清楚的話能夠直接看下面的模板題或者看下劉毅學長的代碼

int Manacher(string s) {
    memset(len, 0, sizeof(len));
    int mx = 0, id = 0;
    int ans = 0;
    for(int i = 1; i < s.size() - 1; ++i) {
        if(mx > i) {
            len[i] = min(mx - i, len[2 * id - i]);  // 上面提到的最關鍵的一行代碼
        } else {
            len[i] = 1;  // 若是 i 超過右邊界要從頭計算
        }
        while(s[i - len[i]] == s[i + len[i]]) {  // 從頭計算的方法,就是上面提到的從中心向兩邊擴展
            ++len[i];
        }
        // 更新 mx 和 id
        if(i + len[i] > mx) {
            mx = i + len[i]; 
            id = i;
        }
        ans = max(ans, len[i]);
    }
    return ans - 1; // len[i] 中的最大值-1 即爲原串的最長迴文子串長度 
}

模板題:HDU 3068 最長迴文

題目連接:HDU 3068 最長迴文

// Author : RioTian
// Time : 20/11/11
#include <bits/stdc++.h>
#define ms(a, b) memset(a, b, sizeof(a))
using namespace std;
const int maxn = 220000;
int len[maxn];
string init(string s) {
    string res;
    res += '@';
    for (int i = 0; i < s.size(); ++i) {
        res += '#';
        res += s[i];
    }
    res += '#';
    res += '$';
    return res;
}
int Manacher(string s) {
    ms(len, 0);
    int mx = 0, id = 0;
    int ans = 0;
    for (int i = 1; i < s.size() - 1; ++i) {
        if (mx > i)
            len[i] = min(mx - i, len[2 * id - i]);
        else
            len[i] = 1;

        while (s[i - len[i]] == s[i + len[i]]) ++len[i];

        if (i + len[i] > mx) {
            mx = i + len[i];
            id = i;
        }
        ans = max(ans, len[i]);
    }
    return ans - 1;
}

int main() {
    // freopen("in.txt", "r", stdin);
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    string s;
    while (cin >> s) {
        string tmp = init(s);
        cout << Manacher(tmp) << endl;
    }
}

參考

OI wiki

劉毅學長關於馬拉車算法的講解

相關文章
相關標籤/搜索