1. 程式人生 > >動態規劃:最長迴文子串 & 最長迴文子序列

動態規劃:最長迴文子串 & 最長迴文子序列

一、題目

所謂迴文字串,就是一個字串,從左到右讀和從右到左讀是完全一樣的,比如 “a”、“aba”、“abba”。

對於一個字串,其子串是指連續的一段子字串,而子序列是可以非連續的一段子字串。

最長迴文子串最長迴文子序列(Longest Palindromic Subsequence)是指任意一個字串,它說包含的長度最長的迴文子串和迴文子序列。

例如:字串 “ABCDDCEFA”,它的 最長迴文子串 即 “CDDC”,最長迴文子序列 即 “ACDDCA”。

二、最長迴文子串

1. 思路

首先這類問題通過窮舉的辦法,判斷是否是迴文子串並再篩選出最長的,效率是很差的。我們使用 動態規劃 的策略來求解它。首先我們從子問題入手,並將子問題的解儲存起來,然後在求解後面的問題時,反覆的利用子問題的解,可以極大的提示效率。

由於最長迴文子串是要求連續的,所以我們可以假設 j 為子串的起始座標,i 為子串的終點座標,其中 ij 都是大於等於 0 並且小於字串長度 length 的,且 j <= i,這樣子串的長度就可以使用 i - j + 1 表示了。

我們從長度為 1 的子串依次遍歷,長度為 1 的子串肯定是迴文的,其長度就是 1;然後是長度為 2 的子串依次遍歷,只要 str[i] 等於 str[j] ,它就是迴文的,其長度為 2;接下來就好辦了,長度大於 2 的子串,如果它要滿足是迴文子串的性質,就必須有 str[i] 等於 str[j] ,並且去掉兩頭的子串 str[j+1 ... i-1] 也一定是迴文子串,所以我們使用一個數組來儲存以 j

為子串起始座標,i 為子串終點座標的子串是否是迴文的,由於我們是從子問題依次增大求解的,所以求解 [i ... j] 的問題時,比它規模更小的問題,結果都是可以直接使用的了。

2. 程式碼

public class Main {
  
    public static void main(String[] args) {
        String s = "cabbaeeaf";
        System.out.println(getLPS(s));
    }
      
    public static String getLPS(String s) {
        char
[] chars = s.toCharArray(); int length = chars.length; // 第一維引數表示起始位置座標,第二維引數表示終點座標 // lps[j][i] 表示以 j 為起始座標,i 為終點座標是否為迴文子串 boolean[][] lps = new boolean[length][length]; int maxLen = 1; // 記錄最長迴文子串最長長度 int start = 0; // 記錄最長迴文子串起始位置 for (int i = 0; i < length; i++) { for (int j = 0; j <= i; j++) { if (i - j < 2) { // 子字串長度小於 2 的時候單獨處理 lps[j][i] = chars[i] == chars[j]; } else { // 如果 [i, j] 是迴文子串,那麼一定有 [j+1, i-1] 也是回子串 lps[j][i] = lps[j + 1][i - 1] && (chars[i] == chars[j]); } if (lps[j][i] && (i - j + 1) > maxLen) { // 如果 [i, j] 是迴文子串,並且長度大於 max,則重新整理最長迴文子串 maxLen = i - j + 1; start = j; } } } return s.substring(start, start + maxLen); } }

三、最長迴文子序列

1. 思路

子序列的問題將比子串更復雜,因為它是可以不連續的,這樣如果窮舉的話,問題規模將會變得非常大,我們依舊是選擇使用 動態規劃 來解決。

首先我們假設 str[0 ... n-1] 是給定的長度為 n 的字串,我們使用 lps(0, n-1) 表示以 0 為起始座標,長度為 n-1 的最長迴文子序列的長度。那麼我們需要從子問題開始入手,即我們一次遍歷長度 1 到 n-1 的子串,並將子串包含的 最長迴文子序列的長度 儲存在 lps 的二維陣列中。

遍歷過程中,迴文子序列的長度一定有如下性質:

  • 如果子串的第一個元素 str[j] 和最後一個元素 str[i+j] 相等,那麼 lps[j, i+j] = lps[j+1, i+j-1] + 2,其中 lps[j+1, i+j-1] 表示去掉兩頭元素的最長子序列長度。
  • 如果兩端的元素不相等,那麼lps[j, i+j] = max(lps[j][i+j-1], lps[j+1][i+j]),這兩個表示的分別是去掉末端元素的子串和去掉起始元素的子串。

2. 程式碼

public class Main {
 
    public static void main(String[] args) {
        String s = "cabbeaf";
        System.out.println(getLPS(s));
    }
     
    public static int getLPS(String s) {
        char[] chars = s.toCharArray();
        int length = chars.length;
        // 第一維引數表示起始位置的座標,第二維引數表示長度,使用 0 表示長度 1
        int[][] lps = new int[length][length];
        for (int i = 0; i < length; i++) {
            lps[i][i] = 1; // 單個字元的最長迴文子序列長度為1,特殊對待一下
        }
        // (i + 1) 表示當前迴圈的子字串長度
        for (int i = 1; i < length; i++) {
            // j 表示當前迴圈的字串起始座標
            for (int j = 0; i + j < length; j++) {
                // 即當前迴圈的子字串座標為 [j, i + j]
                // 所以第一個字元是 chars[j],最後一個字元就是 chars[i + j]
                if (chars[j] == chars[i + j]) {
                    lps[j][i + j] = lps[j + 1][i + j - 1] + 2;
                } else {
                    lps[j][i + j] = Math.max(lps[j][i + j - 1], lps[j + 1][i + j]);
                }
            }
        }
        // 最大值一定在以0為起始點,長度為 length - 1 的位置
        return lps[0][length - 1];
    }
     
}

最後,這題只返回了最長迴文子序列的長度,一般面試題中也只是要求返回長度即可。但是如果你也想知道最長迴文子序列具體是啥,這可以額外新增一個變數記錄最長迴文子序列是哪些字元,例如維護一個鍵為 lps[j][i + j],值為 String 的 map。