动态规划学习笔记 :解题五步曲

195 阅读3分钟

动态规划

  • 动态规划(Dynamic Programming),简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。
  • 动态规划中每一个状态一定是由上一个状态推导出来的。

解题五部曲

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

下面运用这五步解决下面多个例题:

LeetCode70. 爬楼梯

image.png 1.确定dp数组(dp table)以及下标的含义

dp[i]数组表示,爬到第i层楼梯,有dp[i]种方法。

2. 确定递推公式

根据定义dp[i]等于爬上i层的方法数量,动态规划中当前状态是由上一个状态推断出来的,由于一次只能爬一或者二个台阶,那么和容易想到dp[i]应该是和dp[i - 1]以及dp[i - 2]有关系。 dp[i - 1]跳一台阶就是dp[i], dp[i - 2]跳两台阶就是dp[i],两者相加的方法总和便是dp[i]!

3. dp数组如何初始化

在这里很显然,dp[1] = 1, dp[2] = 2, 初始化完成,这里不应该讨论dp[0],没有意义,因为只能根据dp[2]的结果逆推dp[0]为1, 也就是说在0层不动也是一种方法,这其实就有些荒谬了hh

4. 确定遍历顺序

dp[i] = dp[i - 1] + dp[i - 2],很显然是从前往后便利的

5. 举例推导dp数组

举例得: dp[1] = 1, dp[2] = 2, dp[3] = 3, dp[4] = 5, 验证成功。

  • 代码如下
class Solution {
    public int climbStairs(int n) {
        int[] dp = new int[n + 1];
        if (n <= 1) {
            return n;
        }
        dp[1] = 1;
        dp[2] = 2;
        for (int i = 3; i < n + 1; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }
}

LeetCode5. 最长回文子串

image.png 代码如下:

class Solution {
    public String longestPalindrome(String s) {
        int len = s.length();
        //1. dp数组的意义为以i,j为边界的字符串是否为回文子串
        //2. 初始化dp皆为false;
        boolean[][] dp = new boolean[len][len];
        int start = 0;
        int end = 0;
        int maxLen = 0;

        //4. 递归顺序应该是从下往上,从左往右的,因为dp[i][j]的值需要dp[i + 1][j - 1]来确定
        for (int i = len - 1; i >= 0; i--) {
            for (int j = i ; j < len; j++) {
                //3. 递归逻辑: j 与 i 相差为1或者0的时候说明必然是回文子串,否则根据dp[i + 1][j - 1]来确定!
                if (s.charAt(i) == s.charAt(j)) {
                    if (j - i <= 1) {
                        if (j - i > maxLen) {
                            start = i;
                            end = j;
                            maxLen = j - i;
                        }
                        dp[i][j] = true;
                    } else {
                        if (dp[i + 1][j - 1] == true) {
                            if (j - i > maxLen) {
                                start = i;
                                end = j;
                                maxLen = j - i;
                            }
                            dp[i][j] = true;
                        }
                    }   
                }
            }
        }
        return s.substring(start, end + 1);
    }
}

LeetCode198. 打家劫舍

image.png

class Solution {
    public int rob(int[] nums) {
        if (nums == null) {
            return 0;
        }
        if (nums.length == 1) {
            return nums[0];
        }
        int[] dp = new int[nums.length];
        //1. dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]。
        //3. 初始化
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0], nums[1]);
        
       //4. 确定遍历顺序 dp[i] 是根据dp[i - 2] 和 dp[i - 1] 推导出来的,那么一定是从前到后遍历!
        for (int i = 2; i < nums.length; i++) {
        //2. 如果偷第i房间,那么dp[i] = dp[i - 2] + nums[i]。如果不偷第i房间,那么dp[i] = dp[i - 1]
            dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
        }
        return dp[nums.length - 1];
    }
}

这里有个升级版就是环型数组打家劫舍,比较两种情况即可,情况一: 不含尾元素, 情况二: 不含头元素。

LeetCode337. 打家劫舍(二叉树版)

image.png

这题就直接看卡尔哥的解析就很好了😁 programmercarl.com/0337.%E6%89…

代码如下:

class Solution {
    public int rob(TreeNode root) {
        int[] res = robTree(root);
        return Math.max(res[0], res[1]);
    }

    public int[] robTree(TreeNode node) {
        // dp[0]表示不偷该节点能盗取的最高金额,dp[1]表示偷该节点能盗取的最高金额。
        int[] dp = new int[2];
        if (node == null) {
            dp[0] = 0;
            dp[1] = 0;
            return dp;
        }

        int[] left = robTree(node.left);
        int[] right = robTree(node.right);
        
        dp[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
        dp[1] = node.val + left[0] + right[0];
        return dp;
    }
}
  • 本文是代码随想录的学习笔记, 感谢卡尔哥的教学!!!🥰