第九章 动态规划 part07 70. 爬楼梯 (进阶) 322. 零钱兑换 279. 完全平方数

第四十五天| 第九章 动态规划 part07 70. 爬楼梯 (进阶) 322. 零钱兑换 279. 完全平方数

一、70. 爬楼梯 (进阶)

  • 题目链接:https://leetcode.cn/problems/climbing-stairs/

  • 题目介绍:

    • 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

      每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

      示例 1:

      输入:n = 2
      输出:2
      解释:有两种方法可以爬到楼顶。
      1. 1 阶 + 1 阶
      2. 2 阶
      
  • 思路:

    • 之前采用DP五部曲的基本思路分析过一次

    • 但其实本题稍微进阶一点,就变成一个完全背包问题。

    • 比如题目改为:如果一次可以爬1阶、2阶、3阶或者…m阶(这里从1开始到m都是连续的正整数),这时你应该怎么求解。

      • 这里就用到了完全背包的思路,还是回到本题中:

        • 这里的物品相当于有两个,分别是1阶和2阶

        • 背包相当于最高阶楼梯n

        • 本题就转化为,我们有两个物品,有一个最大容量为n的背包,每个物品有无限个,怎么装满这个背包(装满这个背包的方法有多少种)

        • 按照完全背包的DP五部曲分析:

          • (1)确定dp数组的下标及含义:

            • dp[j]:表示的是装满容量为j的背包,共有dp[j]种方法
              
          • (2)确定递推公式:

            • dp[j] += dp[j - i];
              
          • (3)初始化dp数组:

            • dp[0] = 1;
              
          • (4)确定遍历顺序:

            • 因为这是一道求排列数的题目,所以必须先遍历背包,再遍历物品

            • for (int j = 1; j <= n; j++) {
                  for (int i = 1; i <= m; i++) {
                      if (j >= i) {
                          dp[j] += dp[j-i];
                      }
                  }
              }
              
  • 代码:

class Solution {
    public int climbStairs(int n) {
        int m = 2;
        int[] dp = new int[n+1];
        dp[0] = 1;
        for (int j = 1; j <= n; j++) {
            for (int i = 1; i <= m; i++) {
                if (j >= i) {
                    dp[j] += dp[j-i];
                }
            }
        }
        return dp[n];
    }
}

扩展题:爬楼梯

  • 题目:如果一个人一步可以爬1或2或3或4或5…或M阶台阶,那么走到楼梯顶(第n阶),有多少种方法?
  • 思路:多重背包
  • 代码:

第九章 动态规划 part07 70. 爬楼梯 (进阶) 322. 零钱兑换 279. 完全平方数_第1张图片

二、322. 零钱兑换

  • 题目链接:https://leetcode.cn/problems/coin-change/

  • 题目介绍:

    • 给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

      计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1

      你可以认为每种硬币的数量是无限的。

      示例 1:

      输入:coins = [1, 2, 5], amount = 11
      输出:3 
      解释:11 = 5 + 5 + 1
      
  • 思路:

    • 本题求解的相当于装满容量为j的背包,所需的最少的物品个数

    • DP五部曲:

      • (1)确定dp数组及下标的含义:

        • dp[j]:表示的是装满容量为j的背包,最少需要dp[j]个物品
          
      • (2)确定递推公式:

        • dp[j] = Math.min(dp[j], dp[j-coins[i]]+1);
          
      • (3)初始化dp数组:

        • 因为每次取的是最小值,因此需要把dp数组的每一个值初始化为Integer.MAX_VALUE
      • (4)确定循环顺序:

        • 因为这里求的既不是组合数,也不是排列数,因此先遍历哪一个都可以

        • 这里还需要注意的是:

          • 只有dp[j-coins[i]]不是初始最大值时,该位才有选择的必要
          • 如果dp[j-coins[i]]是初始最大值,+1之后就会变成Integer.MIN_VALUE,那么求min之后就变成Integer.MIN_VALUE。
          • 所以要跳过这个。
        • // 先遍历物品,再遍历背包
          for (int i = 0; i < coins.length; i++) {
              for (int j = coins[i]; j <= amount; j++) {
                  if (dp[j - coins[i]] != Integer.MAX_VALUE) {
                      // 只有dp[j-coins[i]]不是初始最大值时,该位才有选择的必要
                      // 否则的话,Integer.MAX_VALUE + 1会变为一个很小的负数
                      dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
                  }
              }
          }
          
  • 代码:

