上一篇KMP算法之后好幾天都沒有更新,今天介紹最長回文子串。
首先介紹一下什么叫回文串,就是正著讀和倒著讀的字符順序都是一樣的,eg:level,noon。而回文子串,顧名思義,就是主串中滿足回文性質的子串。
求解的常規思想,就是先求出主串的所有子串,在判斷是否是回文串,然后選出最長的,這一種方法的時候復雜度較高,是O(n^3),所以一般不采用這種方法,下面介紹兩種方法求解。
1. 中心擴展法
中心擴展法可以說是常規算法的改進。首先我們知道,回文串是中心對稱的,相比從頭到尾遍歷字符串的方法,從中間開始向兩邊擴展,時間會減少一半。
算法思想:把主串中的每一個字符當做回文串的中心,向兩邊擴展,求出最長的回文子串。其中要注意奇數位的回文子串和偶數位的回文子串的區別。eg:aba的中心是b,而abba的中心應該是bb。使用中心擴展法的時間復雜度是O(n^2),空間復雜度是O(1)。
代碼
核心算法是l2r的部分,以傳入的mid為回文串的中心計算最長的回文子串,其中需要注意的地方有兩點:
l2r中的第一個while循環,之前提到過要注意奇數位的回文串和偶數位的回文串,在代碼中,判斷中心點的字符和右邊的字符是否相等,就可以跳過偶數位的中心點。
注意l2r中返回語句,從第二個while循環中跳出的時候,已經多進行了一步left--,right++的操作了。
int longest;//子串長
int start;//最長回文子串在主串中的起始位置
/*計算以mid為中心的最長回文子串*/
int l2r(char *string, int mid) {
int len = strlen(string);
int left = mid - 1, right = mid + 1;
//跳過相同的部分
while (string[right] == string[mid])
right++;
while (left > 0 && right < len){
if (string[left] == string[right]) {
//從中間開始算,各向外移一位
start = left;
left--;
right++;
}
else {
break;
}
}
//重置回文串的起始位置
if (left < 0)
start = 0;
//printf("\tstart:%d left:%d right:%d len:%d\n", start, left, right, right - 1 - (left + 1) + 1);
//跳出while循環的時候,要么是index不滿足條件,要么是當前right和left的位置字符不等,都要在將right,left還原成刪一個狀態才能計算回文串長
return right - 1 - (left + 1) + 1;
}
/*計算最長回文子串的長度*/
int longestPald(char *string) {
int len = strlen(string);
if (string == NULL || len == 0)
assert("ERROR");
if (len == 1){
longest = 1;
start = 0;
return 1;
}
for (int i = 0; i < len; i++){//遍歷整個主串,以它的每一位為回文串的中心,計算最長的回文串
int tempLen = l2r(string, i);
if (tempLen > longest) {
longest = tempLen;
}
}
return 0;
}
int main(void)
{
char string[] = "abcdeedcbdac";
longestPald(string);
printf("\t%s的最長回文子串:\n", string);
printf("\t起始位置:%d 串長:%d\n", start, longest);
system("pause");
return 0;
}
結果:
2. 動態規劃法???
之前看到網上有很多用動態規劃法求解最長回文子串的,但是我看了之后覺得有問題。動態規劃法中是用二維矩陣保存回文串長,c[i][j]表示主串中s[i...j]是回文串,當前位置的c[i][j]需要依賴于c[i+1][j-1],但是有的地方c[i+1][j-1]是不知道的,反而覺得用遞歸來計算矩陣c會更好。不知道是我理解錯誤還是這個方法確實不對。如果有用動態規劃法求解出最長回文子串的,還請賜教~
3. Manacher算法
這是幾個方法中最為高效的方法,時間復雜度為O(n).Manacher算法也是利用回文串的對稱性,標記回文串的中間位,向兩邊遍歷。同樣是標記中間位,向兩邊遍歷,那它和中心擴展法有什么區別呢?
區別:中心擴展法的思想是以主串的每一個字符為中心,計算最長的回文子串,外層循環執行n次,內存循環至多2/n次;而Manacher的中心字符并不是這樣的,Manacher利用之前計算過的回文子串,巧妙的計算出新的中心點。但同時它也做出了一些折中的處理,比如說,要確定唯一的中心點,所以要擴展主串。
算法思想:Manacher采用從中間向兩邊遍歷得到最長回文子串的思想,將原來的主串進行擴展,這個算法嚴格要求對稱,只允許有一個中心點。eg:abc-- > #a#b#c#,這樣不管回文串是奇數位還是偶數位都都會變成奇數位的,滿足只有一個中心字符的要求。Manacher利用之前計算的回文子串,避免了一些重復的回文子串的計算。
輔助變量:
既然要利用之前求得信息,就需要記錄。
p[]:數組p保存的是主串中以某個字符為中心的最長回文子串的半徑,eg:p[i]存儲的是以str[i]為中心的最長回文串的半徑,這個半徑值是在擴展之后的字符串中。
mid:保存得到的回文串的中心點。
max:保存當前的回文串的影響范圍,也就是這個回文串的右邊界。
注:mid和max的值是由最長回文串計算得到的。
現在,我們來看一下str和p的關系,便于理解。s是在原來的字符串
接下來計算p[],這時要用到max和mid。先解釋一下最難懂的地方。利用之前計算的回文子串的信息計算當前的p[i],現則最小的值。
p[i] = (max - i) > p[j] ? p[j] : (max-i);
解釋:(以下解釋摘自另一篇博客)
1.當 mx - i > P[j] 的時候,以S[j]為中心的回文子串包含在以S[id]為中心的回文子串中,由于 i 和 j 對稱,以S[i]為中心的回文子串必然包含在以S[id]為中心的回文子串中,所以必有 P[i] = P[j],見下圖。
2.當 P[j] > mx - i 的時候,以S[j]為中心的回文子串不完全包含于以S[id]為中心的回文子串中,但是基于對稱性可知,下圖中兩個綠框所包圍的部分是相同的,也就是說以S[i]為中心的回文子串,其向右至少會擴張到mx的位置,也就是說 P[i] >= mx - i。至于mx之后的部分是否對稱,就只能一個一個匹配了。
接下來解釋算法為線性的原因:(算法中其實有兩層循環)
代碼:
代碼中有幾個需要注意的地方:
- 在pre函數中,擴展主串時,擴展串的第一個位置是'$',這是為了諸侯方便處理越界的問題。而字符串越界會出現在哪里呢?就是manacher中的為一個一個while循環那里。
- 注意重置longest和start時候的值,在介紹str和p的關系的時候已經提到過p[i]-1的意義,在設置longest和start時要考慮到這個關系。(longest是最長回文子串的長度,start是其在原串中的下標)。
- 理解p[i] = (max - i) > p[j] ? p[j] : (max-i);很重要,結合代碼中的注釋和上面的圖多理解。
int pre(char *string, char *strCopy) {
strCopy[0] = '$';
int j = 1;
for (int i = 0; i < strlen(string); i++) {
strCopy[j++] = '#';
strCopy[j++] = string[i];
}
strCopy[j] = '#';
return j + 1;//表示strCopy的長度
}
void manacher(char *str,int n) {
int p[MAXLEN];//數組p中保存字符串str中以某一點為中心點的最長回文子串的半徑
p[0] = 0;//p[0]對應str[0]-->$
//max存儲之前計算的回文子串的右邊界,mid保存當前的回文子串的中心,這兩個值都不一定是最長回文子串求得
int max = 0, mid = 1;
for (int i = 1; i < n; i++) {
if (max > i)
{
int j = 2 * mid - i;//j是字符串中i關于mid的對稱點
p[i] = (max - i) > p[j] ? p[j] : (max - i);//!!!
}
else {//否則max<i,說明i不包含在當前計算的回文串中,
//那么就不能用便捷方法來計算p[i],只能一個一個計算
p[i] = 1;//初始值為1
}
//基于當前以i為中心的回文串的半徑,計算下一個位置的字符是否滿足回文。這里會出現越界的問題!!!
while (str[i - p[i]] == str[i + p[i]]){
p[i]++;
}
if ((i + p[i]) > max){
max = i + p[i];//當前計算得到的回文串已經大于之前計算的邊界了,重置邊界
mid = i;
}
if (longest < p[i] - 1){//p[i]-1就是原串中以i為中心的回文串的長度
longest = p[i] - 1;
//在遇到最長回文子串包含第0個字符的時候,start計算得-1,所以這里要處理一下
if ((i - p[i] - 1) < 0)
start = 0;
else
start = i - p[i] - 1;
}
}
}
int main(void)
{
char string[] = "acab";//"abcdeedcbdac";
char strCopy[MAXLEN];
int len = pre(string, strCopy);
printf("原串:%s -->", string);
//輸出strCopy
for (int i = 0; i < len; i++){
printf("%c", strCopy[i]);
}
printf("\n");
manacher(strCopy,len);
printf("\t%s的最長回文子串:\n", string);
printf("\t起始位置:%d 串長:%d\n", start, longest);
system("pause");
return 0;
}
結果:
總結
好了,這次就到這里了。不足之處,歡迎指正。