1. 程式人生 > >leetcode 5: 最長迴文子串

leetcode 5: 最長迴文子串

pre

這條題目的常見時間複雜度為O(N^2),在看解答分析的時候,有一位博主給出了時間複雜度為O(N)的演算法,這裡我將著重討論最後號稱O(N)的演算法。

題目

給定一個字串 s,找到 s 中最長的迴文子串。你可以假設 s 的最大長度為1000。

示例 1:

輸入: “babad”
輸出: “bab”
注意: “aba”也是一個有效答案。
示例 2:

輸入: “cbbd”
輸出: “bb”

詳細題目請直接訪問原題[^題目]

分析

相關的思路在原題的解題報告中已經討論到了[^解題報告],諸位可以直接閱讀解題報告

方案1

首先考慮到迴文的特性,那麼第一個想法就是將字串反轉過來,找出兩者的公共字串

"eabac" 反轉過來是 "cabae",最長公共字串是"aba" 也就是答案了

但是這種方法存在問題,請考慮如下情況

"abac" 反轉過來是"caba"

也就是在找到公共字串的時候我們需要確認兩個字串是否在同一個位置。

到這裡,我們可以計算演算法的時間複雜度,首先掃描一遍的效率是O(n^2) ,針對每個位置需要確認最長公共字串,然後判斷是否對應同一個字串,最後判斷是否是最長,也就是實際的效率接近是O(2 * n^2)
空間複雜度為O(n^2),可以優化為O(2*n)

方案2

考慮迴文的中心點,對於原來的字串可能存在2n*1箇中心點(考慮如下情況)

"ababa"
"abba" 中心點在"bb"中間

針對每個中心點,依次掃描左右兩邊,從而確認最大值
時間複雜度為O( (2*n - 1) *n ), 空間複雜度為O(1)

方案3(或者可以被稱為優化過之後的方案2)

思路來源於論壇大佬[^大佬思路]
我們這裡基於方案2談論方案三,我們可以通過方案2的思路計算前k個元素的最長迴文字串,這裡我們可以快取一個包含最遠範圍的迴文字串邊界

初始字串為 
a b a b a b c

掃描過前3a b a b a b c
0 1 2 ?
L   C   R

考慮上面這個案例,當我們掃描到第4個的時候 s[3] == ‘b’, 我們按照原來的思路,去考慮左右兩邊的情況,s[2] == s[4],,接下來掃描下一位s[1] ?= s[5], 但是我們發現當前掃描的索引(記為i),被包含在(C,R]這個範圍內, 這樣我們考慮迴文的特性,座標i的特徵應該和 座標為 C - (i - C)的有關係,如果C-(i-C) 位置在 [L,R] 之間存在迴文字串,那麼座標i也應該具有相應的位置(注意如果映象座標具有的迴文字串邊界超出了快取的邊界,那麼這邊我們不能考慮超出的部分)

s[3]的映象位置為s[1]
s[1]的迴文範圍為 s[0:2],該範圍在[0:4],所以對應的s[2:4]也應該是迴文的
所以s[3]初始檢查的座標應該為s[1] ?= s[5]

掃描過前5個時
a b a b a b c
0 1 2 2 1 ?
  L   C   R
s[5]的映象位置為s[1],s[1]在快取的迴文範圍內
s[1]的迴文範圍為s[0:2],超出快取的範圍,進行裁剪之後為s[1:1]
映象位置為s[5:5]
所以s[5]初始檢查的情況為s[4] ?= s[6]

基於這個優化,我們可以減少一部分不必要的掃描工作,但是至少直觀上,這個演算法並沒有能夠將原來的演算法效率提升一個數量級,在部落格的評論部分也討論這個問題,這個演算法在劣勢情況下依舊是O(N^2), 理論上優於方案2,空間上覆雜度為O(n),遜色於方案2

程式碼

方案1

