天天看點

LeetCode5. 最長回文子串題目描述暴力動态規劃方法三:中心擴散法馬拉車算法,自行參考下面兩篇文章

題目描述

給定一個字元串 s,找到 s 中最長的回文子串。你可以假設 s 的最大長度為 1000。
示例 1:
輸入: "babad"
輸出: "bab"
注意: "aba" 也是一個有效答案。
示例 2:
輸入: "cbbd"
輸出: "bb"
           

暴力

根據回文子串的定義,枚舉所有長度大于等于 2 的子串,依次判斷它們是否是回文;

在具體實作時,可以隻針對大于“目前得到的最長回文子串長度”的子串進行“回文驗證”;

在記錄最長回文子串的時候,可以隻記錄“目前子串的起始位置”和“子串長度”,不必做截取。這一步我們放在後面的方法中實作。

public class Solution {

    public String longestPalindrome(String s) {
        int len = s.length();
        if (len < 2) {
            return s;
        }

        int maxLen = 1;
        int begin = 0;
        // s.charAt(i) 每次都會檢查數組下标越界,是以先轉換成字元數組,小細節
        char[] charArray = s.toCharArray();

        // 枚舉所有長度大于 1 的子串 charArray[i..j]
        for (int i = 0; i < len - 1; i++) {
            for (int j = i + 1; j < len; j++) {
            	//j - i + 1 > maxLen這個用的很精髓。利用&&運算的短路特征,寫在前面會優化許多。
                if (j - i + 1 > maxLen && validPalindromic(charArray, i, j)) {
                    maxLen = j - i + 1;
                    begin = i;
                }
            }
        }
        return s.substring(begin, begin + maxLen);
    }

