最長回文子串


給定一個字符串 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); }
復制代碼
 

求str最長回文子序列是求這個原字符串和它反轉字符串的最長公共子序列。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM