一:背景
給定一個字串,求出其最長迴文子串。例如:
(1)s=”abcd”,最長迴文長度為 1;
(2)s=”ababa”,最長迴文長度為 5;
(3)s=”abccb”,最長迴文長度為 4,即 bccb。
以上問題的傳統思路大概是,遍曆每一個字元,以該字元為中點向兩邊尋找。其時間複雜度為 O(n2) O(n^2),很不高效。而在1975年,一個叫Manacher的人發明了一個演算法,Manacher演算法,也稱馬拉車演算法,該演算法可以把時間複雜度提升到 O(n) O(n)。下面來看看馬拉車演算法是如何工作的。 二:演算法過程分析
由於迴文分為偶迴文(比如 bccb)和奇迴文(比如 bcacb),而在處理奇偶問題上會比較繁瑣,所以這裡我們使用一個技巧,在字元間插入一個字元(前提這個字元未出現在串裡)。舉個例子:s="abbahopxpo",轉換為s_new="$#a#b#b#a#h#o#p#x#p#o#"(這裡的字元 $ 只是為了防止越界,下面代碼會有說明),如此,s 裡起初有一個偶迴文abba和一個奇迴文opxpo,被轉換為#a#b#b#a#和#o#p#x#p#o#,長度都轉換成了奇數。
定義一個輔助數組int p[],p[i]表示以s_new[i]為中心的最長迴文的半徑,例如:
| i |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
| s_new[i] |
$ |
# |
a |
# |
b |
# |
b |
# |
a |
# |
h |
# |
o |
# |
p |
# |
x |
# |
p |
# |
o |
# |
| p[i] |
|
1 |
2 |
1 |
4 |
5 |
2 |
1 |
2 |
1 |
2 |
1 |
2 |
1 |
2 |
1 |
6 |
1 |
2 |
1 |
2 |
1 |
可以看出,p[i]-1正好是原字串中最長迴文串的長度。
Manacher演算法之所以快,就快在對 p 數組的求法上有個捷徑。在我們解決了奇偶迴文的繁瑣時,剩下的痛點就是求 p 數組,按照普通思維,我們是這樣求解的:求解p[i],先初始化p[i]=1,再以s_new[i]為中心判斷兩邊是否相等,相等就p[i]++。這就是普通的思維,但是我們想想,能否讓p[i]的初始化不是 1,讓它更大點,看下圖:
設定兩個變數,mx 和 id 。
mx 代表以s_new[id]為中心的最長迴文最右邊界,也就是mx=id+p[id]。
假設我們現在求p[i],也就是以s_new[i]為中心的最長迴文半徑,如果i<mx,如上圖,那麼:
if (i < mx) p[i] = min(p[2 * id - i], mx - i);
2 * id -i其實就是等於 j ,p[j]表示以s_new[j]為中心的最長迴文半徑,見上圖,因為 i 和 j 關於 id 對稱,我們利用p[j]來加快尋找。 三:代碼
/** * * author 劉毅(Limer) * date 2017-02-25 * mode C++ */#include<iostream> #include<string.h>#include<algorithm> using namespace std;char s[1000];char s_new[2000];int p[2000];int Init(){ int len = strlen(s); s_new[0] = '$'; s_new[1] = '#'; int j = 2; for (int i = 0; i < len; i++) { s_new[j++] = s[i]; s_new[j++] = '#'; } s_new[j] = '\0'; //別忘了哦 return j; //返回s_new的長度 }int Manacher(){ int len = Init(); //取得新字串長度並完成向s_new的轉換 int maxLen = -1; //最長迴文長度 int id; int mx = 0; for (int i = 1; i < len; i++) { if (i < mx) p[i] = min(p[2 * id - i], mx - i); //需搞清楚上面那張圖含義, mx和2*id-i的含義 else p[i] = 1; while (s_new[i - p[i]] == s_new[i + p[i]]) //不需邊界判斷,因為左有'$',右有'\0' p[i]++; if (mx < i + p[i]) //我們每走一步i,都要和mx比較,我們希望mx儘可能的遠,這樣才能更有機會執行if (i < mx)這句代碼,從而提高效率 { id = i; mx = i + p[i]; } maxLen = max(maxLen, p[i] - 1); } return maxLen;}int main(){ while (printf("請輸入字串:\n")) { scanf("%s", s); printf("最長迴文長度為 %d\n\n", Manacher()); } return 0;}
四:演算法複雜度分析
文章開頭已經提及,Manacher演算法為線性演算法,即使最差情況下其時間複雜度亦為 O(n) O(n),在進行證明之前,我們還需要更加深入地理解上述演算法過程。
定義 mx 為以s_new[id]為中心的最長迴文最右邊界,也就是mx=id+p[id]。j 與 i 關於 id 對稱,根據迴文的性質,p[i]的值基於以下三種情況得出:
(1)j 的迴文串有一部分在 id 的之外,如下圖:
上圖中,黑線為 id 的迴文,i 與 j 關於 id 對稱,紅線為 j 的迴文。那麼根據代碼此時p[i]=mx-i,即紫線。那麼p[i]還可以更大麼。答案是不可能。見下圖:
假設右邊新增的紫色部分是p[i]可以增加的部分,那麼根據迴文的性質,a 等於 d ,也就是說 id 的迴文不僅僅是黑線,而是黑線+兩條紫線,矛盾,所以假設不成立,故p[i]=mx-i,不可以再增加一分。
(2)j 迴文串全部在 id 的內部,如下圖:
此時p[i]=p[j],那麼p[i]還可以更大麼。答案亦是不可能。見下圖:
假設右邊新增的紅色部分是p[i]可以增加的部分,那麼根據迴文的性質,a 等於 b ,,也就是說 j 的迴文應該再加上 a 和 b ,矛盾,所以假設不成立,故p[i]=p[j],也不可以再增加一分。
(3)j 迴文串左端正好與 id 的迴文串左端重合,見下圖:
此時p[i]=p[j]或p[i]=mx-i,並且p[i]還可以繼續增加,所以需要
while (s_new[i - p[i]] == s_new[i + p[i]]) p[i]++;
根據(1)(2)(3),很容易推出Manacher演算法的最壞情況,即為字串內全是相同字元的時候。在這裡我們重點研究Manacher()中的for語句,推算髮現for語句內平均訪問每個字元5次,即時間複雜度為: Tworst(n)=O(n) T_{worst}(n)=O(n)。
同理,我們也很容易知道最佳情況下的時間複雜度(最佳情況即字串內字元各不相同)。推算得平均訪問每個字元4次,即時間複雜度為: Tbest(n)=O(n) T_{best}(n)=O(n)。
綜上,Manacher演算法的時間複雜度為 O(n) O(n)。
參考文獻:
[1] Stephen__. hdu3068之manacher演算法+詳解
文章轉自我的個人部落格:http://www.61mon.com/index.php/archives/181/