算法模板——Manacher

字符串算法在各大高級比賽中均有用到,所以,學習好字符串算法對我們而言十分重要。那麼,今天我們就給大家介紹一個快速求迴文串的算法,Manacher算法,我們也習慣性叫它馬拉車算法。

一.引入

首先我們要知道什麼是迴文串——當一個字符串它從右到左和從左到右讀是一樣的,我們就稱它爲迴文串。考慮一下最暴力的算法,我們可以枚舉字符串的每個子串,判斷其是否爲迴文串,時間複雜度是O(n^3)。當然,我們可以加點優化,枚舉每個中心點,然後向兩邊匹配,時間複雜度是O(n^2)。不過這個複雜度依然不讓人滿意,因此,我們引入Manacher算法, 將時間複雜度降到線性,提高了算法效率。

二.算法流程

由於迴文串分爲奇迴文和偶迴文,因此給算法帶來不小的麻煩,所以我們可以在字符串中間加入一些字符,使得其一定爲奇迴文,如 s= ‘abaoyyo’,轉換後就成了 s_new= ‘#&a&b&a&o&y&y&o&^’(前後加字符只是爲了防止越界,後面會講),這樣,原有的迴文串 ‘ababa’ 和 ‘oyyo’ 便變成了 ‘&a&b&a&’ 和 ‘&o&y&y&o&’ ,都是奇迴文了。同時,我們要引入一個數組 p,p[i] 代表以 i 爲中心的迴文串的最大半徑,如:

i 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
s_new # & a & b & a & o & y & y & o & ^
p 0 1 2 1 4 1 2 1 2 1 2 5 2 1 2 1 0

爲什麼開始和最後面是0呢,是因爲我們在計算的時候一般不考慮這兩個邊界,只是防止越界用的。並且我們可以看到,p[i]-1 對應的就是在原串中以 s[i] 爲中心的迴文串的長度(不包括添加的字符)。那麼,爲什麼Manacher算法要比一般的算法要快呢?因爲它在求 p 的時候有一個捷徑,如下圖:
這裏寫圖片描述
p[i] 是按順序求的,我們記錄 Max 爲以 s_new[id] 爲中心,右端點最大的值,即爲 p[i]+i,其中,i,j 關於 id 對稱,紅色箭頭代表對於一個點的擴張半徑。如果 i < Max 的話,我們則有

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

三.解釋

就上圖而言,p[i]=p[j] 這點是毋庸置疑的,也就是 p[i]=p[id*2-i](因爲i,j 關於 id 對稱)。那麼,爲什麼要取min呢?這是我們要保證 p[i] 在直接更新的時候,右端點不能超過 Max 。那麼爲什麼不能超過 Max 呢?我們畫個圖理解下
這裏寫圖片描述
假定 p[j] 的左邊界超過 id 的左邊界,那麼當我們直接令 p[i]=p[j] 時,i 的右邊界就會超過 id 的右邊界,那麼這種情況是否存在呢,答案是否定的。
因爲根據假設可得 j 的紅色擴張部分和 i 的紅色擴張部分是一樣的,並且由於對稱,綠色的箭頭也也是對稱的,既然如此,那麼id的邊界爲什麼不到兩個綠色箭頭的端點呢?
因此,在這種情況下,p[i] 不能直接等於 p[j],p[i] 最大隻能到 Max 的右邊界,即 p[i]=Max-i 。同時,我們可以知道,在這種情況下,p[i] 是不能再擴張的。

Manacher還有其他的一些情況,如下圖
這裏寫圖片描述
如果 p[j] 的左右邊界都在 id 內部,那麼在 p[i]=p[j] 後,p[i] 還能繼續擴張嗎?答案依然是否定的。
若 i 能擴張,則必定有一段擴張在 id 內部,即綠色部分。那麼根據對稱可知,j 也會有兩段對稱的綠色,那麼 p[j] 爲什麼不擴張呢?
因此,這種情況下,p[i] 也是不能擴張的。

那麼,是不是 p[i]=min(p[id*2-i],Max-i) 就好了呢?答案依然是否定的
這裏寫圖片描述
如果說j的左邊界與 id 的左邊界重合,那麼i的右邊界就和 Max 重合。在這個情況下,i 是可以繼續擴張的,之後的擴張,就只能不斷的暴力匹配了

四.補充

我們開始講到的所有情況都是建立在 i < Max 的基礎之上的。那麼,如果 i > Max 的話該如何呢?其實,當 i > Max 的時候,我們沒有辦法對 i 做出任何的假設,只能令其等於1,然後暴力匹配即可

對於 id 和 Max 而言,每次更新完 i 後進行比較,取最大值即可

暴力匹配的時候很有可能導致數組越界,因此我們在最前面和最後面加上兩個不同的字符來保證其失配

五.算法複雜度

由於本算法對於匹配過的字符串基本不匹配,沒有匹配過的字符串也只是O(n)掃過,因此時間複雜度可以看爲是線性的,十分優秀

六.代碼

#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define inf 0x7f7f7f7f
using namespace std;
typedef long long ll;
typedef unsigned int ui;
typedef unsigned long long ull;
inline int read(){
    int x=0,f=1;char ch=getchar();
    for (;ch<'0'||ch>'9';ch=getchar())  if (ch=='-')    f=-1;
    for (;ch>='0'&&ch<='9';ch=getchar())    x=(x<<1)+(x<<3)+ch-'0';
    return x*f;
}
inline void print(int x){
    if (x>=10)     print(x/10);
    putchar(x%10+'0');
}
const int N=1e6;
char s[N*2+10],t[N+10];
int p[N*2+10];
int main(){
    printf("請輸入字符串\n"); 
    scanf("%s",t+1);
    int len=strlen(t+1),Max=0,ID=0,Ans=0,cnt=0;
    for (int i=1;i<=len;i++)    s[i<<1]=t[i],s[i<<1|1]='&';  //添加字符,使其變爲奇串
    len=len<<1|1;
    s[1]='&',s[0]='%',s[len+1]='#';  //防止越界
    for (int i=1;i<=len;i++){
        p[i]=Max>i?min(p[ID*2-i],Max-i):1;  //核心部分
        while (s[i+p[i]]==s[i-p[i]])    p[i]++;  //暴力匹配
        if (Max<i+p[i]) Max=p[ID=i]+i;  //更新Max
        if (Ans<p[i])   Ans=p[i],cnt=i-p[i];  //更新答案
    }
    cnt>>=1;
    printf("最長迴文串爲\n"); 
    for (int i=cnt+1;i<cnt+Ans;i++) putchar(t[i]);
    putchar('\n');
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章