1. 程式人生 > 其它 >LeetCode 0005 Longest Palindromic Substring

LeetCode 0005 Longest Palindromic Substring

原題傳送門

1. 題目描述

2. Solution 1: [TLE] Brute force

1、思路
兩層迴圈遍歷所有子串,判斷是否為迴文,保留最長的子串。
2、程式碼實現

/*
    Solution 1: Brute force
    兩層迴圈遍歷所有子串,判斷是否為迴文,保留最長的子串。
 */
public class Solution1 {

    public String longestPalindrome(String s) {
        String result = "";
        int maxLen = 0;
        int n = s.length();
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j <= n; j++) {
                String cur = s.substring(i, j);  // s[i, j), 故上面的j要取到等號
                if (isPalindromic(cur) && cur.length() > maxLen) {
                    result = cur;
                    maxLen = Math.max(maxLen, result.length());
                }
            }
        }
        return result;
    }

    private boolean isPalindromic(String s) {
        int n = s.length();
        for (int start = 0, end = n - 1; start < end; start++, end--)
            if (s.charAt(start) != s.charAt(end)) return false;
        return true;
    }
}

time complexity: 兩層迴圈O(n2),判斷是否迴文O(n),綜合為O(n3)
space complexity: O(1)

提交之後,TLE(Time Limit Exceeded)

3. Solution 2: LCS

3.1 子問題:最長公共子串(Longest Common Substring)

首先,引入問題最長公共子串(Longest Common Substring),原題見 NC127
1、分析: DP

   1) 狀態定義
      dp[i][j] 表示字串s1中第i個字元和s2中第j個字元所構成的最長公共子串。
             2) 初始狀態
                dp\[i][j] = {0}
                       3) 狀態轉移方程
                          dp\[i][j] = 0, if s1[i] != s2[j]
                          dp\[i][j] = dp\[i-1][j-1] + 1, if s1[i] == s2[j]
                          狀態轉移方程中出現了 i - 1,要求必須 i>=1,遍歷字串的時候,i從0開始。
                          故,對上面的公式做個替換 i -> i + 1,匯出
                          dp\[i+1][j+1] = dp\[i][j] + 1, if s1[i] == s2[j]
                          **​**

2、示例

String s1 = "1AB2345CD", s2 = "12345EF", expected = "2345";

手動模擬結果:

3、程式碼實現

/*
    DP
    1) 狀態定義
        dp[i][j] 表示字串s1中第i個字元和s2中第j個字元所構成的最長公共子串。
    2) 初始狀態
        dp[i][j] = {0}
    3) 狀態轉移方程
        dp[i][j] = 0, if s1[i] != s2[j]
        dp[i][j] = dp[i-1][j-1] + 1, if s1[i] == s2[j]
 */
public class Solution {

    @Test
    public void test1() {
        String s1 = "1AB2345CD", s2 = "12345EF", expected = "2345";
        assertEquals(expected, LCS(s1, s2));
    }

    /**
     * longest common substring
     *
     * @param s1 string字串 the string
     * @param s2 string字串 the string
     * @return string字串
     */
    public String LCS(String s1, String s2) {
        // write code here
        int maxLen = 0;
        int end = 0;
        int m = s1.length(), n = s2.length();
        int[][] dp = new int[m + 1][n + 1];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (s1.charAt(i) == s2.charAt(j)) {
                    dp[i + 1][j + 1] = dp[i][j] + 1;
                    if (dp[i + 1][j + 1] > maxLen) {
                        maxLen = dp[i + 1][j + 1];
                        end = i;
                    }
                }
            }
        }
        return s1.substring(end - maxLen + 1, end + 1);
    }
}
time complexity: 兩層迴圈,O(n^2)
space complexity: 二維陣列,O(n^2)

4、優化,DP使用一維陣列

/*
    DP使用一維陣列
 */
