Longest Palindromic Substring
Given a string S, find the longest palindromic substring in S. You may assume that the maximum length of S is 1000, and there exists one unique longest palindromic substring.
求最長迴文字符串,最直觀的做法是分別檢測字符串S的所有子字符串是否爲迴文字符串。長度爲n的字符串S,其子字符串的總數目爲n+(n-1)+……+3+2+1 = n*(n+1)/2
判斷子字符串是否爲迴文字符串算法複雜度O(n)就可以實現,因此總的來說此算法複雜度爲O(n^3)。
這種直觀的算法複雜度很高,在此就不採用了。這種直觀的算法思路很明瞭,就是從外向裏去檢測的,而回文字符串左右對稱,必定有一個對稱點。我們如果能將這個對稱點找出,然後再左右擴展,是不是就可以找出最長的迴文子字符串了呢?
舉個例子來說吧,S=“abcdeedcba”這個字符串是迴文字符串,當我們循環到ee處時,我們發現我們找到一個對稱點了,我們就可以左右擴展:判斷ee左右各添加一個字符,是否仍是迴文字符串,如此循環下去。
這個時候有可能會有這種字符串 S=“abcdefedcba”,這個字符串也是迴文字符串的,對稱點是 f 字符。既然有這種情況,我們就得將每一個字符都當做對稱點然後左右擴展了。
總體來說,這是有兩種情況的,我們需要分別對待,分別考慮。主要分兩種假設來分別考慮:(1)、迴文字符串長度爲偶數;(2)、迴文字符串長度爲奇數。之後再將兩者的最長挑出來。這種算法的複雜度爲O(n^2)
class Solution
{
public:
string longestPalindrome(string s)
{
int len = s.length();
string palindorme;
int maxlen = 0;
int templen = 0;
int i,j;
//迴文字符串長度爲偶數
for(i=0; i<len-1; i++)
{
if(s[i]==s[i+1])
{
j = 1;
while(i-j>=0 && i+j+1<len && s[i-j]==s[i+j+1])
++j;
if(2*j > maxlen)
{
maxlen = 2*j;
palindorme = s.substr(i-j+1,maxlen);
}
}
}
//迴文字符串長度爲奇數
for(i=0; i<len; i++)
{
j = 0;
while(i-j>=0 && i+j<=len-1 && s[i-j]==s[i+j])
++j;
templen = 2*j-1;
if(templen > maxlen)
{
maxlen = templen;
palindorme = s.substr(i-j+1,maxlen);
}
}
return palindorme;
}
};
經過搜索後,發現這種算法還並非是最優算法,還有一種複雜度爲O(n)的算法——manacher算法
http://blog.csdn.net/xingyeyongheng/article/details/9310555 這篇博客介紹manacher算法邏輯比較清晰,下面就以該博客爲主體,對其做些許修改。
算法介紹如下:
定義數組 p[i] 表示以i爲中心的(包含 i 這個字符)迴文字符串半徑長度
將字符串s從前掃到後 for(int i=0; i<s.length(); i++) 來計算 p[i],則最大的p[i]就是最長迴文串長度,則問題是如何去求 p[i] ?
由於s是從前掃到後的,所以需要計算p[i]時一定已經計算好了p[1]……p[i-1]
假設現在掃描到了i+k這個位置,現在需要計算p[i+k]
定義 maxlen 是 i+k 位置前所有迴文串中能延伸到的最右端的位置,即 maxlen=p[i]+i; //p[i]+i 表示最大的。
共有三種情況需要討論:(下面圖中黑色是 i 的迴文字符串範圍,藍色是 i-k 的迴文字符串範圍)
第一種情況:i+k這個位置不在前面的任何迴文串中,即 i+k>maxlen,則初始化p[i+k]=1;//本身是迴文串
然後 p[i+k] 左右延伸,即 while(s[i+k+p[i+k]] == s[i+k-p[i+k]]) ++p[i+k]
爲了防止越界我們加上邊界判斷條件while(i+k+p[i+k]<len && i+k-p[i+k]>=0 && s[i+k+p[i+k]] == s[i+k-p[i+k]])++p[i+k];
很多博客都用字符串左右添加特殊符號*或者@的方式,這裏不採用這種方法,而是直接加上邊界條件判斷。
第一種情況圖示:
或者,如下情況
第二種情況:i+k這個位置剛好被前面以位置i爲中心的迴文串包含,即maxlen>i+k,這種情況與情況一是相同的,還是需要重新判斷 i+k 的迴文字符串長度。
第三種情況:i+k這個位置被前面以位置i爲中心的迴文串包含,即maxlen>i+k,這樣的話 p[i+k] 就不是從1開始。由於迴文串的性質,可知i+k這個位置關於i與i-k對稱。
i-k迴文字符串範圍有一部分在 i 的迴文字符串之外,如下圖靛色左端在黑色左端之外。
這種情況 p[i+k] = p[i]-k ; //maxlen-(i+k)=p[i]-k,即半徑爲橙色部分
那麼p[i+k]有沒有可能更長呢?答案是不可能的。證明如下:
如上圖,假設p[i+k]可以延長,增加紫色部分,我們將左右分別稱爲c和d,由i+k的迴文字符串特性可知c=d,
根據 i 的迴文字符串特性,我們可以知道與c對應的應該會有b,c=b
再根據i-k的迴文字符串特性b和紫色部分a對稱,最終得到a=b
總結上述a=b=c=d,則 i 的迴文字符串長度就不會僅僅是黑色長度了,而是黑色+左右兩端紫色部分了,這與已經求出的 i 的迴文字符串長度僅爲黑色長度矛盾,所以假設不成立,則 p[i+k]=p[i]-k; //maxlen-(i+k)
第四種情況:i+k這個位置被前面以位置i爲中心的迴文串包含,即maxlen>i+k,這樣的話 p[i+k] 就不是從1開始。由於迴文串的性質,可知i+k這個位置關於i與i-k對稱。
i-k 迴文字符串全部落在 i 的迴文字符串內,如下圖,此時p[i+k] = p[i-k]。
p[i+k]還有沒有可能更長呢?我們仍然採用反證法來證明
假設p[i+k]可以延長並增加紫色部分c和d,c=d
由 i 的迴文字符串對稱特性,a=d ,b=c,又c=d 故a=b
這與前面已經計算出 i-k 的迴文字符串長度僅爲靛色長度矛盾,故假設不成立。
將上面四種情況綜合在一起就是:
p[i+k] = min(p[i-k], p[i]-k); //k=(i+k)-i
while(i+k+p[i+k]<len && i+k-p[i+k]>=0 && s[i+k+p[i+k]] == s[i+k-p[i+k]])++p[i+k];
根據上面的算法我們知道p[i] 是以 i 爲中心的迴文字符串長度,那麼對於aa這種偶數長度的迴文字符串,其對稱中心 i 並不是一個字符,如何將偶數長度的迴文字符串和奇數長度的迴文字符串統一起來呢?manacher算法提供了一個非常巧妙的思路,那就是將字符串所有字符左右都插入一個 '#' 字符。如 aba 則變成 #a#b#a#,aa 則變成#a#a#,如此一來回文字符串中心均爲一個字符,而不會爲空!
下面來看下代碼:
class Solution
{
public:
string preprocess(string s) //將字符串中插入'#'字符
{
int len = s.length();
string str(2*len+1, '#');
for(int i=0; i<len; i++)
str[2*i+1] = s[i];
return str;
}
string longestPalindrome(string s)
{
string str = preprocess(s);
string palindrome;
int len = str.length();
int i,id = 0;
int maxlen = 0; //迴文字符串最長半徑
int index = 0; //迴文字符串中心下標
int * p = new int[len];
p[0] = 0;
for(i=0; i<len; i++)
{
if(p[i]+id>i)
p[i]=min(p[2*id-i], p[id]+id-i);
else
p[i]=1;
while(i-p[i]>=0 && i+p[i]<len && str[i-p[i]]==str[i+p[i]])
++p[i];
if(id+p[id] < i+p[i])
id = i;
if(maxlen<p[i])
{
maxlen = p[i];
index = i;
}
}
delete[] p;
palindrome = s.substr((index-maxlen+1)/2,maxlen-1);
return palindrome;
}
};