class Solution {
   public String longestPalindrome(String s) {
        char[] cArray = s.toCharArray();

        int[][] lengthArray = new int[s.length()][s.length()];

        int start =-1;
        int len = -1;

        for(int i = 0 ; i < s.length(); i++){
            for(int j = 0 ; j < s.length();j++){
                if(cArray[i] == cArray[s.length()-j-1]){
                    int pre = 0;
                    if (i>0 &&j>0) {
                        pre = lengthArray[j-1][i-1];
                    }

                    lengthArray[j][i] = pre+1;
                    //檢查是否構成迴文
                    if((s.length() - j-1) == (i - lengthArray[j][i] +1)){
                        if(lengthArray[j][i] > len){
                            len = lengthArray[j][i];
                            start = s.length() - j-1;
                        }
                    }
                }
            }
        }
        String maxStr ="";
        if(len > 0)
            maxStr = s.substring(start,start+len);
        return maxStr;
    }
}

方案2

class Solution {
    public String expendString (String s ){
        StringBuffer T = new StringBuffer();
        for(int i= 0; i < s.length();i++){
            T .append("#");
            T.append(s.charAt(i));
        }
        T .append("#");
        return T.toString();
    }
    public int findMaxLength(String T ,int middleIndex ,int[] cacheArray ){
        if(cacheArray[middleIndex] > 0){
            return cacheArray[middleIndex];
        }
        int len = 1;
        while(middleIndex-len >= 0 && middleIndex+len < T.length()){
            if(T.charAt(middleIndex-len) == T.charAt(middleIndex+len)){
                //動態朝外擴張,減少重複計算
                len ++;
                continue;
            }else{
               break;
            }
        }
        cacheArray[middleIndex] = len -1;
        return cacheArray[middleIndex];
    }
    public String longestPalindrome(String s) {
        String T = expendString(s);
        int[] cacheArray = new int[T.length()];
        //進行一遍計算
        for(int i = 0 ; i < T.length() ; i++){
            findMaxLength(T,i,cacheArray);
        }

        int maxMiddleIndex = -1;
        int size = -1;
        for(int i = 0 ; i < T.length();i++){
            if(cacheArray[i] > size){
                size = cacheArray[i];
                maxMiddleIndex = i;
            }
        }
        StringBuffer maxStr = new StringBuffer();

        for(int i = maxMiddleIndex - size ; i < maxMiddleIndex +size ; i++){
            if(i%2 == 1){
                maxStr.append(T.charAt(i));
            }
        }
        return maxStr.toString();
    }
}

方案3

// Transform S into T.
// For example, S = "abba", T = "^#a#b#b#a#$".
// ^ and $ signs are sentinels appended to each end to avoid bounds checking
string preProcess(string s) {
  int n = s.length();
  if (n == 0) return "^$";
  string ret = "^";
  for (int i = 0; i < n; i++)
    ret += "#" + s.substr(i, 1);

  ret += "#$";
  return ret;
}

string longestPalindrome(string s) {
  string T = preProcess(s);
  int n = T.length();
  int *P = new int[n];
  int C = 0, R = 0;
  for (int i = 1; i < n-1; i++) {
    int i_mirror = 2*C-i; // equals to i' = C - (i-C)

    P[i] = (R > i) ? min(R-i, P[i_mirror]) : 0;

    // Attempt to expand palindrome centered at i
    while (T[i + 1 + P[i]] == T[i - 1 - P[i]])
      P[i]++;

    // If palindrome centered at i expand past R,
    // adjust center based on expanded palindrome.
    if (i + P[i] > R) {
      C = i;
      R = i + P[i];
    }
  }

  // Find the maximum element in P.
  int maxLen = 0;
  int centerIndex = 0;
  for (int i = 1; i < n-1; i++) {
    if (P[i] > maxLen) {
      maxLen = P[i];
      centerIndex = i;
    }
  }
  delete[] P;

  return s.substr((centerIndex - 1 - maxLen)/2, maxLen);
}

summary

這個優化思路是題目相關的,具有一定的參考價值,推廣價值低。