public class Solution1 {
    public String LCS(String s1, String s2) {
        int maxLen = 0;
        int end = 0;
        int m = s1.length(), n = s2.length();
        int[] dp = new int[n + 1];
        for (int i = 0; i < m; i++) {
            for (int j = n - 1; j >= 0; j--) {
                if (s1.charAt(i) == s2.charAt(j)) {
                    dp[j + 1] = dp[j] + 1;
                    if (dp[j + 1] > maxLen) {
                        maxLen = dp[j + 1];
                        end = i;
                    }
                } else dp[j + 1] = 0;
            }
        }
        return s1.substring(end - maxLen + 1, end + 1);
    }
}

time complexity: 兩層迴圈,O(n^2)
space complexity: 一維陣列,O(n )

3.2 用LCS解決當前問題

1、思路
根據迴文的定義,正序讀與倒序讀一樣。故,把輸入字串s做逆置為 rev,求LCS(s, rev)。

例1:

String s = "caba";
String rev = new StringBuilder(s).reverse().toString();  // "abac"
LCS(s, rev) = "aba";

例2:

String s = "abc435cba";
String rev = "abc534cba";
LCS(s, rev) = "abc" || "cba";

顯然這兩個字串都不是迴文串,所以求出最長公共子串後,並不一定是迴文串,還需要判斷該字串倒置前的下標和當前的字串下標是不是一致。
具體地,正確示例
a) dp過程

b) 判斷下標一致

在dp[3][4] = 3時,取得最長公共子串
此時,j = 2, rev[j] = 'a',為倒著讀終點。
由j推匯出順著讀的起點為: idxInSource = n - 1 - j = 4 - 1- 2 = 1。
由dp推匯出的順著讀終點,idxInSource + dp[3][4] - 1 = 1 + 3 - 1 = 3。
實際順著讀終點為 i = 3。
綜上,滿足要求。

錯誤示例
a) dp過程

b) 判斷下標一致

2、程式碼實現

public class Solution2 {

    public String longestPalindrome(String s) {
        if (s == null || s.length() == 0) return s;
        String rev = new StringBuilder(s).reverse().toString();
        int n = s.length();
        int[] dp = new int[n + 1];
        int maxLen = 0;
        int end = 0;
        for (int i = 0; i < n; i++) {
            for (int j = n - 1; j >= 0; j--) {
                if (s.charAt(i) == rev.charAt(j)) {
                    dp[j + 1] = dp[j] + 1;
                    if (dp[j + 1] > maxLen) {
                        int idxInSource = n - 1 - j;    // dp推匯出的順讀起點
                        // dp匯出的順讀終點                  實際順讀終點
                        if (idxInSource + dp[j + 1] - 1 == i) {  
                            maxLen = dp[j + 1];
                            end = i;
                        }
                    }
                } else dp[j + 1] = 0;
            }
        }
        return s.substring(end - maxLen + 1, end + 1);
    }
}

time complexity: O(n^2)
space complexity: O(n)

4. Solution 3

1、思路: 用DP優化暴力法
Dynamic Programming

  1. 狀態定義
    boolean dp[i][j] = s[i, j] is Palindromic
  2. 狀態初始條件
    dp[i][j] = true if len(s[i, j]) == 1 or j == i or j - i == 0;
    dp[i][j] = s[i] == s[j] if len(s[i, j]) == 2 or j == (i + 1) or j - i == 1;
  3. 狀態轉移方程
    dp[i][j] = s[i] == s[j] && dp[i+1][j-1]

判斷條件整合:
a. 若: j - i == 0 , 則dp[i][j] = (s[i] == s[j]) 恆為true, 1個字元自然迴文
b. 若: j - i == 1 , 則dp[i][j] = s[i] == s[j] 2個字元,首尾相等即可
c. 若: j - i == 2 , 則dp[i][j] = s[i] == s[j] 3個字元,首尾相等即可,中間有1個字元,不打緊
d. 若: j - i > 2 , 則dp[i][j] = s[i] == s[j] && dp[i + 1][j - 1] 4個字元(含),首尾相等,外加中間子串為迴文才行
a. b. c. 判斷條件整合 => s[i] == s[j] && (j - i <= 2)
與d. 合併後 判斷條件整合為一個 => s[i] == s[j] && ( (j - i <= 2) || dp[i + 1][j - 1] )
2、示例
input: s = "babad";
用j遍歷行,i遍歷列,只使用了下三角

