\(Manacher\) 演算法
\(Manacher(馬拉車)\) 演算法,是一種高效解決最長迴文子串問題的演算法。其 \(O(n)\) 的複雜度相較於暴力 \(O(n^2)\) 和字串雜湊 \(O(nlogn)\) 來說,快了不少。
演算法實現:
首先說一下暴力的解法,對於每一個字串上的字元,考慮以其為起點,向兩邊擴充套件。若字串上回文串不多且長度普遍較短,則暴力的時間複雜度接近 \(O(n)\) 。當然,若其迴文串數量較多且長度較長時,暴力的複雜度將退化成 \(O(n^2)\) 。
如上圖,在計算以 \(i\) 和以 \(j\) 為中心的迴文字串時,重複計算了一段 \(w\) 區間,這也是暴力的效率低的原因,考慮對其進行最佳化。應該最佳化掉這種重複計算。
首先,同一其形式,由於迴文字串分為奇數長度的和偶數長度的,其中心也有所不同,所以先將待處理的字串進行一個小處理。在開頭和結尾處加上兩個不同的字元,防止越界,在其中的每個字元中,加入其中不會出現的字元來分割每個字元。這樣使的兩種不同的字串都會變為奇數字符串。
例:由 \(ABBA\) 變為 \(\$A\#B\#B\#A\&\) ,則其中心字元由 \(BB\) 變為 \(\#\) 省去了分類討論,同時可證得這樣初始化字串並不會改變其的迴文性質,即原先回文的子串在初始化後仍是迴文的。
然後是對迴文子串長度的統計,設 \(p[i]\) 表示以 \(i\) 為中心的迴文串的半徑,當然,我們還需要了解另一個重要的性質:迴文的映象也是迴文,概括一下說的話大概是:對於一個迴文字串 \(s\) 來說,其中心字元左右兩側的迴文字串在另一側必定會有一個相同的迴文子串 ,我們可以利用這個性質,對於 \(p[i]\) 來說,已知 \(p[1],p[2],p[3]...p[i-1]\) ,令 \(R\) 為這之前的迴文字串的最右端點,\(C\) 是 \(R\) 所在的迴文字串的中心字元,即 \(p[C]\) 是一個已經求過的點, \(R\) 是其右端點且是已經求得的所有右端點中最大的, \(R=C+p[C]\) 。對於 \(R\) 左邊的迴文串來說,可以利用性質減少暴力的次數,從而最佳化程式。
當有節點 \(i<R\) 時,其分為兩種情況(PS: \(j\) 為 \(i\) 關於 \(C\) 對稱的迴文子串),如下圖:
1.若 \(j\) 的區間左端點大於區間 \(C\) 的左端點,即圖上第一種情況,即 \(j \subseteq C\) ,那麼由於 \((i+j)/2=C\) (中點座標公式),可得 \(j=C \times 2-i\) 則 \(p[i]=p[C \times 2-i]\) ,後再用暴力擴充套件法擴充套件(由於不知道是否到達最長)。
2.若 \(j\) 的區間左端點小於區間 \(C\) 的左端點,即圖上第二種情況,即 \(j \nsubseteq C\) ,由於性質條件不成立,則只能投影一部分,則可以被投影的區間為 \(p[i]=w=R-i=p[C]+C-i\) ,後再用暴力擴充套件法向後推。
這兩種情況可以統一處理,取最小值,後再用暴力向外擴充套件即可。
當然,不可能全部都在其左邊,當有節點 \(i \geqslant R\) 時,由於還沒有計算到,則只能令 \(p[i]=1\) 後向後暴力擴充套件求 \(q[i]\) 。
我們會發現無論是那種情況,最後還是要回到暴力擴充套件法上,不禁擔心其複雜度的正確性,關於其證明,後面再給出。
code
char s[maxn]; //要處理的字串
char s1[maxn]; //初始化後的字串
int n,ans; //字串長度,答案
void in_it(){
int k=0;
s1[k++]='$'; //開頭
s1[k++]='#';
for(int i=0;i<n;i++){
s1[k++]=s[i];
s1[k++]='#';
}
s1[k++]='&'; //結尾
n=k; //更新字串長度
}
void manacher(){
int r=0,c; //初始化最右點,
for(int i=1;i<n;i++){
//分類討論
if(i<r) p[i]=min(p[(c<<1)-i],p[c]+c-i);
else p[i]=1;
//暴力向兩邊擴充套件
while(s1[i+p[i]]==s1[i-p[i]]) p[i]++;
if(p[i]+i>r){ //若超出最右點
c=i;
r=p[i]+i; //更新
}
}
}
signed main(){
cin>>s;
n=strlen(s);
in_it();
manacher();
for(int i=0;i<n;i++){
ans=max(ans,p[i]); //更新ans
}
cout<<ans-1;
}
在看完程式碼後,我們發現其實執行 \(manacher\) 函式的過程其實是不斷計算 \(p[i]\) ,實際是向右推進 \(R\) 的過程。由於 \(R\) 不會重複遍歷,所以其複雜度為 \(O(n)\) 。