manacher 演算法
基本概念
\(manacher\) 演算法是一種 字串演算法,通常用於求解給定的字串 \(S\) 內最長的迴文子串長度。其充分利用了迴文串的 對稱性,可以在 \(O(n)\) 的時間複雜度內求出最長迴文子串。\(manacher\) 演算法的主要思路和 \(KMP\) 演算法類似,都是通過已經求出的子串資訊來加速暴力列舉的過程。
演算法思想
\(manacher\) 演算法通過迴文的對稱性來確定出一部分已經確定的迴文子串,從而減少列舉的長度,達到線性的時間複雜度。當然,在對字串進行 \(manacher\) 演算法之前需要對字串進行處理。
首先考慮最暴力的做法:列舉 \(0 \leq i < n\)
為了避免分類討論,我們考慮在每個字元的首尾都增加一個沒有在字串 \(S\) 中出現過的字元。例如當字串 \(S = \texttt{texas}\) 時,在每個字元的首位都增加一個字元 \(\texttt{#}\) 以後,最終的字串 \(S^{\prime} = \texttt{#t#e#x#a#s#}\)
對於優化過暴力的 \(manacher\) 演算法,為了減少對邊界的判斷,我們再在字串的開頭加上另一個沒有字串中出現過的字元。最終的字串 \(S^{\prime\prime}\) 可能會是 \(\texttt{|#t#e#x#a#s#}\)。具體的原因參見下文。為方便起見,下文出現的字串 \(S\) 全部指代此處的字串 \(S^{\prime\prime}\)。
定義 \(len_i\) 表示字串 \(S\) 以下標 \(i\) 為迴文子串中點時的最長迴文子串長度半徑。例如 \(S = \texttt{|#a#b#a#b#a#}\)
假如 \(i > r\),那麼因為以 \(i\) 為中點的最長迴文子串一定會包含 \(S_{0, i}\) 以外的部分,所以無法直接從 \([len_0, len_{i - 1}]\) 轉移,考慮暴力雙指標列舉。反之,若 \(i \leq r\),那麼我們可以先確定一部分迴文子串。因為 \(i\) 被包含在某一個迴文子串內,所以在該回文子串 \(a = [S_l, S_r]\) 內一定存在一個迴文子串,使得這個迴文子串是以 \(i\) 為中點的迴文子串的子串。
結合一個例子來理解。不妨設 \(S = \texttt{|#t#e#x#e#x#}\),假設現在要更新 \(len_8, S_8 = \texttt{e}\)。此時 \(p = 6, r = 9\)。因為 \(8 < 9\),所以可以考慮先確定一部分迴文子串。我們發現因為 \(i\) 被包含在迴文子串內,所以在區間 \([i - len_i + 1, i + len_i - 1]\) 內一定存在至少兩個 \(S_i\),並且 \(S_i = S_{2 \times p - i}\)。具體到這個例子也就是在 \(4\) 的位置一定為 \(\texttt{e}\)。這時 \(len_{2 \times p - i}\) 一定已經計算過了,並且由於位置 \(i\) 也在目前最右側的最長迴文子串內,所以位置 \(i\) 的迴文子串一定包含長度為 \(len_{2 \times p - i} \times 2 - 1\) 的與位置 \(2 \times p - i\) 的迴文子串相同的子串。我們還需要考慮溢位,假如 \(len_{2 \times p - i}\) 過大,延伸到了右側最長迴文子串的右邊,那麼因為我們沒有計算右邊的值,所以不能用這個值來更新。那麼因為 \(i + len_{2 \times p - i} > r, 2 \times p - i - len_{2 \times p - i} < l\),所以迴文子串一定可以包含 \(S_{i, r}\)。因此當 \(i \leq r\) 的時候,可以確定的迴文子串長度最大為 \(\min\{len_{2 \times p - i}, r - i + 1\}\)。
從確定的迴文子串兩側開始延伸雙指標,直到遇到不同的字元為止。因為在迴圈的過程中最右側最長迴文子串的右端點是不降的,如果當前迴文子串的右端點不超過右側最長迴文子串的右端點,那麼直接轉移的時間複雜度顯然是 \(O(1)\)。如果超過,則此時右側最長迴文子串的右端點一定會增加。右端點右移的時間複雜度為 \(O(n)\)。因此 \(manacher\) 演算法的時間複雜度為 \(O(n)\)。
在程式碼實現的時候,儲存 \(len\) 時不必過多判斷邊界。這裡可以不用 \(- 1\),相應地程式碼中也有一些需要更改的地方。下面的模板字串下標從 \(0\) 開始,因此邊界條件的判斷是對的。
參考程式碼
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 1.1e7 + 5;
int n, cnt, ans;
int len[maxn * 3];
char a[maxn], s[maxn * 3];
void get_str()
{
s[cnt] = '#';
s[++cnt] = '|';
for (int i = 0; i < n; i++)
{
s[++cnt] = a[i];
s[++cnt] = '|';
}
}
int main()
{
int r = 0, mid = 0;
scanf("%s", a);
n = strlen(a);
get_str();
for (int i = 1; i <= cnt; i++)
{
if (i <= r)
len[i] = min(len[2 * mid - i], r - i + 1);
while (s[i - len[i]] == s[i + len[i]])
len[i]++;
if (len[i] + i - 1 >= r)
{
r = len[i] + i - 1;
mid = i;
}
ans = max(ans, len[i]);
}
printf("%d\n", ans - 1);
return 0;
}