最長迴文子串
題意:給定一個字符串s,找出該字符串中最長的迴文子串。
字符串如“abcba”,”abbbba”這樣呈中心對稱的子串稱爲迴文串。該題目是一個老題了,有多種不同的解法,我整理一下方便以後查詢。
暴力動態規劃法
這個方法是我們看到這個題目後最容易想到的方法,暴力搜索所有的子串,判斷每個子串是否是迴文串;我們用一個二維空間記錄已計算過的子串是否爲迴文串,這樣之後針對每個新子串進行判定的時候可以利用之前記錄信息輔助判斷。基本的動規方程如下:
- s[i]==s[j] 時 : dp[i][j] = dp[i+1][j-1]; (j>0)
- s[i] != s[j] 時: dp[i][j] = false ;
該方法的代碼很簡單,可去網上查詢,最終的時間複雜度是O(n^2).
中心點法
對於迴文子串,我們還需要想到其性質:字符串關於中心位置對稱。那麼可以看到若子串s[3,5]不是迴文串的話,那麼s[2,6],s[1,7]等必然不是迴文串,利用該性質我們可以省去一些不必要的比較計算。
相對於第一種方法,肯定有着更好的實際運行效果。
參考代碼如下:
string longestPalindrome(string s)
{
if (s.empty()) return "";
if (s.size() == 1) return s;
int min_start = 0, max_len = 1;
for (int i = 0; i < s.size();)
{
if (s.size() - i <= max_len / 2) break;//不會超過最大回文串長度,直接跳出
int j = i, k = i;
while (k < s.size()-1 && s[k+1] == s[k]) ++k; // 跳過重複的元素.
i = k+1;
while (k < s.size()-1 && j > 0 && s[k + 1] == s[j - 1]) { ++k; --j; } // 向左右兩邊擴張.
int new_len = k - j + 1;
if (new_len > max_len) { min_start = j; max_len = new_len; }
}
return s.substr(min_start, max_len);
}
思路即是 從字符串的第一個字符做中心點開始,不斷向串的兩邊擴張直到不能擴張,之後將中心點右移,不斷重複操作;過程中記錄下迴文串起始點和迴文長度。
當然該方法的最壞的時間情況仍然是O(n^2). 對於s=”aaaaaaaa”這樣的特殊字符串,每個中心點都會左右擴張到字符串的邊緣,因此此時時間複雜度爲O(n^2).
Manacher算法
該算法從網上學來,充分巧妙的利用了迴文串的性質,能保證線性的時間複雜度。
算法思路:
首先在每相鄰兩個字符間插入一個分隔符(這個分隔符要在原串中沒有出現過),一般都用‘#’分隔,這樣就將奇數長度與偶數長度迴文串統一起來考慮了(迴文串長度全爲奇數了),然後用一個輔助數組P記錄以每個字符爲中心的最長迴文串的半徑。爲了方便理解,可以參照下圖:
從新字符串的開始字符爲中心點,不斷向串的兩邊擴張,直到不能擴張,之後將中心點右移,不斷重複操作;
算法中要維護一箇中心點(center)和一個邊界(boundary).從新的字符串的首字符開始,不斷向兩邊擴張,直到達到以該中心點構成的最長迴文子串,此時若該回文串的右邊界大於boundary,那就更新boundary和center。也就是說,boundary記錄算法運行至今的最偏右的右邊界…
- 每次遍歷到一個新的中心點時,我們可以判斷該點是否落在boundary的左邊,若落在左邊,那麼則可以利用P數組簡化計算,如圖(圖醜不要在意細節)。
- 根據迴文串的對稱性,我們找到i關於center的對稱點j,若P[j]小於boundary-i 那麼爲了保證大的迴文串的對稱性,此時P[i] =P[j],沒有必要繼續比較下去。
- 若P[j]>boundary-i 那麼還需要繼續計算P[i]。讓P[i] = bounar-i,之後繼續循環判斷 (t[i+1+P[i]] == t[i-1-P[i]])。
算法過程中 同時記錄最大回文子串的長度和起始點。
參考代碼如下:
string longestPalindrome(string s)
{
string t;
for(int i=0;i<s.size();i++)
t=t+"#"+s[i];
t.push_back('#');
vector<int> P(t.size(),0);
int center=0,boundary=0,maxLen=0,resCenter=0;
for(int i=1;i<t.size()-1;i++)
{
int j= 2*center -i;
if(boundary>i)
{
P[i] = min(P[j],boundary-i);
}else
P[i] =0;
while(i-1-P[i]>=0 && i+1+P[i]<t.size() && t[i+1+P[i]] == t[i-1-P[i]])
P[i]++;
if(i+P[i]>boundary)
{ // update center and boundary
center = i;
boundary = i+P[i];
}
if(P[i]>maxLen)
{ // update result
maxLen = P[i];
resCenter = i;
}
}
return s.substr((resCenter - maxLen)/2, maxLen);
}
時間複雜度分析:boundary在整個算法過程中是一直向右移動的,並且每次移動都是因爲新的對稱字符的比較,通過聚合分析方法來考慮代碼段
while(i-1-P[i]>=0 && i+1+P[i]<t.size() && t[i+1+P[i]] == t[i-1-P[i]])
P[i]++;
這裏的代碼執行 引起了boundary的向右擴張,而boundary最大隻能是n。所以整個算法的時間複雜度是線性的,即O(n)。
本文分析了三種不同的求解最長迴文子串的方法,希望能給讀本文的人一點收穫和思考。