Manacher演算法

2017BeiJiang發表於2024-06-29

Manacher 演算法可在 \(O(n)\) 解決最長迴文串的問題。

透過預處理 \(d_i\) 表示以 \(i\) 為中心的迴文串向兩邊延伸的最長長度來解決原問題。

如對於字串 abcba(下標從\(1\)開始),\(d_3=3\),即 \(cba\)

P3805【模板】manacher

演算法流程如下:

改造字串

在開頭插入 $,末尾插入 !,每個字元的左邊和右邊都插入一個 #

例如:

abcdefg
改造為:
$#a#b#c#d#e#f#g#!

在開頭和末尾插入兩個不同的字元,可以避免邊界問題。而在相鄰字元插入 #,則是為了處理長度為偶數的迴文串,例如:aa 轉化為 #a#a#,即變為以第二個 # 為中心的迴文串。

預處理 \(d_i\)

在預處理 \(d_i\) 時,需要維護一個表示當前右端點最右邊的迴文串區間,不妨用 \([l,r]\) 表示。

假設現在正在處理 \(i\),那麼分為兩種情況:

\(i\in [l,r]\)
對於 \(i\)\([l,r]\) 中,可以找到一些性質,記 \(i'\) 表示 \(i\) 關於 \(\frac{l+r}{2}\) 的對稱點(注意:此時 \(d_{i'}\) 已經求出),那麼分為以下幾種情況:

\(i'\) 的迴文長度未超出 \([l,r]\)(下圖藍色為迴文部分)
由於 \([l,r]\) 本身為迴文串,所以在對稱位置,迴文情況是一致的,那麼直接 \(d_i=d_{i'}\) 即可。

\(i'\) 的迴文長度超出 \([l,r]\)
那麼我們只能確認圖中(即在\([l,r]\)內的部分)藍色部分為迴文,至於紅色部分(超出區間部分),我們無法確認,只能透過暴力列舉處理。

\(i>r\)
直接暴力往兩邊擴充套件即可。

計算完 \(d_i\) 記得更新區間。

程式碼:

#include<bits/stdc++.h>
using namespace std;

typedef long long LL;
const int N=3e7;
int n,d[N];
string s;
char ch[N];

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);

    cin>>s;
    ch[0]='$';//改造字串
    for(int i=0;i<s.size();i++) {
        ch[++n]='#';
        ch[++n]=s[i];
    }
    ch[++n]='#';
    ch[++n]='!';
    d[1]=1;
    for(int l=1,r=1,i=2;i<n;i++) {
        if(i>=l&&i<=r) d[i]=min(d[l+r-i],r-i+1);//l+r-i即為i',r-i+1即i到右邊界距離
        while(ch[i+d[i]]==ch[i-d[i]]) d[i]++;//暴力擴充套件
        if(i+d[i]-1>=r) {r=i+d[i]-1; l=i-d[i]+1;}//更新區間
    }
    int ans=0;
    for(int i=1;i<=n;i++) {
        ans=max(ans,d[i]-1);
    }
    cout<<ans;
    return 0;
}

擴充套件應用

裸的Manacher演算法只能求出以一個字元為中心向兩邊延伸的最長迴文長度,但是沒法求出以某個點為起點的最長迴文長度,需要透過遞推的方式求解。

首先預處理出上文的 \(d\) 陣列,然後用 \(i\)\(d_i\) 計算出 \([l_i,r_i]\),此時計算出的所有區間,都是儘可能長的區間,也就是說:如果 \([1,5]\)是迴文,那麼\([2,4]\)也一定是迴文,而目前的做法只能處理出 \([1,5]\),處理不出 \([2,4]\)

所以考慮用 \(dp\) 解決問題,設 \(f_{i}\) 表示以 \(i\) 為開頭的最長長度,先用 \(i,d_i\) 計算出一部分 \(f_{i}\)

從上文 \([1,5]\)\([2.4]\) 的例子也能得到一些啟發,即前一個開頭的最長迴文損失兩個(一頭一尾)就變成當前位置迴文長度,即:

\[f_i\leftarrow f_{i-1}-2 \]

當然,在 Manacher 演算法中,由於字元之間用 \(#\) 隔開,所以實際改寫為:

\[f_i\leftarrow f_{i-2}-2 \]

相關文章