問題:
找出字符串中的最長回文子串
思路:
舉例分析下,例如 ”abadaba“ 這個字符串的計算
1、從左往右遍歷,整個字符串,把每個字符和字符間的空隙當作回文的中心,然后向兩邊擴展來找到最長回文串,這種情況下默認得對每一個字符進行計算,計算量比較大,而且有部分計算其實能通過之前的計算得到答案,后面第2點開始討論下如何減少計算次數
2、利用回文的特性,結合已經計算的部分,盡量減少后面的計算次數。比如下圖,當我們計算到第5位的時候,通過前面的計算可以知道2-4位是一個回文子串,由於5也是回文子串,且跨幅度是2-8,因為4位不是回文子串,所以與4對應的6位也不是回文子串,同理可以推斷出6-8是一個回文子串
3、由於之前的計算已經知道了第5位為中心的abadaba是回文,而第4位為中心的a的回文長度是1,所以第6位為中心的回文長度只能是1,不用再去計算了。
4、以第7位為中心的回文串的計算,由之前分析已經知道最小長度是3了,但是還是需要進行擴展,因為第9位是什么根據之前的信息無法得知,需要擴展進行探索。
5、考慮到回文子串可能是復數,這里得在字符之間加多個標記進行特殊處理
總結下步驟:
1、先對字符串進行預處理,兩個字符之間加上特殊符號#
2、然后遍歷整個字符串,用一個數組來記錄以該字符為中心的回文長度,為了方便計算右邊界,在數組中記錄長度的一半(覆蓋半徑)
3、每一次遍歷的時候,如果該字符在已知回文串最右邊界的覆蓋下,那么就計算其相對最右邊界回文串中心對稱的位置,得出已知回文串的長度
4、判斷該長度和右邊界,如果達到了右邊界,那么需要進行中心擴展探索。當然,如果第3步該字符沒有在最右邊界的“半徑”下,則直接進行中心擴展探索。進行中心擴展探索的時候,同時又更新右邊界
5、最后得到最長回文之后,去掉其中的特殊符號即可
實現:
C++:
#include <iostream> #include <string> #include <cstdio> #include <sstream> #include <vector> using namespace std; string changeString(string source){ int length = 2*source.length()+1; char root[length]; root[0] = '#'; for(int i=0,j=1;i<source.length();i++){ root[j++] = source[i]; root[j++] = '#'; } string new_source = root; return new_source.substr(0,length); } void printArray(int a[],int size){ for(int i = 0; i < size; i++){ cout<<a[i]<<" "; } cout<<endl; } void printMaxString(int center,int half_size,string source){ cout<<"Max string is: "; for(int k=center-half_size;k<center+half_size;k++){ if(source[k]!='#'){ cout<<source[k]; } } cout<<endl; } void calculate(string source){ int left = 0,right = source.length();//記錄字符串的最左最右邊界 int center = 0,half_size = 0;//記錄最長回文子串的中心以及半徑 int cur_center = 0;//當前回文串在左側半徑內的中心位置 int max_right = 0;//計算當前訪問位置回文子串右半徑最大覆蓋范圍 int cover[right];//記錄字符串每個位置回文子串的覆蓋半徑 bool flag = true; int times = 0; for(int i=0;i<source.length();i++){ flag = true; //判斷要不要計算 if(i<max_right){ //因為在覆蓋的右半徑內,由於回文的特性,右半徑任何位置的回文子串覆蓋半徑在前面已經算出來了,這里僅需要找出對應的左半徑位置 cur_center = 2*center-i; //該位置的回文子串的半徑 cover[i] = cover[cur_center]; //當前位置加上回文覆蓋的半徑未超過當前回文子串右半徑最大覆蓋范圍,無需重新計算 if(cover[cur_center]+i<max_right){ flag = false; } } if(flag){ times++; int step = 1; while(i-step>=left && i+step<right){ if(source[i-step]==source[i+step]){ step++; }else{ break; } } cover[i] = step-1; } if(cover[i]>=half_size){ half_size = cover[i]; center = i; } if(i+cover[i]>=max_right){ max_right = cover[i]+i; } } printArray(cover,source.length()); printMaxString(center,half_size,source); cout<<"Calculate times is: "<<times<<endl; } int main(){ string source_array[] = { "abcdcef", "adaelele", "cabadabae", "aaaabcdefgfedcbaa", "aaba", "aaaaaaaaa" }; for(int i =0;i<6;i++){ // string source = "adaelele"; string source = source_array[i]; string new_source = changeString(source); cout<<"Source string is: "<<source<<endl; cout<<"New_source string is: "<<new_source<<endl; calculate(new_source); cout<<endl; } return 0; }
輸出:
Source string is: abcdcef New_source string is: #a#b#c#d#c#e#f# 0 1 0 1 0 1 0 3 0 1 0 1 0 1 0 Max string is: cdc Calculate times is: 14 Source string is: adaelele New_source string is: #a#d#a#e#l#e#l#e# 0 1 0 3 0 1 0 1 0 3 0 5 0 3 0 1 0 Max string is: elele Calculate times is: 13 Source string is: cabadabae New_source string is: #c#a#b#a#d#a#b#a#e# 0 1 0 1 0 3 0 1 0 7 0 1 0 3 0 1 0 1 0 Max string is: abadaba Calculate times is: 14 Source string is: aaaabcdefgfedcbaa New_source string is: #a#a#a#a#b#c#d#e#f#g#f#e#d#c#b#a#a# 0 1 2 3 4 3 2 1 0 1 0 1 0 1 0 1 0 1 0 15 0 1 0 1 0 1 0 1 0 1 0 1 2 1 0 Max string is: aabcdefgfedcbaa Calculate times is: 23 Source string is: aaba New_source string is: #a#a#b#a# 0 1 2 1 0 3 0 1 0 Max string is: aba Calculate times is: 8 Source string is: aaaaaaaaa New_source string is: #a#a#a#a#a#a#a#a#a# 0 1 2 3 4 5 6 7 8 9 8 7 6 5 4 3 2 1 0 Max string is: aaaaaaaaa Calculate times is: 19
分析:
這算法也就是”馬拉車“算法,精妙程度可以與KMP算法相媲美,利用回文的特征以及之前的計算結果,最大程度上減少后面的計算次數,由於只進行一次遍歷,時間復雜度上是O(n)