Manacher算法(求最長迴文子串)

  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]的說明

  ξ\xi記住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表示的是當前探測到的迴文子串的中心點。
idid變換的條件是新檢測到的迴文串超出了當前回文串的範圍。
  (2)mx
  mx表示的是當前探測到的迴文子串的右邊界(不是邊界本身,而是最右值+1)。
mxmx=i+P[i]mx的計算方法是mx=i+P[i]。

  我們來分析一下上圖的示例。
  當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。
  所以可以知道的一個情況是:
now_palindromeij在now\_palindrome的範圍內,i和j的迴文半徑是相同的。
  我們分情況來解釋這句話:
  (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的左半徑越界
jP[j]<=2idmxj-P[j]<=2*id-mx2idiP[j]<=2idmx2*id-i-P[j]<=2*id-mxmxi<=P[j]即mx-i<=P[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的左半徑不越界
mxi>P[j]即mx-i>P[j]
  此時P[i]=P[j]。當然這時候如果你接着從i+P[i]的位置比較一定不相等,因爲P[j]已經證實了這一點。

  看到這的時候我們必須要理解一個前提:
idi一旦id變換,i的移動過程就會重置。
  也就是我們討論的是當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);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章