Try   HackMD

最長迴文子字串(Longest Palindromic Substring, LPS)

tags: Competitive Programming Note

本文已停止更新,新版請至 WiwiHo 的競程筆記

給一個字串

S,求
S
中最長的為迴文的子字串長度。

Manacher's Algorithm

為了方便,接下來的索引值都是從 0 開始,也就是說,

S 的第一個字元是
S0

首先,先求出以每個字元為中心點的最長迴文長度,但偶數長度的迴文沒有中心點,所以這邊做一個操作:在每個字元中間和

S 兩端加上一個
S
中沒有的字元
c
,結果會是:
cS0cS1cS2cS3...S|S|1c

這個加工過的字串為

T,長度是
2×|S|+1
,可以用一個簡單的方式來得到
Ti
Ti={Si2if imod2=1celse
接下來的程式碼會用
c
. 作為範例。

#define T(x) ((x) % 2 ? s[(x) / 2] : '.')

接下來,我們用

Ri 來表示以
Ti
為中心點的最長迴文半徑(含中心點)。再來的重點是要如何快速求出
Ri
,先從
T
的左邊讀到右邊,並且一邊計算
Ri
為何。

mx 為目前為止,我們找到的迴文子字串的最右邊的結尾,也就是:
max{i+Ri1}
,而
center
為這個結尾在最右邊的迴文子字串的中心點,也就是:
mx=center+Rcenter1

先寫一個 function 來方便等等的計算,

ex(l,r) 表示從
l
往左和
r
往右的最長對稱字串長度,也就是說:令
t=ex(l,r)
T(lt+1)..l=Tr..(r+t1)
,且
t
盡可能地大。

int ex(int l, int r){
    int i = 0;
    while(l - i >= 0 && r + i < n && T(l - i) == T(r + i)) i++;
    return i;
}

如果我們已經算好了前

i1 項,現在要算第
i
項,我們會遇到幾種狀況:

  • Ti
    沒有被任何中心點在前面的迴文覆蓋(
    i>mx

    在這樣的情況下,我們不能用先前算好的東西來得到
    Ri
    ,所以只能硬算:
    ​​​​r[i] = ex(i, i);
    ​​​​center = i;
    ​​​​mx = i + r[i] - 1;
    
  • Ti
    有被中心點在前面的迴文覆蓋(
    imx

    如果這樣的話,我們可以利用覆蓋
    Ti
    的迴文另一邊的對稱字串來得到
    Ri
    。令
    i
    為迴文另一邊的對稱點,也就是說:
    i=center(icenter)
    ,接著我們令
    len
    Ti
    到這個迴文結尾的長,也就是
    len=mxi+1
    。再來又分成了幾種情況:
    • Ti
      為中心的最長迴文剛好貼齊以
      Tcenter
      為中心的迴文邊界(
      Ri=len

      這樣子我們只能確保
      RiRi
      ,因為我們並不知道
      Ti+Ri
      是否等於
      TiRi
      ,所以只好接著算:
      Ri=Ri+ex(iRi,i+Ri)
    • Ti
      為中心的最長迴文在
      Tcenter
      為中心的迴文範圍以內(
      Ri<len

      這樣我們直接確定
      Ri=Ri
      了,因為以
      Tcenter
      為中心的迴文兩邊是對稱的。
    • Ti
      為中心的最長迴文超出
      Tcenter
      為中心的迴文範圍(
      Ri>len

      仔細想一下會發現:
      TcenterRcenterTcenter+RcenterTcenterRcenter=Tilen=Ti+len=TilenTcenter+Rcenter=Ti+lenTilenTi+len
      由此可知,
      Ri
      不會超過
      len
      ,而我們知道以
      Ti
      為中心的最長迴文是
      Ri
      ,而
      Ri>len
      也就是說,存在以
      Ti
      為中心,長度為
      len
      的迴文,因此,
      Ri=len
    ​​​​int ii = center - (i - center);
    ​​​​int len = mx - i + 1;
    ​​​​if(i > mx){
    ​​​​    r[i] = ex(i, i);
    ​​​​    center = i;
    ​​​​    mx = i + r[i] - 1;
    ​​​​}
    ​​​​else if(r[ii] == len){
    ​​​​    r[i] = len + ex(i - len, i + len);
    ​​​​    center = i;
    ​​​​    mx = i + r[i] - 1;
    ​​​​}
    ​​​​else{
    ​​​​    r[i] = min(r[ii], len);
    ​​​​}
    

最後,只要算怎麼把最大的

Ri 轉成
S
中最大迴文子字串的長度就好了。我們知道
S
的每一個字元在
T
中,前方都會有一個字元
c
,而
T
最後也有一個
c
,剛好以
Ti
為中心的最長迴文,兩端也會是
c
,所以我們可以用一樣的想法來轉回去:令以
Ti
為中心的最長迴文長度為
t=Ri×21
,轉回去
S
的長度就是
(t1)2
(注意
t
必為奇數),化簡後得到
Ri1

#define T(x) ((x) % 2 ? s[(x) / 2] : '.')

string s;
int n;

int ex(int l, int r){
    int i = 0;
    while(l - i >= 0 && r + i < n && T(l - i) == T(r + i)) i++;
    return i;
}

int main(){
    cin >> s;
    n = 2 * s.size() + 1;

    int mx = 0;
    int center = 0;
    vector<int> r(n);
    int ans = 1;
    r[0] = 1;
    for(int i = 1; i < n; i++){
        int ii = center - (i - center);
        int len = mx - i + 1;
        if(i > mx){
            r[i] = ex(i, i);
            center = i;
            mx = i + r[i] - 1;
        }
        else if(r[ii] == len){
            r[i] = len + ex(i - len, i + len);
            center = i;
            mx = i + r[i] - 1;
        }
        else{
            r[i] = min(r[ii], len);
        }
        ans = max(ans, r[i]);
    }

    cout << ans - 1 << "\n";
    return 0;
}

仔細觀察會發現,這個過程只是不斷把

mx 往右移,因此複雜度是漂亮的
O(|S|)