Manacher的主要用途是求一個字符串中包含的最長迴文子串。
一、前期處理
1.原始字符串長度有奇有偶,爲了方便處理字符串,我們使用了一種統一的處理方法。在每個字符兩邊都插入一個特殊字符(注意這個字符一定是原始字符串不包含的,否則就會混了)。
比如原始字符串是"abcba",那增加特殊字符"#“之後就變成”#a#b#c#b#a#";原始字符串是"abba",增加特殊字符"#“之後就變成”#a#b#b#a#"。這樣無論原始字符串長度是奇還是偶,處理之後都會變成奇數長度。
2.在增加特殊符號之後,爲了方便地處理數組越界問題,我們在字符串頭部再增加一個特殊字符(這個字符不在原始字符串內,也不能與初次添加的特殊字符相同),這樣做的目的是在判定迴文串時不可能越過0。
比如初次添加後字符串爲"#a#b#c#b#a#",我們可以再選用"@“作爲二次特殊字符加到頭部,這樣字符串就變成”@#a#b#c#b#a#"。
爲了講解方便,我們先給出最終結果。
我們以字符串"12212321"爲例,在前期處理之後字符串變爲"@#1#2#2#1#2#3#2#1#",記爲S[i],然後我們用一個數組P[i]來記錄以i爲中心的迴文串的半徑(包括i本身)。比如:
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
s[i] | @ | # | 1 | # | 2 | # | 2 | # | 1 | # | 2 | # | 3 | # | 2 | # | 1 | # |
P[i] | - | 1 | 2 | 1 | 2 | 5 | 2 | 1 | 4 | 1 | 2 | 1 | 6 | 1 | 2 | 1 | 2 | 1 |
max(P[i]-1)就是最長迴文子串的長度,我們下面來講P[i]怎麼求。
二、關於P[i]的說明
記住P[i]就是以i爲中心的迴文半徑!
首先P[i]的求解是從左到右的,也就是如果要求P[i],那P[1]~P[i-1]已經求出來了,其中P[0]是越界指標不用考慮和計算。
P[i]指的是包括i本身在內的迴文串半徑,也就是說P[i]最小是1,因爲只有一個字符也算迴文串。比如檢測到了迴文串"#2#3#2#",其中3爲中心,那P[i]=4。
三、輔助變量
(1)id
id表示的是當前探測到的迴文子串的中心點。
(2)mx
mx表示的是當前探測到的迴文子串的右邊界(不是邊界本身,而是最右值+1)。
我們來分析一下上圖的示例。
當i=1時,令P[i]=1,然後看i+1和i-1是否相等,發現不等,判定結束。所以P[i]=1,此時id=1,mx=2。
當i=2時,令P[i]=1,然後看i+1和i-1是否相等,發現相等,P[i]+=1,然後看i+2和i-2是否相等,發現不等,判定結束。我們得到一個迴文串"#1#",P[i]=2,此時新的迴文串右邊界爲4,超過了原來的mx,所以我們需要更新id=2,mx=4。
當i=3時,P[i]=1,此時新的右邊界爲4,沒有超過mx,所以id和mx不更新。
當i=4時,我們檢測到新的迴文串"#2#",P[i]=2,此時新的迴文串右邊界爲6,超過了原來的mx,所以我們需要更新id=4,mx=6。
當i=5時,我們檢測到新的迴文串"#1#2#2#1#2#",P[i]=6,此時新的迴文串右邊界爲11,超過了原來的mx,所以我們需要更新id=5,mx=11。
…
四、核心方法
當你看上面的求解過程,你會覺得跟平常的迴文串求解步驟是一樣的。別急,下面就是Manacher的優化的地方了。先上圖。
當前我們探測到的迴文串爲now_palindrome,中心點爲id,右邊界爲mx,左邊界爲mx相對於id的對稱點,即2*id-mx。當我們檢測完id,i向後移動的過程中,我們可以利用迴文串的對稱性來縮減運算。圖中i相對於迴文串中心id的對稱點是j,也就是i+j=2*id,即j=2*id-i。
所以可以知道的一個情況是:
我們分情況來解釋這句話:
(1)j是迴文串中心,左半徑在now_palindrome內。也就是說以j爲中心的迴文串最左邊沒有超過mx的對稱點,即左半徑在圖中橙色方塊內。這時候由對稱性可知P[i]=P[j],這樣我們就不用對i進行逐個比較了。
(2)j是迴文串中心,但左半徑超出了now_palindrome。這時候我們只能確定的一件事是以i爲中心的迴文串右半徑至少到達了mx。也就是P[i]>=mx-i。至於超過mx的部分是否仍然對稱,就需要我們去逐個判斷了。這樣我們節省了mx之內的判斷步驟。
用數學表達式來寫就是:
(1)j的左半徑越界
這時先令P[i]=mx-i,也就是i的半徑至少從i到mx這麼長。然後比較mx和2*i-mx是否相等,如果相等P[i]加1,然後接着比較mx+1和2*i-mx-1是否相等,如果相等P[i]加1,這樣一直比較直到遇到兩個不相等的數,P[i]計算完畢。
(2)j的左半徑不越界
此時P[i]=P[j]。當然這時候如果你接着從i+P[i]的位置比較一定不相等,因爲P[j]已經證實了這一點。
看到這的時候我們必須要理解一個前提:
也就是我們討論的是當id固定的時候,i的移動情況,但是實際過程中有可能i移動兩下id就換了,這樣當然i又會從id+1的位置開始判定;但還存在一種情況就是i移動到mx了,id仍然保持不變。
我們上面說過了id更換的條件是新檢測到的迴文串超出了原來回文串的範圍,而當i移動到mx時,此時檢測到的迴文串肯定超出了原來的範圍了,所以是相當於自然重置了。所以當i>=mx時,我們應該先令P[i]=1,然後逐個比較i+1和i-1,i+2和i-2……一直比較到兩個數不相等,這樣就能夠計算出P[i]。
總結起來就是:
if(mx>i)
{
if(mx-i<=P[j])
{
P[i]=mx-i
比較mx和2*i-mx,mx+1和2*i-mx-1
}
else
{
P[i]=P[j]
比較i+P[i]和i-P[i]//其實不用比較,由P[j]可知一定不等
}
}
else
{
P[i]=1;
比較i+1和i-1,i+2和i-2……
}
你會發現無論什麼情況,開始比較的位置一定都是i+P[i],因爲P[i]已經告訴我們i的半徑探到哪了,所以這就爲我們節省了時間,把代碼優化一下就變成:
P[i]=mx>i?min(mx-i,P[2*id-i]):1;
while(S[i+P[i]]==S[i-P[i]])
P[i]++;
可以看到我們用min(mx-i,P[2*id-i])統一 了mx-i<=P[j]和mx-i>P[j]的情況。其實仔細想一下就能知道,如果j左半徑探出去了,那P[i]=mx-i,此時mx-i<P[j];如果j沒探出去,那P[i]=P[j],P[j]<mx-i。
五、總體代碼
void Manacher(string str)
{
string s=str;
if(s=="")
return ;
for(int i=0;i<s.length();i+=2)//前期處理
s.insert(i,1,'#');
s="@"+s+"#";
int P[s.length()];
int id=0,mx=0;
int max_num=0;//記錄最大半徑
int max_id=0;//記錄最大半徑下標
for(int i=1;i<s.length();i++)
{
P[i]=mx>i?min(P[2*id-i],mx-i):1;//初始賦值
while(s[i+P[i]]==s[i-P[i]])//繼續搜索P[i]之外的地方
P[i]++;
if(i+P[i]>mx)//判定id是否更新
{
mx=i+P[i];
id=i;
}
if(P[i]>max_num)
{
max_num=P[i];
max_id=i;
}
}
cout<<"最長迴文子串長度爲:"<<max_num-1<<endl;
cout<<"最長迴文子串爲:"<<str.substr((max_id-P[max_id])/2,max_num-1);
}