1. 程式人生 > 實用技巧 >Manacher 演算法學習筆記

Manacher 演算法學習筆記

原更新日期:2018-11-01 16:52:35

\(O(n)\)迴文串

Manacher 是什麼

Manacher 是一種可以在\(O(n)\)的時間複雜度內求出一個字串的最長迴文子串的演算法。

Manacher,中文一般念做「馬拉車」。

Manacher Algorithm 的思想

首先我們來看一道題洛谷P3805【模板】manacher演算法

考慮一下暴力做法,就是列舉字串的邊界並進行驗證,時間複雜度\(O(n^3)\)

考慮一下優化,我們可以列舉所有“迴文子串”的對稱軸(儘管它現在不一定是迴文子串)並向兩邊進行擴充套件,用一個數組external[i]記錄第i個字元可向外擴充套件的數量,顯然陣列中最大值的二倍就是答案,時間複雜度均攤\(O(n^2)\)

但這還不夠快……畢竟\(\text{|s|} \leq 11000000\)

於是我們考慮在優化的思想基礎上進行再次優化。


在此之前,我們首先要解決一個棘手的問題——字串的長度。
一個字串子串的對稱軸是在字母中間還是在字母上,是由子串長度為偶數還是奇數決定的。於是,為了統一對於奇數長度字串和偶數長度字串的做法,我們需要對字串進行修改。(程式碼見「程式碼實現」Pre()部分)

就比如說
 - - - - - -
|%|%|%|w|y|h|
我們要用一些無關緊要的字元填一下
 - - - - - - - - - - -
|%|!|%|!|%|!|w|!|y|!|h|
這樣更好處理

修改完了之後,就是真正的Manacher()

過程了
首先,我們要用一個變數maxRight記錄「當前的 最靠右的 迴文子串的 右端點」,和一個變數mid記錄「當前的 最靠右的 迴文子串的 對稱軸所在的 字元的 下標」,注意這裡的mid是可以不賦初值的

我們迴圈列舉經過處理的字串的每一個字元。對於每一個字元的下標i,如果i < maxRight,那麼我們就可以獲取external[i]的部分資訊(external[i]的意義和上文相同),否則就只能將external[i]設為1

接著就是和暴力一樣的擴充套件了,我這裡選擇用for語句實現(

最後更新一下maxRightmid即可

最終答案就是external[]的最大值——而不是2倍,因為這是我們擴充套件過的字串,最終答案還要\(\times \frac{1}{2}\)

Manacher Algorithm 的程式碼實現

同樣也是「manacher模版」的程式碼實現。

/* -- Basic Headers -- */
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cctype>
#include <algorithm>

/* -- STL Iterators -- */
#include <vector>
#include <string>
#include <stack>
#include <queue>

/* -- External Headers -- */

/* -- Defined Functions -- */
#define For(a,x,y) for (int a = x; a <= y; ++a)
#define Forw(a,x,y) for (int a = x; a < y; ++a)
#define Bak(a,y,x) for (int a = y; a >= x; --a)

/* -- Defined Words -- */

using namespace std;

namespace FastIO {
    
    inline int getint() {
        int s = 0, x = 1;
        char ch = getchar();
        while (!isdigit(ch)) {
            if (ch == '-') x = -1;
            ch = getchar();
        }
        while (isdigit(ch)) {
            s = s * 10 + ch - '0';
            ch = getchar();
        }
        return s * x;
    }
    inline void __basic_putint(int x) {
        if (x < 0) {
            x = -x;
            putchar('-');
        }
        if (x >= 10) __basic_putint(x / 10);
        putchar(x % 10 + '0');
    }
    
    inline void putint(int x, char external) {
        __basic_putint(x);
        putchar(external);
    }
}


namespace Solution {
    const int MAXN = 31000000 + 10;
    // 沒錯,就是要開這麼大
    
    int n, external[MAXN];
    char s[MAXN], str[MAXN << 1];
    
    void Pre() {
        str[0] = str[1] = '~';
        for (int i = 0; i < n; ++i) {
            str[i * 2 + 2] = s[i];
            str[i * 2 + 3] = '~';
        }
        n = n * 2 + 2;
        str[n] = 0;
    }
    
    void Manacher() {
        int maxRight = 0, mid = 0; // mid 初值無所謂
        for (int i = 1; i < n; ++i) {
            if (i < maxRight) {
                external[i] = std::min(external[(mid << 1) - i], external[mid] + mid - i);
            } else {
                external[i] = 1;
            }
            for (; str[i + external[i]] == str[i - external[i]]; ++external[i]);
            if (external[i] + i > maxRight) {
                maxRight = external[i] + i;
                mid = i;
            }
        }
    }
    
    void Work() {
        cin >> s;
        n = (int) strlen(s);
        Pre();
        Manacher();
        int ans = 1;
        for (int i = 0; i < n; ++i) ans = std::max(ans, external[i]);
        cout << ans - 1 << endl;
    }
}

int main(int argc, char *const argv[]) {
#define HANDWER_FILE
#ifndef HANDWER_FILE
    freopen("testdata.in", "r", stdin);
    freopen("testdata.out", "w", stdout);
#endif
    using namespace Solution;
    using namespace FastIO;
    Work();
    return 0;
}