一、問題描述
迴文子串和迴文子序列不同:
子串,一定要連續
子序列,不一定連續
其實最長迴文子串是可以轉換成LCS來做的,具體方法就是:
將原串產生反向串,然後用dp求原串和反向串的LCS
但是這樣缺點也很明顯的是O(n*n)的複雜度,即使最佳化到:滾動數組+下標找反向串,也不能從根本上解決這個演算法的低效。
如果想在 O(n) 時間內解決迴文子串問題呢。
答案就是Manacher演算法,用一句話來概括這個演算法:
通過記錄已匹配的最右位置和對應的對稱中心來跳過一些沒用的比較
在下面的代碼中,這個已匹配的最右位置就是mx,對稱中心就是id。 二、Manacher演算法模板
完整模板
int Manacher(string tmp) { memset(P,0,sizeof(P)); int len=tmp.size(); int k; for(k=0;k<len;k++){ s[2*k] = '#'; s[2*k+1] = tmp[k]; } s[2*k] = '#'; s[2*k+1] = '\0'; //演算法核心 len=strlen(s); int mx = 0; //mx 記在i之前的迴文串中,延伸至最右端的位置 int id=0; //id 記下取得這個最優mx時的 座標值 for (int i=0;i<len;++i) { if( i < mx ) //在當前最優邊界左邊 P[i] = min(P[2*id-i],mx-i); else P[i]=1; for (;s[i-P[i]] == s[i+P[i]] && s[i-P[i]] != '\0' && s[i+P[i]] != '\0' ; ) //對於超出mx或者P[j]邊界的計算 P[i]++; if( P[i]+i > mx ) { //當前最佳情況,update mx和id mx = P[i] + i; //最右端情況 id = i; //記下座標 } } int res = 0; for (int i=0;i<len;++i) { res = max(res,P[i]); } return res-1;}
三、演算法思想
1、預先處理
演算法在字串開頭、結尾、字元之間都插入特殊字元(這裡用’#’),這樣就巧妙的把奇數的迴文串和偶數的迴文串統一起來考慮了(統一為奇數),不然處理迴文串會很麻煩。
比如:abc 變成 #a#b#c# 這樣有7個字元。
abcd 變成 #a#b#c#d# 有9個字元。
可能有疑問:預先處理成這樣,後面不是要算迴文子串長度的時候,又要減去這些特殊字元嗎。
其實不會,因為演算法用 P[i] 來記錄 i 這個 pos 的最長單臂子串長度,比如 #a#b#c#b#a# 這個單臂子串長度為 #a#b#c,即長度為6,
而這裡有一個很好的性質,P[i]-1 就是該迴文子串在原串中的長度。即 abcba 就是5 2、核心演算法
其實Manacher演算法的核心代碼很簡單,但是理解起來感覺相當精妙,果然是演算法是代碼寫成的詩歌。
核心代碼:
//演算法核心len=strlen(s);int mx = 0; //mx 記在i之前的迴文串中,延伸至最右端的位置int id=0; //id 記下取得這個最優mx時的 座標值for (int i=0;i<len;++i) { if( i < mx ) //在當前最優邊界左邊 P[i] = min(P[2*id-i],mx-i); //算出對稱初始值 else P[i]=1; //如果超出mx邊界,P[i]初始值為1 for (;s[i-P[i]] == s[i+P[i]] && s[i-P[i]] != '\0' && s[i+P[i]] != '\0' ; ) //對於超出mx或者P[j]邊界的計算 P[i]++; if( P[i]+i > mx ) { //當前最佳情況,update mx和id mx = P[i] + i; //最右端情況 id = i; //記下座標 }}
演算法開始是會通過一個if else判斷來決定P[i]的初始值。
演算法的關鍵點就在這裡:
if( i < mx ) //在當前最優邊界左邊 P[i]= min(P[2*id-i],mx-i); //算出對稱初始值else P[i]=1; //如果超出mx邊界,P[i]初始值為1
那為什麼是 min(P[2*id-i],mx-i) 呢。 3.2.1 取 P[2*id-i],即 P[2*id-i] <= mx-i
這種情況下,整個 中心i(或中心j) 的子串都涵蓋在 中心為 id,長度為 mx-id 的巨型臂膀下,由於 id為中心的完全對稱,必然 P[j] == P[i], 3.2.2 取 mx-i,即 P[2*id-i] >= mx-i
這種情況下,是 id 的臂展無法完全包裹 以i為中心的臂展時:
根據對稱性可知,圖中兩個綠框所包圍的部分是相同的,也就是說以 i 為中心的迴文子串,其向右至少會擴張到mx的位置,也就是說 P[i] >= mx - i。至於mx之後的部分是否對稱,就只能一個一個匹配了。
另外特別要注意的是,要注意設定“哨兵”,也就是自己設定特殊字元或者’\0’代表邊界,不然後面的
for (;s[i-P[i]] == s[i+P[i]] ; P[i]++);
就會越界。
給出我畫出的原手稿
下圖中,當i為13時,臂展最大,但是這個字元的P[i]初始化為6,即 i = 5那個點 ‘c’。 參考資料
[1] http://www.cnblogs.com/biyeymyhjob/archive/2012/10/04/2711527.html
[2] http://blog.csdn.net/ggggiqnypgjg/article/details/6645824/