1. 程式人生 > 其它 >Manacher 和 KMP 學習筆記

Manacher 和 KMP 學習筆記

給同級神仙們講的演算法。

\(\text{Manacher}\)

解決問題:求一個字串中最長迴文連續子序列的演算法。

首先我們考慮一個基礎的暴力:列舉l和r,對於每個l和r求遍歷一遍判斷是否為迴文,複雜度 \(O(N^3)\)

在這個基礎上稍微優化一下,也是很顯然的做法:長度為奇數迴文串以最中間字元的位置為對稱軸左右對稱,而長度為偶數的迴文串的對稱軸在中間兩個字元之間的空隙。可以遍歷這些對稱軸,在每個對稱軸上同時向左和向右擴充套件,直到左右兩邊的字元不同或者到達邊界。

但這也是 \(O(N^2)\) 的。

那麼我們需要一個更優秀的解法。

這就引入了馬拉車演算法,可以 \(O(n)\) 實現。

首先我們考慮兩種迴文:\(\text{abba}\)

\(\text{aba}\) ,一種是偶迴文,另一種是奇迴文。

這樣分開很難處理,因此我們考慮把兩個字串變成 #a#b#b#a# 和 #a#b#a 兩個 迴文。

我們定義一個數組 \(p\)\(p_i\) 表示 以 \(i\) 為中心的最長迴文半徑。答案即為 \(p\) 的最大值減一乘二。

現在問題是如何求 \(p\)

定義兩個變數mx和id。mx就是以id為中心的最長迴文右邊界,也就是mx=id+p[id],隨後我們需要mx做出它的最大貢獻。

假設我們在求 \(p_i\)(以 \(i\)為中心的最長迴文半徑),如果 \(i<mx\)(如上圖),那麼我們就用 \(mx\)

\(j\) 來更新到我們已知的可以更新的最大長度。

if(i<mx)  
    p[i]=min(p[2*id-i],mx-i);

\(2 \times id-i\)\(i\) 關於 \(id\) 的對稱點,而 \(p_j\) 表示以 \(j\) 為中心的最長迴文半徑,這樣我們就可以利用 \(p_j\)\(mx\) 加快速度了。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;

char s[11000002];
char s_new[21000002];//存新增字元後的字串 
int p[21000002];

int Init() {//形成新的字串 
    int len=strlen(s);//len是輸入字串的長度
    s_new[0]='$';//處理邊界,防止越界 
    s_new[1]='#';
    int j=2; 
    for(int i=0;i<len;i++) {
        s_new[j++]=s[i];
        s_new[j++]='#';
    } 
    s_new[j]='\0';//處理邊界,防止越界(容易忘記) 
    return j;// 返回s_new的長度 
}

int Manacher() {//返回最長迴文串 
    int len=Init();//取得新字串的長度, 完成向s_new的轉換
    int max_len=-1;//最長迴文長度
    int id;
    int mx=0;
    for(int i=1;i<=len;i++) {
        if(i<mx)
            p[i]=min(p[2*id-i],mx-i);//上面圖片就是這裡的講解 
        else p[i]=1;
        while(s_new[i-p[i]]==s_new[i+p[i]])//不需邊界判斷,因為左有'$',右有'\0'標記;
            p[i]++;//mx對此迴文中點的貢獻已經結束,現在是正常尋找擴大半徑
        if(mx<i+p[i]) {//每走移動一個迴文中點,都要和mx比較,使mx是最大,提高p[i]=min(p[2*id-i],mx-i)效率 
            id=i;//更新id 
            mx=i+p[i];//更新mx 
        }
        max_len=max(max_len,p[i]-1); 
    } 
    return max_len; 
}
 
int main()
{
    scanf("%s",&s);
    printf("%d",Manacher());
    return 0;
}