文章目录
1.动态规划解决背包问题
详情参考另一篇博文: 14.常用10大算法-3.动态规划算法。通过该文可以先了解动态规划算法的基本思想,在此基础上更容易理解下文题目的解题思路。
2.动态规划求最大回文子串
LeetCode题目 5. 最长回文子串:
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:输入: "babad" 输出: "bab" 注意: "aba" 也是一个有效答案。
示例 2:
输入: "cbbd" 输出: "bb"
2.1 思路分析
对于一个子串而言,如果它是回文串,并且长度大于 2,那么将它首尾的两个字母去除之后,它仍然是个回文串。例如对于字符串 ababa
,如果我们已经知道bab
是回文串,那么ababa
一定是回文串,这是因为它的首尾两个字母都是a
。根据这样的思路,即可以基于子情况判断当前最优情况,所以本题可以采用动态规划的方法来解决。
「动态规划」的一个关键的步骤是想清楚「状态如何转移」。事实上,「回文」天然具有「状态转移」性质。
- 一个回文去掉两头以后,剩下的部分依然是回文(这里暂不讨论边界情况);
依然从回文串的定义展开讨论:
- 如果一个字符串的头尾两个字符都不相等,那么这个字符串一定不是回文串;
- 如果一个字符串的头尾两个字符相等,才有必要继续判断下去。
- 如果里面的子串是回文,整体就是回文串;
- 如果里面的子串不是回文串,整体就不是回文串。
即:在头尾字符相等的情况下,里面子串的回文性质据定了整个子串的回文性质,这就是状态转移。因此可以把「状态」定义为原字符串的一个子串是否为回文子串。
2.2 算法实现
第1步:定义状态
dp[i][j]
表示子串 s[i..j]
是否为回文子串,这里子串s[i..j]
定义为左闭右闭区间,可以取到 s[i]
和s[j]
。
第2步:思考状态转移方程
在这一步分类讨论(根据头尾字符是否相等),根据上面的分析得到:
dp[i][j] = (s[i] == s[j]) and dp[i + 1][j - 1]
说明:
-
「动态规划」事实上是在填一张二维表格,由于构成子串,因此
i
和j
的关系是i <= j
,因此,只需要填这张表格对角线以上的部分。 -
看到
dp[i + 1][j - 1]
就得考虑边界情况。
边界条件是:表达式[i + 1, j - 1]
不构成区间,即长度严格小于 2,即j - 1 - (i + 1) + 1 < 2
,整理得j - i < 3
。
这个结论很显然:j - i < 3
等价于 j - i + 1 < 4
,即当子串 s[i..j]
的长度等于2
或者等于 3
的时候,其实只需要判断一下头尾两个字符是否相等就可以直接下结论了。
- 如果子串
s[i + 1..j - 1]
只有 1 个字符,即去掉两头,剩下中间部分只有 11 个字符,显然是回文; - 如果子串
s[i + 1..j - 1]
为空串,那么子串s[i, j]
一定是回文子串。
因此,在s[i] == s[j]
成立和j - i < 3
的前提下,直接可以下结论,dp[i][j] = true
,否则才执行状态转移。
第3步:考虑初始化
初始化的时候,单个字符一定是回文串,因此把对角线先初始化为 true,即dp[i][i] = true
。
事实上,初始化的部分都可以省去。因为只有一个字符的时候一定是回文,dp[i][i]
根本不会被其它状态值所参考。
第4步:考虑输出
只要一得到 dp[i][j] = true
,就记录子串的长度和起始位置,没有必要截取,这是因为截取字符串也要消耗性能,记录此时的回文子串的「起始位置」和「回文长度」即可。
第5步:考虑优化空间
因为在填表的过程中,只参考了左下方的数值。事实上可以优化,但是增加了代码编写和理解的难度,丢失可读和可解释性。在这里不优化空间。
注意事项:总是先得到小子串的回文判定,然后大子串才能参考小子串的判断结果,即填表顺序很重要。
大家能够可以自己动手,画一下表格,相信会对「动态规划」作为一种「表格法」有一个更好的理解。
上述思路分析与算法实现步骤参考自:方法二:动态规划 。
2.3 参考代码
在了解动态规划算法解决该题的实现思路后,我自己提供的示例参考代码如下,其中l
和r
分别表示子串的左右边界下标:
public class Solution {
public String longestPalindrome(String s){
int len = s.length();
// 特殊判定:如果长度为1,一定为回文
if (len<=1) return s;
int maxLen = 1;
int begin = 0;
// dp[i][j] 表示 s[i, j] 是否是回文串
boolean[][] dp = new boolean[len][len];
char[] charArray = s.toCharArray();
// 单个字符一定是回文,需要初始化,作为后续长度大于1情况下的前置状态
for (int i = 0; i < len; i++) {
dp[i][i] = true;
}
for (int r = 1;r < len; r++) {
for (int l = 0; l < r; l++) {
if (charArray[l]!=charArray[r]){
dp[l][r] = false;
}else {
// 如果长度为2且左右边界字符相等,则一定是回文
if (r-l+1==2){
dp[l][r] = true;
}else {
dp[l][r] = dp[l+1][r-1];
}
}
// 只要 dp[i][j] == true 成立,就表示子串 s[i..j] 是回文,此时记录回文长度和起始位置
if (dp[l][r] && r-l+1>maxLen){
maxLen = r-l+1;
begin = l;
}
}
}
return s.substring(begin, begin + maxLen);
}
}