3、程式碼實現

public class Solution3 {
    public String longestPalindrome(String s) {
        if (s == null || s.length() == 0) return s;
        String res = "";
        boolean[][] dp = new boolean[s.length()][s.length()];
        int max = 0;
        for (int j = 0; j < s.length(); j++) {
            for (int i = 0; i <= j; i++) {
                dp[i][j] = s.charAt(i) == s.charAt(j) &&
                        ((j - i <= 2) || dp[i + 1][j - 1]);
                if (dp[i][j] && (j - i + 1 > max)) {
                    max = j - i + 1;
                    res = s.substring(i, j + 1);  // [i, j+1)
                }
            } // end of inner for loop
        } // end of outer for loop
        return res;
    }
}

time: O(n^2)
space: O(n^2)
4、優化使用一維陣列

/*
    對Solution3的優化,使用一維陣列
 */
public class Solution4 {

    public String longestPalindrome(String s) {
        if (s == null || s.length() == 0) return s;
        String res = "";
        boolean[] dp = new boolean[s.length()];
        int n = s.length();
        for (int i = n - 1; i >= 0; i--) {
            for (int j = n - 1; j >= i; j--) {
                dp[j] = s.charAt(i) == s.charAt(j) &&
                        ((j - i) < 3 || dp[j - 1]);
                if (dp[j] && j - i + 1 > res.length())
                    res = s.substring(i, j + 1); // [i, j+1)
            }
        }
        return res;
    }
}

time: O(n^2)
space: O(n)

5. Solution 4

1、思路:中心擴充套件
迴文一定是對稱的,所以可以迴圈選擇一箇中心,進行左右擴充套件,判斷左右字元是否相等。
因為字串長度可能為奇數,也可能是偶數,所以中心可以是字元,也可以是兩個字元間隙。
設字串長度為n,則間隙為n-1,可能的中心有 n+(n-1) = 2n - 1個。

2、程式碼實現

public class Solution5 {
    public String longestPalindrome(String s) {
        if (s == null || s.length() == 0) return null;
        int start = 0, end = 0;
        for (int i = 0; i < s.length(); i++) {
            int n = Math.max(expandAroundCenter(s, i, i),  // 以i為中心擴充套件
                    expandAroundCenter(s, i, i + 1)); // 以i後面的間隔為中心擴充套件
            if (n > end - start) {
                start = i - (n - 1) / 2;
                end = i + n / 2;
            }
        }
        return s.substring(start, end + 1);
    }

    private int expandAroundCenter(String s, int left, int right) {
        int l = left, r = right;
        while (l >= 0 && r < s.length() &&
                s.charAt(l) == s.charAt(r)) {
            l--;
            r++;
        }
        return r - l - 1;
    }
}

time complexity: O(n^2)
space complexity: O(1)

6. Solution 5: Manacher's Algorithm 馬拉車演算法

馬拉車演算法 Manacher‘s Algorithm 是用來查詢一個字串的最長迴文子串的線性方法,由一個叫Manacher的人在1975年發明的,這個方法的最大貢獻是在於將時間複雜度提升到了線性。

1、思路分析

  • Step 1: 預處理

由於迴文分為偶迴文(如,bccb)和奇迴文(如,bcacb),而在處理奇偶問題上會比較繁瑣,這裡使用一個技巧
a.) 在字串首尾及每個字元間都插入一個"#",這樣可以使得原先的奇偶迴文都變為奇迴文;
b.) 接著在首尾兩端各插入"$"和"^",這樣中心擴充套件尋找回文的時候會自動退出迴圈,不需要每次判斷是否越界;