    /**
     * 驗證子串 s[left..right] 是否為回文串
     */
    private boolean validPalindromic(char[] charArray, int left, int right) {
        while (left < right) {
            if (charArray[left] != charArray[right]) {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }
}
           

動态規劃

第 1 步:定義狀态

dp[i][j] 表示子串 s[i…j] 是否為回文子串,這裡子串 s[i…j] 定義為左閉右閉區間,可以取到 s[i] 和 s[j]。

第二步,列出狀态轉移方程。

依然從回文串的定義展開讨論:

如果一個字元串的頭尾兩個字元都不相等,那麼這個字元串一定不是回文串;

如果一個字元串的頭尾兩個字元相等,才有必要繼續判斷下去。

如果裡面的子串是回文,整體就是回文串;

如果裡面的子串不是回文串,整體就不是回文串。

dp[i][j] = (s[i] == s[j]) && dp[i + 1][j - 1]

「動态規劃」事實上是在填一張二維表格,由于構成子串,是以 i 和 j 的關系是 i <= j ,是以,隻需要填這張表格對角線以上的部分。

求 長度為 1 和長度為 2 的 dp(i,j) 時不能用上邊的公式,因為我們代入公式後會遇到 dp[i][j] 中 i > j 的情況,比如求 P[1][2] 的話,我們需要知道 dp[1+1][2-1]=dp[2][1] ,而 dp[2][1] 代表着 S[2,1] 是不是回文串,顯然是不對的,是以我們需要單獨判斷。

public class Solution {
    public String longestPalindrome(String s) {
        // 特判
        int len = s.length();
        if (len < 2) {
            return s;
        }
        int maxLen = 1;
        int begin = 0;
        // dp[i][j] 表示 s[i, j] 是否是回文串
        boolean[][] dp = new boolean[len][len];
        char[] charArray = s.toCharArray();
        //初始化。
        for (int i = 0; i < len; i++) {
            dp[i][i] = true;
        }
        for (int j = 1; j < len; j++) {
            for (int i = 0; i < j; i++) {
            	//下面if else可以用 || 運算來代替,增加可讀性。因為 || 有短路功能。
            	//dp[i][j] = charArray[i] == charArray[j] && (j - i < 3 || dp[i + 1][j - 1]);
                if (charArray[i] != charArray[j]) {
                    dp[i][j] = false;
                } else if (j - i < 3) {
                    //長度為3,中間不用考慮,而且相等,那麼就是true。
                    dp[i][j] = true;
                } else{
                    dp[i][j] = dp[i + 1][j - 1];
                }

                // 隻要 dp[i][j] == true 成立,就表示子串 s[i..j] 是回文,此時記錄回文長度和起始位置
                if (dp[i][j] && j - i + 1 > maxLen) {
                    maxLen = j - i + 1;
                    begin = i;
                }
            }
        }
        return s.substring(begin, begin + maxLen);
    }
}
           

下面分别展示了錯誤的填表順序和正确的填表順序,以便大家了解動态規劃要滿足「無後效性」的意思。

說明:表格中的數字表示「填表順序」,從 1 開始。表格外的箭頭和數字也表示「填表順序」,與表格中的數字含義一緻。

LeetCode5. 最長回文子串題目描述暴力動态規劃方法三:中心擴散法馬拉車算法,自行參考下面兩篇文章
LeetCode5. 最長回文子串題目描述暴力動态規劃方法三:中心擴散法馬拉車算法,自行參考下面兩篇文章

方法三:中心擴散法

暴力法采用雙指針兩邊夾,驗證是否是回文子串。

除了枚舉字元串的左右邊界以外,比較容易想到的是枚舉可能出現的回文子串的“中心位置”,從“中心位置”嘗試盡可能擴散出去,得到一個回文串。

是以中心擴散法的思路是:周遊每一個索引,以這個索引為中心,利用“回文串”中心對稱的特點,往兩邊擴散,看最多能擴散多遠。

枚舉“中心位置”時間複雜度為 O(N),從“中心位置”擴散得到“回文子串”的時間複雜度為 O(N),是以時間複雜度可以降到 O(N^2)。

在這裡要注意一個細節:回文串在長度為奇數和偶數的時候,“回文中心”的形式是不一樣的。

奇數回文串的“中心”是一個具體的字元,例如:回文串 “aba” 的中心是字元 “b”;

偶數回文串的“中心”是位于中間的兩個字元的“空隙”,例如:回文串串 “abba” 的中心是兩個 “b” 中間的那個“空隙”。

LeetCode5. 最長回文子串題目描述暴力動态規劃方法三:中心擴散法馬拉車算法,自行參考下面兩篇文章

我們看一下一個字元串可能的回文子串的中心在哪裡?

LeetCode5. 最長回文子串題目描述暴力動态規劃方法三:中心擴散法馬拉車算法,自行參考下面兩篇文章

我們可以設計一個方法,相容以上兩種情況:

1、如果傳入重合的索引編碼,進行中心擴散,此時得到的回文子串的長度是奇數;

2、如果傳入相鄰的索引編碼,進行中心擴散,此時得到的回文子串的長度是偶數。

具體編碼細節在以下的代碼的注釋中展現。

public class Solution {

    public String longestPalindrome(String s) {
        int len = s.length();
        if (len < 2) {
            return s;
        }
        int maxLen = 1;
        String res = s.substring(0, 1);
        // 中心位置枚舉到 len - 2 即可,區間是[0,len -2]
        for (int i = 0; i < len - 1; i++) {
            String oddStr = centerSpread(s, i, i);
            String evenStr = centerSpread(s, i, i + 1);
            String maxLenStr = oddStr.length() > evenStr.length() ? oddStr : evenStr;
            if (maxLenStr.length() > maxLen) {
                maxLen = maxLenStr.length();
                res = maxLenStr;
            }
        }
        return res;
    }

    private String centerSpread(String s, int left, int right) {
        // left = right 的時候,此時回文中心是一個字元,回文串的長度是奇數
        // right = left + 1 的時候,此時回文中心是一個空隙,回文串的長度是偶數
        int len = s.length();
        int i = left;
        int j = right;
        while (i >= 0 && j < len && s.charAt(i) == s.charAt(j)) {
            i--;
            j++;
        }
        // 這裡要小心,跳出 while 循環時,恰好滿足 s.charAt(i) != s.charAt(j),是以不能取 i,不能取 j
        return s.substring(i + 1, j);
    }
}
           

馬拉車算法,自行參考下面兩篇文章

https://www.jianshu.com/p/392172762e55

https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zhong-xin-kuo-san-dong-tai-gui-hua-by-liweiwei1419/