動態規劃:最長迴文子串 & 最長迴文子序列
一、題目
所謂迴文字串,就是一個字串,從左到右讀和從右到左讀是完全一樣的,比如 “a”、“aba”、“abba”。
對於一個字串,其子串是指連續的一段子字串,而子序列是可以非連續的一段子字串。
最長迴文子串 和 最長迴文子序列(Longest Palindromic Subsequence)是指任意一個字串,它說包含的長度最長的迴文子串和迴文子序列。
例如:字串 “ABCDDCEFA”,它的 最長迴文子串 即 “CDDC”,最長迴文子序列 即 “ACDDCA”。
二、最長迴文子串
1. 思路
首先這類問題通過窮舉的辦法,判斷是否是迴文子串並再篩選出最長的,效率是很差的。我們使用 動態規劃 的策略來求解它。首先我們從子問題入手,並將子問題的解儲存起來,然後在求解後面的問題時,反覆的利用子問題的解,可以極大的提示效率。
由於最長迴文子串是要求連續的,所以我們可以假設 j
為子串的起始座標,i
為子串的終點座標,其中 i
和 j
都是大於等於 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。