# 最長迴文子字串(Longest Palindromic Substring, LPS)
###### tags: `Competitive Programming Note`
本文已停止更新,新版請至 [WiwiHo 的競程筆記](https://cp.wiwiho.me/)
給一個字串 $S$,求 $S$ 中最長的為迴文的子字串長度。
## Manacher's Algorithm
為了方便,接下來的索引值都是從 0 開始,也就是說,$S$ 的第一個字元是 $S_0$。
首先,先求出以每個字元為中心點的最長迴文長度,但偶數長度的迴文沒有中心點,所以這邊做一個操作:在每個字元中間和 $S$ 兩端加上一個 $S$ 中沒有的字元 $c$,結果會是:$c S_0 c S_1 c S_2 c S_3 ... S_{|S|-1} c$。
這個加工過的字串為 $T$,長度是 $2 \times |S| + 1$,可以用一個簡單的方式來得到 $T_i$:
$$
T_i =
\begin{cases}
S_{\lfloor \frac{i}{2} \rfloor} &\quad \text{if }i \bmod 2 = 1 \\
c &\quad \text{else}
\end{cases}
$$
接下來的程式碼會用 $c$ 為 `.` 作為範例。
```cpp
#define T(x) ((x) % 2 ? s[(x) / 2] : '.')
```
接下來,我們用 $R_i$ 來表示以 $T_i$ 為中心點的最長迴文半徑(含中心點)。再來的重點是要如何快速求出 $R_i$,先從 $T$ 的左邊讀到右邊,並且一邊計算 $R_i$ 為何。
令 $\text{mx}$ 為目前為止,我們找到的迴文子字串的**最右邊的結尾**,也就是:$\max \{i+R_i-1\}$,而 $\text{center}$ 為這個**結尾在最右邊**的迴文子字串的中心點,也就是:$\text{mx}=\text{center}+R_\text{center}-1$。
先寫一個 function 來方便等等的計算,$\text{ex}(l,r)$ 表示從 $l$ 往左和 $r$ 往右的最長對稱字串長度,也就是說:令 $t=\text{ex}(l,r)$,$T_{(l-t+1)..l}=T_{r..(r+t-1)}$,且 $t$ 盡可能地大。
```cpp
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;
}
```
如果我們已經算好了前 $i-1$ 項,現在要算第 $i$ 項,我們會遇到幾種狀況:
- $T_i$ 沒有被任何中心點在前面的迴文覆蓋($i > \text{mx}$)
在這樣的情況下,我們不能用先前算好的東西來得到 $R_i$,所以只能硬算:
```cpp
r[i] = ex(i, i);
center = i;
mx = i + r[i] - 1;
```
- $T_i$ 有被中心點在前面的迴文覆蓋($i \leq \text{mx}$)
如果這樣的話,我們可以利用覆蓋 $T_i$ 的迴文另一邊的對稱字串來得到 $R_i$。令 $i'$ 為迴文另一邊的對稱點,也就是說:$i'=\text{center}-(i-\text{center})$,接著我們令 $\text{len}$ 為 $T_i$ 到這個迴文結尾的長,也就是 $\text{len}=\text{mx}-i+1$。再來又分成了幾種情況:
- 以 $T_{i'}$ 為中心的最長迴文剛好貼齊以 $T_{\text{center}}$ 為中心的迴文邊界($R_{i'}=\text{len}$)
這樣子我們只能確保 $R_i \geq R_{i'}$,因為我們並不知道 $T_i+R_{i'}$ 是否等於 $T_i-R_{i'}$,所以只好接著算:$R_i=R_{i'}+\text{ex}(i-R_{i'}, i+R_{i'})$。
- 以 $T_{i'}$ 為中心的最長迴文在 $T_{\text{center}}$ 為中心的迴文範圍以內($R_{i'}<\text{len}$)
這樣我們直接確定 $R_i=R_{i'}$ 了,因為以 $T_{\text{center}}$ 為中心的迴文兩邊是對稱的。
- 以 $T_{i'}$ 為中心的最長迴文超出 $T_{\text{center}}$ 為中心的迴文範圍($R_{i'}>\text{len}$)
仔細想一下會發現:
$$
T_{\text{center}-R_\text{center}} \neq T_{\text{center}+R_\text{center}}\\
T_{\text{center}-R_\text{center}} = T_{i'-\text{len}} = T_{i'+\text{len}} = T_{i-\text{len}} \\
T_{\text{center}+R_\text{center}} = T_{i+\text{len}} \\
T_{i-\text{len}} \neq T_{i+\text{len}}
$$
由此可知,$R_i$ 不會超過 $\text{len}$,而我們知道以 $T_{i'}$ 為中心的最長迴文是 $R_{i'}$,而 $R_{i'} > \text{len}$也就是說,存在以 $T_i$ 為中心,長度為 $\text{len}$ 的迴文,因此,$R_{i}=\text{len}$。
```cpp
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);
}
```
最後,只要算怎麼把最大的 $R_i$ 轉成 $S$ 中最大迴文子字串的長度就好了。我們知道 $S$ 的每一個字元在 $T$ 中,前方都會有一個字元 $c$,而 $T$ 最後也有一個 $c$,剛好以 $T_i$ 為中心的最長迴文,兩端也會是 $c$,所以我們可以用一樣的想法來轉回去:令以 $T_i$ 為中心的最長迴文長度為 $t=R_i \times 2 - 1$,轉回去 $S$ 的長度就是 $\frac{(t-1)}{2}$(注意 $t$ 必為奇數),化簡後得到 $R_i-1$。
```cpp
#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;
}
```
仔細觀察會發現,這個過程只是不斷把 $\text{mx}$ 往右移,因此複雜度是漂亮的 $O(|S|)$。