地址:https://leetcode-cn.com/problems/longest-palindromic-substring
这道题比较烦人的是判断回文子串,因此需要一种能够快速判断原字符串的所有子串是否是回文子串的方法,于是想到了“动态规划”。
“动态规划”最关键的步骤是想清楚“状态如何转移”,因此我们的思路是这样的,依然从回文串的定义入手:
1、如果一个字符串的头尾两个字符都不相等,那么这个字符串一定不是回文串;
2、如果一个字符串的头尾两个字符相等,继续判断下去,如果里面的子串是,整体就是回文串,如果里面的子串不是回文串,整体就不是回文串。反复在做相同的事情,即找到了状态转移。这就启发我们可以把“状态”定义为原字符串的一个子串是否为回文子串。即
- 第 1 步:定义状态:
dp[i][j]
表示子串s[i, j]
是否为回文子串。 - 第 2 步:思考状态转移方程:就是在做分类讨论,根据上面的分析得到:
dp[i][j] = (s[i] == s[j]) and dp[i + 1][j - 1]
从这个状态转移方程我们知道:
(1)“动态规划”事实上是在填表,i
和 j
的关系是 i <= j
,因此,只需要填这张表的上半部分;
(2)看到 dp[i + 1][j - 1]
就得考虑边界情况。事实上,也不难想明白,如果子串 s[i + 1, j - 1]
只有 1 个字符,或者为空,那么子串 s[i, j]
一定是回文子串。因此,状态转移方程能够成立的前提是子串 s[i + 1, j - 1]
的长度严格小于
2
2
2,即 j - 1 - (i + 1) + 1 < 2
,整理得 j - i < 3
,需要对这种情况做一次特判。
- 第 3 步:考虑初始化:初始化的时候,很容易我们知道,一个字符串一定是回文串,因此把对角线先初始化为 1,即
dp[i][i] = 1
; - 第 4 步:考虑输出:只要一得到
dp[i][j] = true
,就记录子串的长度和起始位置,没有必要截取,因为截取字符串也要消耗性能,记录最长的子串即可; - 第 5 步:考虑是否可以状态压缩。因为在填表的过程中,只参考了左下方的数值,事实上可以压缩,但会增加一些判断语句,增加代码理解的难度,丢失可读性,在这里不做状态压缩。有兴趣的朋友可以试试。
下面是编码:编码的时候要注意一点,总是先得到小子串的回文判定,然后大子串才能参考小子串的判断结果。
基本编写代码的思路是:在右边界 j
逐渐扩大的过程中,枚举左边界可能出现的位置。左边界枚举的时候可以从小到大,也可以从大到小。这两版代码的差别仅在内层循环,希望大家能够自己动手,画一下表格,思考为什么这两种代码都是可行的,相信会对“动态规划”作为一种“表格法”有一个更好的理解。
Java 代码:
public class Solution8 {
// 写对代码的方法是:从小到大
public String longestPalindrome(String s) {
int len = s.length();
if (len < 2) {
return s;
}
boolean[][] dp = new boolean[len][len];
// 初始化
for (int i = 0; i < len; i++) {
dp[i][i] = true;
}
int maxLen = 1;
int start = 0;
for (int j = 1; j < len; j++) {
for (int i = 0; i < j; i++) {
if (s.charAt(i) == s.charAt(j)) {
if (j - i < 3) {
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
} else {
dp[i][j] = false;
}
// 只要 dp[i][j] == true 成立,就表示子串 s[i, j] 是回文,此时记录回文长度和起始位置
if (dp[i][j]) {
int curLen = j - i + 1;
if (curLen > maxLen) {
maxLen = curLen;
start = i;
}
}
}
}
return s.substring(start, start + maxLen);
}
}
Java 代码:
public class Solution {
public String longestPalindrome(String s) {
int len = s.length();
if (len < 2) {
return s;
}
boolean[][] dp = new boolean[len][len];
// 初始化
for (int i = 0; i < len; i++) {
dp[i][i] = true;
}
int maxLen = 1;
int start = 0;
for (int j = 1; j < len; j++) {
for (int i = j - 1; i >= 0; i--) {
if (s.charAt(i) == s.charAt(j)) {
if (j - i < 3) {
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
} else {
dp[i][j] = false;
}
// 只要 dp[i][j] == true 成立,就表示子串 s[i, j] 是回文,此时记录回文长度和起始位置
if (dp[i][j]) {
int curLen = j - i + 1;
if (curLen > maxLen) {
maxLen = curLen;
start = i;
}
}
}
}
return s.substring(start, start + maxLen);
}
}
“状态转移方程”其实可以写得更 sao 一点:借用 or
的短路功能,如果 j - i < 3
成立,其实后面就不用看了,状态转移方程可以更新成如下:
dp[i][j] = (s[i] == s[j]) and (j - i < 3 or dp[i + 1][j - 1])
Java 代码:
public class Solution10 {
// 写对代码的方法是:从小到大
public String longestPalindrome(String s) {
int len = s.length();
if (len < 2) {
return s;
}
boolean[][] dp = new boolean[len][len];
// 初始化
for (int i = 0; i < len; i++) {
dp[i][i] = true;
}
int maxLen = 1;
int start = 0;
for (int j = 1; j < len; j++) {
for (int i = 0; i < j; i++) {
dp[i][j] = s.charAt(i) == s.charAt(j) && (j - i < 3 || dp[i + 1][j - 1]);
// 只要 dp[i][j] == true 成立,就表示子串 s[i, j] 是回文,此时记录回文长度和起始位置
if (dp[i][j]) {
int curLen = j - i + 1;
if (curLen > maxLen) {
maxLen = curLen;
start = i;
}
}
}
}
return s.substring(start, start + maxLen);
}
}
总结:
1、我们看到了,用“动态规划”解决的问题有的时候并不是直接面向问题的。
在本题中,“动态规划”依然是“空间换时间”思想的体验,并且本身“动态规划”作为一种打表格法,就是在用“空间”换时间。
2、本人归纳了思考动态规划问题的步骤,仅供大家参考。
下面是代码调试部分:
Java 代码:
import java.util.Arrays;
public class Solution8 {
// 写对代码的方法是:从小到大
public String longestPalindrome(String s) {
int len = s.length();
if (len < 2) {
return s;
}
int[][] dp = new int[len][len];
for (int i = 0; i < dp.length; i++) {
Arrays.fill(dp[i], -1);
}
// 初始化
for (int i = 0; i < len; i++) {
dp[i][i] = 1;
}
int maxLen = 1;
int start = 0;
for (int j = 1; j < len; j++) {
for (int i = 0; i < j; i++) {
if (s.charAt(i) == s.charAt(j)) {
if (j - i < 3) {
dp[i][j] = 1;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
} else {
dp[i][j] = 0;
}
// 只要 dp[i][j] == true 成立,就表示子串 s[i, j] 是回文,此时记录回文长度和起始位置
if (dp[i][j] == 1) {
int curLen = j - i + 1;
if (curLen > maxLen) {
maxLen = curLen;
start = i;
}
}
printDpTable(dp);
}
}
return s.substring(start, start + maxLen);
}
private void printDpTable(int[][] dp) {
for (int i = 0; i < dp.length; i++) {
for (int j = 0; j < dp[i].length; j++) {
if (dp[i][j] == 1) {
System.out.print("1 ");
}
if (dp[i][j] == 0) {
System.out.print("0 ");
}
if (dp[i][j] == -1) {
System.out.print(" ");
}
}
System.out.println();
}
System.out.println();
}
public static void main(String[] args) {
Solution8 solution8 = new Solution8();
String longestPalindrome = solution8.longestPalindrome("abcdcba");
System.out.println(longestPalindrome);
}
}
输出:
1 0
1
1
1
1
1
1
1 0 0
1
1
1
1
1
1
1 0 0
1 0
1
1
1
1
1
1 0 0 0
1 0
1
1
1
1
1
1 0 0 0
1 0 0
1
1
1
1
1
1 0 0 0
1 0 0
1 0
1
1
1
1
1 0 0 0 0
1 0 0
1 0
1
1
1
1
1 0 0 0 0
1 0 0 0
1 0
1
1
1
1
1 0 0 0 0
1 0 0 0
1 0 1
1
1
1
1
1 0 0 0 0
1 0 0 0
1 0 1
1 0
1
1
1
1 0 0 0 0 0
1 0 0 0
1 0 1
1 0
1
1
1
1 0 0 0 0 0
1 0 0 0 1
1 0 1
1 0
1
1
1
1 0 0 0 0 0
1 0 0 0 1
1 0 1 0
1 0
1
1
1
1 0 0 0 0 0
1 0 0 0 1
1 0 1 0
1 0 0
1
1
1
1 0 0 0 0 0
1 0 0 0 1
1 0 1 0
1 0 0
1 0
1
1
1 0 0 0 0 0 1
1 0 0 0 1
1 0 1 0
1 0 0
1 0
1
1
1 0 0 0 0 0 1
1 0 0 0 1 0
1 0 1 0
1 0 0
1 0
1
1
1 0 0 0 0 0 1
1 0 0 0 1 0
1 0 1 0 0
1 0 0
1 0
1
1
1 0 0 0 0 0 1
1 0 0 0 1 0
1 0 1 0 0
1 0 0 0
1 0
1
1
1 0 0 0 0 0 1
1 0 0 0 1 0
1 0 1 0 0
1 0 0 0
1 0 0
1
1
1 0 0 0 0 0 1
1 0 0 0 1 0
1 0 1 0 0
1 0 0 0
1 0 0
1 0
1
abcdcba
Java 代码:
import java.util.Arrays;
public class Solution9 {
public String longestPalindrome(String s) {
int len = s.length();
if (len < 2) {
return s;
}
int[][] dp = new int[len][len];
for (int i = 0; i < dp.length; i++) {
Arrays.fill(dp[i], -1);
}
// 初始化
for (int i = 0; i < len; i++) {
dp[i][i] = 1;
}
int maxLen = 1;
int start = 0;
for (int j = 1; j < len; j++) {
for (int i = j - 1; i >= 0; i--) {
if (s.charAt(i) == s.charAt(j)) {
if (j - i < 3) {
dp[i][j] = 1;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
} else {
dp[i][j] = 0;
}
// 只要 dp[i][j] == true 成立,就表示子串 s[i, j] 是回文,此时记录回文长度和起始位置
if (dp[i][j] == 1) {
int curLen = j - i + 1;
if (curLen > maxLen) {
maxLen = curLen;
start = i;
}
}
printDpTable(dp);
}
}
return s.substring(start, start + maxLen);
}
private void printDpTable(int[][] dp) {
for (int i = 0; i < dp.length; i++) {
for (int j = 0; j < dp[i].length; j++) {
if (dp[i][j] == 1) {
System.out.print("1 ");
}
if (dp[i][j] == 0) {
System.out.print("0 ");
}
if (dp[i][j] == -1) {
System.out.print(" ");
}
}
System.out.println();
}
System.out.println();
}
public static void main(String[] args) {
Solution9 solution9 = new Solution9();
String s = "abcdcba";
String longestPalindrome = solution9.longestPalindrome(s);
System.out.println(longestPalindrome);
}
}
输出:
1 0
1
1
1
1
1
1
1 0
1 0
1
1
1
1
1
1 0 0
1 0
1
1
1
1
1
1 0 0
1 0
1 0
1
1
1
1
1 0 0
1 0 0
1 0
1
1
1
1
1 0 0 0
1 0 0
1 0
1
1
1
1
1 0 0 0
1 0 0
1 0
1 0
1
1
1
1 0 0 0
1 0 0
1 0 1
1 0
1
1
1
1 0 0 0
1 0 0 0
1 0 1
1 0
1
1
1
1 0 0 0 0
1 0 0 0
1 0 1
1 0
1
1
1
1 0 0 0 0
1 0 0 0
1 0 1
1 0
1 0
1
1
1 0 0 0 0
1 0 0 0
1 0 1
1 0 0
1 0
1
1
1 0 0 0 0
1 0 0 0
1 0 1 0
1 0 0
1 0
1
1
1 0 0 0 0
1 0 0 0 1
1 0 1 0
1 0 0
1 0
1
1
1 0 0 0 0 0
1 0 0 0 1
1 0 1 0
1 0 0
1 0
1
1
1 0 0 0 0 0
1 0 0 0 1
1 0 1 0
1 0 0
1 0
1 0
1
1 0 0 0 0 0
1 0 0 0 1
1 0 1 0
1 0 0
1 0 0
1 0
1
1 0 0 0 0 0
1 0 0 0 1
1 0 1 0
1 0 0 0
1 0 0
1 0
1
1 0 0 0 0 0
1 0 0 0 1
1 0 1 0 0
1 0 0 0
1 0 0
1 0
1
1 0 0 0 0 0
1 0 0 0 1 0
1 0 1 0 0
1 0 0 0
1 0 0
1 0
1
1 0 0 0 0 0 1
1 0 0 0 1 0
1 0 1 0 0
1 0 0 0
1 0 0
1 0
1
abcdcba