Manacher's Algorithm ----馬拉車算法


本文是我對博友 BIT祝威 和Grandyang ,以及寒小陽關於最長回文子串上關於馬拉車算法理解的整理,若是對我的整理有所不懂得,建議去看BIT祝威的博客,很詳細,以下純屬個人不成熟的理解。

首先,得先了解什么是回文串(我之前就不是很了解,汗)。回文串就是正反讀起來就是一樣的,如“abba”。關於采用時間復雜度為O(n^2),以每個字符為中心去向兩端遍歷尋找最大回文串的方法,可以見我之前些的博客,戳這里

當我們遇到字符串為“aaaaaaaaa”,之前的算法就會發生各個回文相互重疊的情況,會產生重復計算,然后就產生了一個問題,能否改進?答案是能,1975年,一個叫Manacher發明了Manacher Algorithm算法,俗稱馬拉車算法,其時間復雜為O(n)。該算法是利用回文串的特性來避免重復計算的,至於如何利用,且由后面慢慢道來。

在時間復雜度為O(n^2)的算法中,我們在遍歷的過程要考慮到回文串長度的奇偶性,比如說“abba”的長度為偶數,“abcba”的長度為奇數,這樣在尋找最長回文子串的過程要分別考奇偶的情況,是否可以統一處理了?

馬拉車算法:

一)第一步是改造字符串S,變為T,其改造的方法如下:

在字符串S的字符之間和S的首尾都插入一個“#”,如:S=“abba”變為T="#a#b#b#a#" 。我們會發現S的長度是4,而T的長度為9,長度變為奇數了!!那S的長度為奇數的情況時,變化后的長度還是奇數嗎?我們舉個例子,S=“abcba”,變化為T=“#a#b#c#b#a#”,T的長度為11,所以我們發現其改造的目的是將字符串的長度變為奇數,這樣就可以統一的處理奇偶的情況了

二)第二步,為了改進回文相互重疊的情況,我們將改造完后的T[ i ] 處的回文半徑存儲到數組P[ ]中,P[ i ]為新字符串T的T[ i ]處的回文半徑,表示以字符T[i]為中心的最長回文字串的最端右字符到T[i]的長度,如以T[ i ]為中心的最長回文子串的為T[ l, r ],那么P[ i ]=r-i+1。這樣最后遍歷數組P[ ],取其中最大值即可。若P[ i ]=1表示該回文串就是T[ i  ]本身。舉一個簡單的例子感受一下:

數組P有一性質,P[ i ]-1就是該回文子串在原字符串S中的長度 ,那就是P[i]-1就是該回文子串在原字符串S中的長度,至於證明,首先在轉換得到的字符串T中,所有的回文字串的長度都為奇數,那么對於以T[i]為中心的最長回文字串,其長度就為2*P[i]-1,經過觀察可知,T中所有的回文子串,其中分隔符的數量一定比其他字符的數量多1,也就是有P[i]個分隔符,剩下P[i]-1個字符來自原字符串,所以該回文串在原字符串中的長度就為P[i]-1。【這段解釋引用 dyx心心

另外,由於第一個和最后一個字符都是#號,且也需要搜索回文,為了防止越界,我們還需要在首尾再加上非#號字符,實際操作時我們只需給開頭加上個非#號字符,結尾不用加的原因是字符串的結尾標識為'\0',等於默認加過了。這樣原問題就轉化成如何求數組P[ ]的問題了。

三)如何求數組P [ ]

  從左往右計算數組P[ ], Mi為之前取得最大回文串的中心位置,而R是最大回文串能到達的最右端的值。

  1)當 i <=R時,如何計算 P[ i ]的值了?毫無疑問的是數組P中點 i 之前點對應的值都已經計算出來了。利用回文串的特性,我們找到點 i 關於 Mi 的對稱點 j ,其值為 j= 2*Mi-i 。因,點 j 、i 在以Mi 為中心的最大回文串的范圍內([L ,R]),

       a)那么如果P[j] <R-i (同樣是L和j 之間的距離),說明,以點 j 為中心的回文串沒有超出范圍[L ,R],由回文串的特性可知,從左右兩端向Mi遍歷,兩端對應的字符都是相等的。所以P[ j ]=P[ i ](這里得先從點j轉到點i 的情況),如下圖:

 

     b)如果P[ j ]>=R-i (即 j 為中心的回文串的最左端超過 L),如下圖所示。即,以點 j為中心的最大回文串的范圍已經超出了范圍[L ,R] ,這種情況,等式P[ j ]=P[ i ]還成立嗎?顯然不總是成立的!因,以點 j 為中心的回文串的最左端超過L,那么在[ L, j ]之間的字符肯定能在( j, Mi ]找到相等的,由回文串的特性可知,P[ i ] 至少等於R- i,至於是否大於R-i(圖中紅色的部分),我們還要從R+1開始一一的匹配,直達失配為止,從而更新R和對應的Mi以及P[ i ]。

  2)當 i > R時,如下圖。這種情況,沒法利用到回文串的特性,只能老老實實的一步步去匹配。

相應的代碼如下:

 1 string Manacher(string s)
 2 {
 3     /*改造字符串*/
 4     string res="$#";
 5     for(int i=0;i<s.size();++i)
 6     {
 7         res+=s[i];
 8         res+="#";
 9     }
10 
11     /*數組*/
12     vector<int> P(res.size(),0);
13     int mi=0,right=0;   //mi為最大回文串對應的中心點,right為該回文串能達到的最右端的值
14     int maxLen=0,maxPoint=0;    //maxLen為最大回文串的長度,maxPoint為記錄中心點
15 
16     for(int i=1;i<res.size();++i)
17     {
18         P[i]=right>i ?min(P[2*mi-i],right-i):1;     //關鍵句,文中對這句以詳細講解
19         
20         while(res[i+P[i]]==res[i-P[i]])
21             ++P[i];
22         
23         if(right<i+P[i])    //超過之前的最右端,則改變中心點和對應的最右端
24         {
25             right=i+P[i];
26             mi=i;
27         }
28 
29         if(maxLen<P[i])     //更新最大回文串的長度,並記下此時的點
30         {
31             maxLen=P[i];
32             maxPoint=i;
33         }
34     }
35     return s.substr((maxPoint-maxLen)/2,maxLen-1);
36 }

 

若還是有不懂的,可以參考,這篇文章參考的大神們寫的博客。本人小白啊,若有錯誤,歡迎大神留言指出!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM