动态规划DP方法论

269 阅读4分钟

动态规划无论在算法竞赛还是求职过程笔试面试中都是一个可讨论的话题,本篇集中介绍动态规划问题的解决套路,并将leetcode上相关题目做一个归类整理,相关视频分享在B站

视频链接:

[动态规划(dp)方法论,序列DP]www.bilibili.com/video/BV1Ma…

动态规划分类

  • 动态规划问题宏观上分为以下:
  1. 序列DP,索引错位
  2. 路径DP,索引不错位
  3. 区间DP,索引错位
  4. 树形DP,记忆化搜索
  5. 数位DP,索引错位
  6. 背包DP,索引错位
  7. 状态机DP,索引错位
  8. 状态压缩DP,索引错位
  • DP的核心思想

DP核心在于数列通项表示(dp数组)、对问题分类讨论并结合数学归纳法思想进行状态计算(状态转移方程)、逐步逼近最有结果

关于DP数组定义,我们要根据问题参数维度确定dp数组维度; 关于问题分类讨论,我们要考虑得到目标结果时可能的情况有多少,其分类讨论的中间过程与最终结果可以构成一个拓扑图;(题外话:拓扑图上求最长路用递推的方法) 关于状态计算,我们要考虑如何根据已有的状态(即dp数组中已经得到的值)来计算新的状态,核心思想还是数学归纳法,过程俗称"打表"; 逐步逼近最有结果,由于分类情况有很多,我们要考虑的是在达到目标状态时需要依赖的中间状态,不相关的中间状态不需要考虑,

  • DP的编码套路
  1. 考虑参数的边界情况
  2. 创建dp数组
  3. 初始化边界条件,考虑索引错位避免数组越界
  4. 循环递推状态计算,包括求数量、最值。

动态规划方法论之实践

这里基于LeetCode上比较经典的三道题目来介绍,分别是LeetCode 10正则表达式匹配、LeetCode 44通配符匹配、LeetCode72编辑距离匹配

对于LeetCode10和LeetCode44其实是一类题目,LeetCode44相较于LeetCode10放松了条件限制因此更加简单。

以LeetCode10为例,具体说来:

  1. 我们考虑边界情况,如果两个字符串有一个为null,那么应该直接返回false
  2. 我们考虑dp数组定义,因为两个字符串匹配,所以dp数组确定2维
  3. 我们考虑边界条件,这里的边界条件考虑也是通式通法,我们要考虑"被包含"串长度为0时的情况。"被包含"在后面的题目中都由涉及,对于LeetCode10来讲,我们要用模式串p来匹配原字符串s,模式串p可以包含更多规则只要其中存在匹配原字符串s的子规则即可,所以字符串s被称作"被包含串"。这里我们需要考虑串长度为0时,只要s串也为0或者只有'.'即可,换句话说,如果p中出现'*',由于题目要求'*'需要和之前的字符捆绑匹配,所以我们可以将当前'*'与其之前捆绑字符进行去除,即可实现匹配
  4. 考虑索引错位,我们一般对于字符串操作进行补充前导空格字符的操作
  5. 递推状态计算,我们根据分类情况,对于一个通用状态下的s[i]和p[j],我们考虑索引j及之前的p子字符串与索引i及之前的s子字符串的匹配情况。考虑主动匹配和被动匹配,这里在 [视频]www.bilibili.com/video/BV1Ma… 进行详细阐述

具体代码

`

/**
 * 无匹配符考虑对应字符相等
 * .考虑单个字符匹配
 * *必须与前一个字符共同构成匹配,不能单独出现,所以*的匹配0个字符、一个字符和多个字符的匹配过程
 * f[i][j]=f[i][j-2]
 * f[i][j]=f[i-1][j-1]
 * f[i][j]=f[i-1][j]
 * @param s
 * @param p
 * @return
 */
public boolean isMatch(String s, String p) {
    if (s == null || p == null) {
        return false;
    }
    int n=s.length();
    int m=p.length();
    s=" "+s;
    p=" "+p;
    boolean[][] dp = new boolean[s.length() + 1][p.length() + 1];
    dp[0][0] = true;
    for (int j = 1; j <= m; j++) {
        if (p.charAt(j)=='*'){
            dp[0][j]=dp[0][j-2];
        }
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (p.charAt(j) == '.' || p.charAt(j) == s.charAt(i)) {//如果是任意元素 或者是对于元素匹配
                dp[i][j] = dp[i-1][j-1];
            }
            if (p.charAt(j) == '*') {
                if (p.charAt(j - 1) != s.charAt(i) && p.charAt(j - 1) != '.') {//如果前一个元素不匹配 且不为任意元素
                    dp[i][j] = dp[i][j - 2];    // *匹配0个字符
                } else {
                    dp[i][j] = (dp[i-1][j] || dp[i][j - 2]);  // 匹配多个字符
                        /*
                            dp[i][j] = dp[i-1][j] // 多个字符匹配的情况
                            or dp[i][j] = dp[i][j-1] // 单个字符匹配的情况
                            or dp[i][j] = dp[i][j-2] // 没有匹配的情况
                         */
                }
            }
        }
    }
    return dp[n][m];
}

`