學習筆記 - Manacher演算法
Manacher演算法 - 學習筆記
是從最近Codeforces的一場比賽瞭解到這個演算法的~
非常新奇,畢竟是第一次聽說 \(O(n)\) 的迴文串演算法
我在 vjudge 上開了一個〔練習〕,有興趣的reader們可以參考一下 \(QwQ\)
『演算法簡述』
一個思路比較簡單但非常有效的字串演算法(其實不止字串,反正就是用來求迴文的),用於求給定字串中的迴文子串,有一些研究者證明了它的時間複雜度均攤下來是 \(O(n)\) 的,只可惜我看不懂他們怎麼證明的……
中文名叫“馬拉車”演算法(或許是音譯過來的),它的想法非常簡單,只是利用了之前求解到的迴文串。
首先我們需要對原字串str進行一個操作——假如原串是 \(str=s_0s_1s_2...s_l\)
接下來我們定義 haf[i] 表示以 i 為中心的迴文串的最長半徑,比如 "abcba" 的haf[2]=3。這樣我們就可以表示一個以i為中心的最長的迴文子串了!
下面就是馬拉車演算法的精華——定義 \(Rig\) 為當前找到的迴文子串中右端點的最大值,\(Id\)
我們列舉迴文子串的中心位置 i ,如果 i<Rig ,那麼 i 就一定被包含在一個迴文子串裡2,那麼我們找到 i 關於 Id 的對稱位置即 \(j=(2*Id-i)\) ,可以算出以j為中心的被包含在以Id為中心的最大回文子串中的最大子串長度(我知道說起來有一點暈,但是相信 reader 們看了例子就會明白),舉個例子:
原串為 "1323141323" ,現在 i=8 ,那麼 Rig=9,Id=5(對應的子串為 "323141323")
找到對稱位置 j=2 ,找到以 j 為中心的包含在 "323141323" 中的最大回文子串 "323" (不能是 "13231",因為左邊的 "1" 在 "32314323" 外)
那麼我們可以知道因為 i,j 關於 Id 對稱,所以以 i 為中心的迴文子串的半徑至少是 min(haf[j],Rig-i)(取min是為了限制找到的串在以 Id 為中心的迴文子串中)。但是在這個基礎上,我們可能可以繼續擴充——繼續列舉檢驗兩邊的字元是否相同,如果相同則可以擴充套件。
列舉完為止~
看起來馬拉車演算法侷限性比較強,但實際上可以在迴文串的限制上有很多變化——甚至加上一些單調棧、線段樹之類的優化!至於具體哪些地方可能會用其他的演算法我會在模板程式碼裡註釋出來。
『例題』
一、〔HDU 3068 - 最長迴文〕
(也可以是 URAL - 1297,只是一個輸出具體的子串,另一個只輸出長度)
如果原串是從0開始儲存的話,我們可以在 Manacher 中算得 haf 的最大值 resmax,以及它對應的中心位置 resmid —— 略找規律,我們可以發現在原串中,這個迴文子串起始於 \((resmid-resmax)/2\),長度為 \((resmax-1)\)
二、〔HDU 4513 - 完美隊形II 〕
我的思路大概就是先預處理出 low[i] 表示以 i 為結尾的最長的不下降子串的長度(不是序列!必須連續!),然後找出以當前位置為中心的最長迴文串,再判斷迴文子串中的左半部分的不下降長度,相應的,子串的右半部分就是不上升的了~
『原始碼』
模板程式碼:
int haf[LEN*2+10]; //LEN是原串的長度
int Manacher(string str){
string mdy="-+"; //a='-' , b='+'
for(int i=0;i<str.length();i++)
mdy+=str[i],mdy+='+';
int Rig=0,Id=0;
for(int i=1;i<mdy.length();i++){ //注意這裡從1開始,忽略開頭的'-'
if(i<Rig) haf[i]=min(haf[Id*2-i],Rig-i);
else haf[i]=1; //i本身構成一個迴文子串
while(mdy[i-haf[i]]==mdy[i+haf[i]]){
haf[i]++;
/*
這裡經常會進行一些其他操作;
*/
}
if(i+haf[i]>Rig) Rig=i+haf[i],Id=i;
/*
這裡儲存答案;
這裡也經常進行其他操作;
*/
}
/*
求解完迴文子串後可能還要處理一些東西~
*/
}
HDU 3068 - 最長迴文
/*Lucky_Glass*/
#include<bits/stdc++.h>
using namespace std;
const int SIZ=110000*2;
char str[SIZ+5],mdy[SIZ*2+5];
int haf[SIZ+5];
int Manacher(){
mdy[0]='-';mdy[1]='+';
int lenstr=strlen(str);
for(int i=0;i<lenstr;i++)
mdy[2*i+2]=str[i],mdy[2*i+3]='+';
mdy[2*lenstr+2]='$';
int reslen=0,resmid,Rig=0,Id=0;
for(int i=1;i<lenstr*2+2;i++){
if(i<=Rig) haf[i]=min(haf[2*Id-i],Rig-i);
else haf[i]=1;
while(mdy[i-haf[i]]==mdy[i+haf[i]]) haf[i]++;
if(i+haf[i]>Rig){
Rig=i+haf[i];
Id=i;
}
if(reslen<haf[i]){
reslen=haf[i];
resmid=i;
}
}
return reslen-1;
}
int main(){
while(~scanf("%s",str)){
printf("%d\n",Manacher());
}
return 0;
}
HDU 4513 - 完美隊形II
/*Lucky_Glass*/
#include<bits/stdc++.h>
using namespace std;
const int N=100000;
int Cas,n;
int hgt[N+5],mem[N*2+5],low[N+5],haf[N*2+5];
int Manacher(){
int Rig=0,Id,ret=0;
for(int i=1;i<n*2+2;i++){
if(i<Rig) haf[i]=min(Rig-i,haf[Id*2-i]);
else haf[i]=1;
while(mem[i-haf[i]]==mem[i+haf[i]]) haf[i]++;
if(i+haf[i]>Rig){Rig=i+haf[i];Id=i;}
int len=haf[i]-1,mid=i;
int lef=(mid-len)/2+1;len;
if(len&1){
int fhaf=len/2,fmid=lef+len/2;
fhaf=min(fhaf,low[fmid]-1);
ret=max(ret,fhaf*2+1);
}
else{
int fhaf=len/2,fmid=lef+len/2-1;
fhaf=min(fhaf,low[fmid]);
ret=max(ret,fhaf*2);
}
}
return ret;
}
int main(){
scanf("%d",&Cas);
for(int cas=1;cas<=Cas;cas++){
memset(mem,0,sizeof mem);
memset(low,0,sizeof low);
scanf("%d",&n);
mem[0]=-1;mem[1]=-2;
for(int i=1;i<=n;i++){
scanf("%d",&hgt[i]);
if(hgt[i-1]<=hgt[i]) low[i]=low[i-1];
low[i]++;
mem[i*2]=hgt[i];mem[i*2+1]=-2;
}
printf("%d\n",Manacher());
}
return 0;
}
\(\mathcal{The\ End}\)
\(\mathcal{Thanks\ For\ Reading!}\)
如果有什麼沒看懂的可以在我的郵箱 \(lucky\[email protected]\) 上問,我會定期檢視郵箱並儘可能地解決問題!
簡單的舉個例子:\(str=abba\),假設 \(a='@',b='|'\),那麼修改過後的字串就是 \([email protected]|a|b|b|a|\),可見任意一個迴文子串(例如 \(|b|b|\))都是奇數的長度;↩
其實這裡隱含了一個條件:Id<i,因為 Id 是在列舉到 i 之前計算出來的,所以一定小於 i ;↩