標籤:長度 sub log 方法 csdn 左右 之間 markdown return
提出問題
最長迴文子串問題:給定一個字串,求它的最長迴文子串長度。
如果一個字串正著讀和反著讀是一樣的,那它就是迴文串。如a、aa、aba、abba等。
暴力解法
簡單粗暴:找到字串的所有子串,遍曆每一個子串以驗證它們是否為迴文串。一個子串由子串的起點和終點確定,對於一個長度為n的字串,共有n^2個子串。這些子串的平均長度大約是n/2,因此這個解法的時間複雜度是 \(O(n^3)\)。明顯不可取。
方法改進
迴文子串是連續的,而且是對稱的。長度為奇數迴文串以最中間字元的位置為對稱軸左右對稱,而長度為偶數的迴文串的對稱軸在中間兩個字元之間的空隙。可否利用這種對稱性來提高演算法效率呢?答案是肯定的。我們知道整個字串中的所有字元,以及字元間的空隙,都可能是某個迴文子串的對稱軸位置。可以遍曆這些位置,在每個位置上同時向左和向右擴充,直到左右兩邊的字元不同,或者達到邊界。對於一個長度為n的字串,這樣的位置一共有 n+n-1=2n-1 個,在每個位置上平均大約要進行 n/4 次字元比較,於是此演算法的時間複雜度是 \(O(n^2)\)。
另外一種改進方法是利用動態規劃,DP[i][j]定義成子串[i, j]是否是迴文串。外迴圈 i從 n?1 往 0 遍曆,內迴圈 j 從 i 往 n?1 遍曆,若s[i]==s[j]:
- 若i==j,則dp[i][j]=true;
- 若i和j是相鄰的,則dp[i][j]=true;
- 若i和j中間只有一個字元,則dp[i][j]=true;
- 否則,檢查dp[i+1][j-1]是否為true,若為true,那麼dp[i][j]就是true。
前三條可以合并,即 j?i≤2。求得dp[i][j]真值後,也可快速解決問題。時間複雜度:\(O(n^2)\)。
Manacher‘s Algorithm
對於 \(O(n^2)\) 的複雜度,或許還不滿足,是否可以再最佳化一些呢?
先分析改進方法中的缺陷,利用迴文中心需要分奇偶兩種情況討論,兩種改進都會重複訪問子串,降低效率。
Manacher‘s Algorithm正是針對這兩個問題進行進一步的改進,將時間複雜度降到了神奇的 \(O(n)\):
問題一:迴文長度奇偶性問題
為了不區分奇偶兩種情況,對字串作預先處理,在所有字元之間(包括首尾)插入相同字元如‘#‘,處理之後所有的子串都是奇數長度的。如aba→#a#b#a#。
插入的是同樣的符號,且符號不存在於原串,因此子串的迴文性不受影響,原來是迴文的串,插完之後還是迴文的,原來不是迴文的,依然不會是迴文。
問題二:重複訪問問題
定義迴文半徑:迴文串中最左或最右位置的字元與其對稱軸的距離。演算法中定義迴文半徑數組 \(RL\),\(RL[i]\) 表示以第i個字元為對稱軸的迴文串的迴文半徑。
定義 \(MaxRight\),表示當前訪問到的所有迴文子串,所能觸及的最右一個字元的位置。另外還要記錄下 \(MaxRight\) 對應的迴文串的對稱軸所在的位置,記為 \(pos\)。
核心代碼:RL[i] = i < MaxRight ? min(RL[2*pos-i], MaxRight-i) : 1;
理解了這行代碼,可以說就理解了這個演算法。我們來分情況討論:
(1)i < MaxRight時,可以再分兩種情況:
①MaxRight - i > RL[2*pos-i],如,以S[j]為中心的迴文子串包含在以S[id]為中心的迴文子串中,由於 i 和 j 對稱,以S[i]為中心的迴文子串必然包含在以S[id]為中心的迴文子串中,所以必有 P[i] = P[j]。
②MaxRight - i < RL[2*pos-i],如,以S[j]為中心的迴文子串不一定完全包含於以S[id]為中心的迴文子串中,但是基於對稱性可知,中兩個綠框所包圍的部分是相同的,也就是說以S[i]為中心的迴文子串,其向右至少會擴張到mx的位置,也就是說 P[i] >= mx - i。
(2)i > MaxRight時,無法對 P[i]做更多的假設,只能P[i] = 1,然後再去慢慢匹配了。
代碼實現
返回最長的迴文子串。代碼中resLen為處理後字串的最大迴文半徑,對應到原來的字串中時,只需-1即是整個迴文串的長度。
string Manacher(string s) { //預先處理 string t = "#"; for (int i = 0; i < s.size(); ++i) { t += s[i]; t += "#"; } vector<int> RL(t.size(), 0); int MaxRight = 0, pos = 0; int resLen = 0, resCenter = 0; for (int i = 0; i < t.size(); ++i) { RL[i] = MaxRight > i ? min(RL[2 * pos - i], MaxRight - i) : 1; while (i-RL[i] >=0 && i+RL[i] < t.size() && t[i + RL[i]] == t[i - RL[i]])//擴充,注意邊界 ++RL[i]; //更新最右端及其中心 if (MaxRight < i + RL[i] -1) { MaxRight = i + RL[i] -1; pos = i; } if (resLen < RL[i]) { resLen = RL[i]; resCenter = i; } } return s.substr((resCenter - resLen + 1) / 2 , resLen - 1);}
時間複雜度:\(O(n)\)。在參考連結中有比較詳細的證明過程。
參考連結:1190000003914228
參考連結:http://www.cnblogs.com/grandyang/p/4475985.html
什麼是馬拉車演算法(Manacher's Algorithm)?