动态规划算法

动态规划算法
强烈推介IDEA2020.2破解激活,IntelliJ IDEA 注册码,2020.2 IDEA 激活码

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]

说明:

  • 「动态规划」事实上是在填一张二维表格,由于构成子串,因此ij的关系是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 参考代码

在了解动态规划算法解决该题的实现思路后,我自己提供的示例参考代码如下,其中lr分别表示子串的左右边界下标:

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);
    }
}
本文来源MrKorbin,由架构君转载发布,观点不代表Java架构师必看的立场,转载请标明来源出处:https://javajgs.com/archives/25254

发表评论