class Solution {
    public int coinChange(int[] coins, int amount) {
        int[] dp = new int[amount + 1];
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0;
        for (int i = 0; i < coins.length; i++) {
            for (int j = coins[i]; j <= amount; j++) {
                // 只有dp[j-coins[i]]不是初始最大值时,该位才有选择的必要
                // 否则的话,Integer.MAX_VALUE + 1会变为一个很小的负数
                if (dp[j - coins[i]] != Integer.MAX_VALUE) {
                    dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
                }
            }
        }
        if (dp[amount] == Integer.MAX_VALUE) return -1;
        return dp[amount];
    }
}

三、279. 完全平方数

  • 题目链接:https://leetcode.cn/problems/perfect-squares/

  • 题目介绍:

    • 给你一个整数 n ,返回 和为 n 的完全平方数的最少数量

      完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,14916 都是完全平方数,而 311 不是。

      示例 1:

      输入:n = 12
      输出:3 
      解释:12 = 4 + 4 + 4
      
  • 思路:

    • 本题的思路和上道题目类似,都是求装满背包的最少物品数量
    • 所以看代码就行,我最后会总结一下这两道题的异同,以及需要注意的地方
  • 代码:

class Solution {
    public int numSquares(int n) {
        int max = Integer.MAX_VALUE;
        int[] dp = new int[n + 1];
        Arrays.fill(dp, max);
        dp[0] = 0;
        for (int i = 1; i * i <= n; i++) {
            for (int j = i * i; j <= n; j++) {
                dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
            }
        }
        return dp[n];
    }
}

四、总结:

4.1 总结1:零钱兑换和完全平方数的异同:

  • 首先,可以发现这两道题的本质是类似的,都是求装满一个容量为j的背包,所需要最小的物品数量。
  • 但是,二者在循环和递推中有一点区别
    • 零钱兑换,需要判断当前物品放入背包中,如果背包容量j减去当前物品重量对应的dp数组值为Integer.MAX_VALUE,则跳过此递推公式
    • 而完全平方数就不用判断
    • 为什么呢?
      • 是因为完全平方数的dp数组,每一个位置的值都会被覆盖掉,虽然初始化的时候均为Integer.MAX_VALUE,但是对于每一个数字,最起码都可以用数字1累加得到,因此就不会有凑不成的情况发生
      • 但是,零钱兑换这道题就会出现凑不成的情况,如果出现这个情况,该位置的值仍为Integer.MAX_VALUE,加1之后就会变成Integer.MIN_VALUE,再求解Math.min就会出错。
      • 所以零钱兑换需要判断,完全平方数则不需要

4.2 总结2:目前背包类的题目大致可以分为以下三种及对应的变体

4.2.1 第一种:求解装满背包的最大价值
  • 递推公式:

    • dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
      
  • 初始化:

    • 首先考虑下标为0的情况,在考虑其他下标
    • 这类问题一般都初始化为0
  • 典型题目:

    • 416.分割等和子集==(01背包,判断能否装满背包最大重量的一半,能装满返回true,装不满返回false)==
    • 1049.最后一块石头的重量II**(01背包,尽可能多的装背包(背包最大重量为总重量的一半))**
4.2.2 第二种:求解装满背包的方法有多少种
  • 递推公式:

    • dp[j] += dp[j - weight[i]];
      
  • 初始化:

    • 一般地:

    • dp[0] = 1;
      
  • 典型题目:

    • 494.目标和**(01背包)**
    • 518.零钱兑换II**(完全背包的组合) —> 先遍历物品再遍历背包**
    • 377.组合总和Ⅳ**(完全背包的排列) —> 先遍历背包再遍历物品**
4.2.3 第三种:求解装满背包的最大或最少物品个数
  • 递推公式:

    • 最大:

      • dp[j] = Math.max(dp[j], dp[j - weight[i]] + 1);
        
    • 最小:

      • dp[j] = Math.max(dp[j], dp[j - weight[i]] + 1);
        
  • 初始化:

    • 最大:

      • 全部初始化为0
    • 最小:

      • int max = Integer.MAX_VALUE;
        
      • 先全部初始化为max,再将dp[0]初始化为0。

  • 典型题目:

    • 474.一和零(最大)(01背包,背包是二维的)
    • 322.零钱兑换(最小)(完全背包)
    • 279.完全平方数(最小)(完全背包)

你可能感兴趣的:(动态规划)