給定一個字符串 s,找到 s 中最長的回文子串。你可以假設 s 的最大長度為1000。
示例 1:
輸入: "babad" 輸出: "bab" 注意: "aba"也是一個有效答案。
示例 2:
輸入: "cbbd" 輸出: "bb"
自己的思路:求一個字符串的最長回文子串,我們可以將以每個字符為首的子串都遍歷一遍,判斷是否為回文,如果是回文,再判斷最大長度的回文子串。算法簡單,但是算法復雜度太高,O(n^3)
string longestPalindrome(string s) { if(s.empty()) return ""; if(s.size()==1) return s; int start=0,maxlength=1;//記錄最大回文子串的起始位置以及長度 for(int i=0;i<s.size();i++) for(int j=i+1;j<s.size();j++)//從當前位置的下一個開始算 { int temp1,temp2; for(temp1=i,temp2=j;temp1<temp2;temp1++,temp2--) { if(s[temp1]!=s[temp2]) break; } if(temp1>=temp2 && j-i+1>maxlength)//這里要注意條件為temp1>=temp2,因為如果是偶數個字符,相鄰的兩個經上一步會出現大於的情況 { maxlength = j-i+1; start=i; } } return s.substr(start,maxlength);//利用string中的substr函數來返回相應的子串,第一個參數是起始位置,第二個參數是字符個數 }
很明顯上述的算法復雜度太高,應該有更加快捷的做法來處理。下面介紹兩種方法
(1)DP
動態規划的方法,我會在下一篇單獨來介紹,這里只說明此題的DP代碼
對於字符串str,假設dp[i,j]=1表示str[i...j]是回文子串,那個必定存在dp[i+1,j-1]=1。這樣最長回文子串就能分解成一系列子問題,可以利用動態規划求解了。首先構造狀態轉移方程
上面的狀態轉移方程表示,當str[i]=str[j]時,如果str[i+1...j-1]是回文串,則str[i...j]也是回文串;如果str[i+1...j-1]不是回文串,則str[i...j]不是回文串。
初始狀態
- dp[i][i]=1
- dp[i][i+1]=1 if str[i]==str[i+1]
上式的意義是單個字符,兩個相同字符都是回文串。
string longestPalindrome(string s) { if (s.empty()) return ""; int len = s.size(); if (len == 1)return s; int longest = 1; int start=0; vector<vector<int> > dp(len,vector<int>(len)); for (int i = 0; i < len; i++) { dp[i][i] = 1; if(i<len-1) { if (s[i] == s[i + 1]) { dp[i][i + 1] = 1; start=i; longest=2; } } } for (int l = 3; l <= len; l++)//子串長度 { for (int i = 0; i+l-1 < len; i++)//枚舉子串的起始點 { int j=l+i-1;//終點 if (s[i] == s[j] && dp[i+1][j-1]==1) { dp[i][j] = 1; start=i; longest = l; } } } return s.substr(start,longest); }
這里我們需要用一個二維數組dp來作為備忘錄,記錄子問題的結果,以便重復的計算。這也是動態規划的精髓所在。不過這種做法的算法復雜度也是O(n^2)
(2)Manacher法
這是一個專門用作處理最長回文子串的方法,思想很巧妙,比較難以理解,這里直接借用了別人的講解方法。其實主要思想是,把給定的字符串的每一個字母當做中心,向兩邊擴展,這樣來找最長的子回文串,這個叫中心擴展法,但是這個方法還要考慮到處理abba這種偶數個字符的回文串。Manacher法將所有的字符串全部變成奇數個字符。
Manacher算法原理與實現
下面介紹Manacher算法的原理與步驟。
首先,Manacher算法提供了一種巧妙地辦法,將長度為奇數的回文串和長度為偶數的回文串一起考慮,具體做法是,在原字符串的每個相鄰兩個字符中間插入一個分隔符,同時在首尾也要添加一個分隔符,分隔符的要求是不在原串中出現,一般情況下可以用#號。下面舉一個例子:
(1)Len數組簡介與性質
Manacher算法用一個輔助數組Len[i]表示以字符T[i]為中心的最長回文字串的最右字符到T[i]的長度,比如以T[i]為中心的最長回文字串是T[l,r],那么Len[i]=r-i+1。
對於上面的例子,可以得出Len[i]數組為:
Len數組有一個性質,那就是Len[i]-1就是該回文子串在原字符串S中的長度,至於證明,首先在轉換得到的字符串T中,所有的回文字串的長度都為奇數,那么對於以T[i]為中心的最長回文字串,其長度就為2*Len[i]-1,經過觀察可知,T中所有的回文子串,其中分隔符的數量一定比其他字符的數量多1,也就是有Len[i]個分隔符,剩下Len[i]-1個字符來自原字符串,所以該回文串在原字符串中的長度就為Len[i]-1。
有了這個性質,那么原問題就轉化為求所有的Len[i]。下面介紹如何在線性時間復雜度內求出所有的Len。
(2)Len數組的計算
首先從左往右依次計算Len[i],當計算Len[i]時,Len[j](0<=j<i)已經計算完畢。設P為之前計算中最長回文子串的右端點的最大值,並且設取得這個最大值的位置為po,分兩種情況:
第一種情況:i<=P
那么找到i相對於po的對稱位置,設為j,那么如果Len[j]<P-i,如下圖:
那么說明以j為中心的回文串一定在以po為中心的回文串的內部,且j和i關於位置po對稱,由回文串的定義可知,一個回文串反過來還是一個回文串,所以以i為中心的回文串的長度至少和以j為中心的回文串一樣,即Len[i]>=Len[j]。因為Len[j]<P-i,所以說i+Len[j]<P。由對稱性可知Len[i]=Len[j]。
如果Len[j]>=P-i,由對稱性,說明以i為中心的回文串可能會延伸到P之外,而大於P的部分我們還沒有進行匹配,所以要從P+1位置開始一個一個進行匹配,直到發生失配,從而更新P和對應的po以及Len[i]。
第二種情況: i>P
如果i比P還要大,說明對於中點為i的回文串還一點都沒有匹配,這個時候,就只能老老實實地一個一個匹配了,匹配完成后要更新P的位置和對應的po以及Len[i]。
2.時間復雜度分析
Manacher算法的時間復雜度分析和Z算法類似,因為算法只有遇到還沒有匹配的位置時才進行匹配,已經匹配過的位置不再進行匹配,所以對於T字符串中的每一個位置,只進行一次匹配,所以Manacher算法的總體時間復雜度為O(n),其中n為T字符串的長度,由於T的長度事實上是S的兩倍,所以時間復雜度依然是線性的。
下面是算法的實現,注意,為了避免更新P的時候導致越界,我們在字符串T的前增加一個特殊字符,比如說‘$’,所以算法中字符串是從1開始的。、
string longestPalindrome(string s) { string manaStr = "$#"; for (int i=0;i<s.size();i++) //首先構造出新的字符串
{ manaStr += s[i]; manaStr += '#'; } vector<int> rd(manaStr.size(), 0);//用一個輔助數組來記錄最大的回文串長度,注意這里記錄的是新串的長度,原串的長度要減去1 int pos = 0, mx = 0; int start = 0, maxLen = 0; for (int i = 1; i < manaStr.size(); i++) { rd[i] = i < mx ? min(rd[2 * pos - i], mx - i) : 1; while (i+rd[i]<manaStr.size() && i-rd[i]>0 && manaStr[i + rd[i]] == manaStr[i - rd[i]])//這里要注意數組越界的判斷,源代碼沒有注意,release下沒有報錯 rd[i]++; if (i + rd[i] > mx) //如果新計算的最右側端點大於mx,則更新pos和mx { pos = i; mx = i + rd[i]; } if (rd[i] - 1 > maxLen)
{ start = (i - rd[i]) / 2; maxLen = rd[i] - 1; } } return s.substr(start, maxLen); }