T: 處理後的陣列,P: 從中心擴充套件的長度
首先,用一個數組P儲存從重心擴充套件的字串最多個數,而它剛好也是去掉"#"的原字串的總長度。如,index=6的地方,P[6]=5,所以它是從左邊擴充套件5個字元,相應的右邊也是擴充套件5個字元,也就是"#c#b#c#b#c#"。而去掉#恢復到原來的字串,變成"cbcbc",它的長度剛好也就是5。

  • Step 2: 求原始字串下標

用P的下標i減去P[i],再除以2,就是原字串的開頭下標了。
如,P[6] = 5,也就是迴文串的最大長度是5,對應的下標是6,所以原字串的開頭下標是(6-5) / 2 = 0。所以,只需要返回原字串的第0到第(5-1)位就可以了。

Step 3: 求每個P[i]
用C表示迴文串的中心,用R表示迴文串的右邊半徑,所以R=C+P[C]。C和R所對應的迴文串是當前迴圈中R最靠右的迴文串。
考慮求P[i],用i_m表示當前需要求的第i個字元關於C對應的下標。

現在要求P[i],如果是用中心擴充套件法,那就向兩邊擴充套件比對就行了。其實可以利用迴文串C的對稱性。i關於C的對稱點是i_m,P[i_m]=3,所以P[i] 也等於 3。
但是有三種情況將會造成直接賦值為P[i_m]是不正確的,下邊一一討論。

case 1: 超出了R

當要求P[i]的時候,P[i_m]=7,而此時P[i]並不等於7,為什麼呢?因為我們從i開始往後數7個,等於22,已經超過了最右的R,此時不能利用對稱性了,但我們一定可以擴充套件到R的,所以P[i]至少等於R-i=20-15=5,會不會更大呢,我們只需要比較T[R+1]和T[R+1]關於i的對稱點就行了,就像中心擴充套件法一樣一個個擴充套件。

case 2: P[i_m]遇到了原字串的左邊界

此時P[i_m]=1,但是P[i]賦值成1是不正確的,出現這種情況的原因是P[i_m]在擴充套件的時候首先是"#"=="#",之後遇到了"^"和另一個字元比較,一就是到了邊界,才終止迴圈的。而P[i]並沒有遇到邊界,所以我們可以通過中心擴充套件法一步一步向兩邊擴充套件就行了。

case 3: i等於R
此時,先把P[i]賦值為0,然後通過中心擴充套件法一步一步擴充套件。

  • Step 3: 考慮C和R的更新

就這樣一步一步的求出每個P[i],當求出的P[i]的右邊界大於當前的R時,就需要更新C和R為當前的迴文串了。因為必須保證i在R裡面,所以一旦有更右邊界的R就要更新R。

此時的P[i]求出了將會是3,P[i]對應的右邊界將是10+3=13,所以大於當前的R,需要把C更新成i的值,也就是10,R更新成13。

2、程式碼實現

package Q0099.Q0005LongestPalindromicSubstring;

public class Solution6 {
    public String preProcess(String s) {
        int n = s.length();
        if (n == 0) return "^$";
        StringBuilder ret = new StringBuilder("^");
        for (int i = 0; i < n; i++)
            ret.append("#").append(s.charAt(i));
        ret.append("#$");
        return ret.toString();
    }

    public 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_m = 2 * C - i;
            if (R > i) {
                P[i] = Math.min(R - i, P[i_m]);
            } else {
                P[i] = 0;   // 等於R的情況
            }

            // 碰到之前的三種情況,需要利用中心擴充套件法
            while (T.charAt(i + 1 + P[i]) == T.charAt(i - 1 - P[i]))
                P[i]++;

            // 判斷是否需要更新R
            if (i + P[i] > R) {
                C = i;
                R = i + P[i];
            }
        }

        // 找出P的最大值
        int maxLen = 0;
        int centerIndex = 0;
        for (int i = 1; i < n - 1; i++) {
            if (P[i] > maxLen) {
                maxLen = P[i];
                centerIndex = i;
            }
        }
        int start = (centerIndex - maxLen) / 2;     // 最開始講的求原字串下標
        return s.substring(start, start + maxLen);
    }
}