Day48~70. 爬楼梯 (进阶)、322. 零钱兑换、279.完全平方数

141 阅读6分钟

摘要

本文主要介绍了LeetCode动态规划的几个题目,包括70.爬楼梯 (进阶)、322. 零钱兑换、279.完全平方数。

1、70.爬楼梯 (进阶)

1.1 思路

1、如何推导出递推公式 dp[i] += dp[i - j]

例如,dp[i] = dp[i-1] + dp[i-2],当 j 等于1时,表示你可以从第 i-1 阶走一步到达第 i 阶;当 j 等于2时,表示你可以从第 i-2 阶走两步到达第 i 阶,以此类推。

2、为什么70.爬楼梯也是完全背包问题?

其实本题稍加改动就是一道面试好题。

改为:一步一个台阶,两个台阶,三个台阶,.......,直到 m个台阶。问有多少种不同的方法可以爬到楼顶呢?

1阶,2阶,.... m阶就是物品,楼顶就是背包。

每一阶可以重复使用,例如跳了1阶,还可以继续跳1阶。

问跳到楼顶有几种方法其实就是问装满背包有几种方法。

此时大家应该发现这就是一个完全背包问题了

  • dp 数组以及下标的含义

    • dp[i]:爬到有i个台阶的楼顶,有dp[i]种方法
  • 递推公式

    • dp[i] += dp[i - j]
  • dp 数组如何初始化

    • dp[0] 一定为1

      • dp[0]是递归中一切数值的基础所在,如果dp[0]是0的话,其他数值都是0了
    • 下标非0的dp[i]初始化为0

  • dp 数组遍历顺序

    • 这是背包里求排列问题,即:1、2 步 和 2、1 步都是上三个台阶,但是这两种方法不一样!
  • 举例推导dp数组

1.2 代码

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

2、322. 零钱兑换

2.1 思路

1、为什么需要初始化dp数组为最大值?

初始化dp数组为最大值的目的是为了确保任何无法通过零钱兑换方式达到的金额状态都能被正确标识为无法到达,因为最大值通常表示无穷大,即无法兑换成功

具体而言,如果我们将dp数组初始化为一个很大的正整数(例如正无穷大),在状态转移时,如果某个状态没有有效的零钱组合可以到达,那么通过状态转移方程计算后,该状态仍然会保持为正无穷大,从而表示无法兑换成功。这有助于我们在最终的计算结果中正确识别无法兑换的情况。

2、动态规划完全背包中为什么初始化过程中有的是 dp[0] = 0、有的是 dp[0] = 1?

  1. 初始化为 dp[0] = 0:

    • 一般情况下,当你想要解决一个典型的背包问题,其中背包的容量为0时,不应该有任何价值,因为背包是空的。因此,在这种情况下,你应该将 dp[0] 初始化为0,表示背包容量为0时,能够获得的最大价值也为0。这是典型的背包问题的初始化方式,如 0-1 背包问题。
  2. 初始化为 dp[0] = 1:

    • 有些特殊的完全背包问题可能要求你在背包容量为0时也能够获得一定的价值,这通常涉及到问题的变种或约束。 例如,假设你有一个硬币找零问题,要求你找零0元时也有一种方法(不拿硬币),那么你可以将 dp[0] 初始化为1,表示找零0元也有一种方法,即不拿硬币。这种情况下,背包容量为0时已经包含了一种解决方案。

动规五部曲

  • dp 数组以及下标的含义

    • dp[j] 代表可以凑成总金额 j 所需最少硬币个数为 dp[j]
  • 递推公式

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

    • 首先凑足总金额为0所需钱币的个数一定是0,那么dp[0] = 0;

    • 所以下标非0的元素都是应该是最大值。

      • 考虑到递推公式的特性,dp[j]必须初始化为一个最大的数,否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中被初始值覆盖。
  • dp 数组遍历顺序

    • 本题求钱币最小个数,那么钱币有顺序和没有顺序都可以,都不影响钱币的最小个数
    • 所以本题并不强调集合是组合还是排列。
  • 举例推导dp数组

    • 322.零钱兑换

2.2 代码

    public int coinChange(int[] coins, int amount) {
        int max = Integer.MAX_VALUE;
        int[] dp = new int[amount + 1];
        Arrays.fill(dp, max);
        //当金额为0时需要的硬币数目为0
        dp[0] = 0;
​
        for(int i=0; i<coins.length; i++) {
            for(int j=1; j<=amount; j++) {
                if(j >= coins[i] && dp[j - coins[i]] != max) {
                    dp[j] = Math.min(dp[j], dp[j-coins[i]] + 1);
                }
            }
        }
        return dp[amount] != max ? dp[amount] : -1;
    }

3、279.完全平方数

3.1 思路

我来把题目翻译一下:完全平方数就是物品(可以无限件使用),凑个正整数n就是背包,问凑满这个背包最少有多少物品?

感受出来了没,这么浓厚的完全背包氛围,而且和昨天的题目动态规划:322. 零钱兑换就是一样一样的!

1、如何理解递推公式dp[j] = Math.min(dp[j], dp[j - i*i] + 1)

从 1 开始遍历到 n,对每个整数 i,执行以下操作:

a. 对于每个正整数 j,计算 dp[i - j * j] + 1。这表示如果我们选择了一个完全平方数 j^2,那么问题就变成了求解 dp[i - j^2] + 1。

b. 更新 dp[i] 为上述计算中的最小值

  • 动规五部曲

    • dp 数组以及下标的含义

      • dp[j] 代表和为 j 的完全平方数的最少数量 dp[j]
    • 递推公式

      • dp[j] = Math.min(dp[j], dp[j - i*i] + 1)
    • dp 数组如何初始化

      • dp[0] = 0;其他初始化为最大值

        • 题目描述中可没说要从0开始,dp[0]=0完全是为了递推公式
        • 所以非0下标的dp[j]一定要初始为最大值,这样dp[j]在递推的时候才不会被初始值覆盖
    • dp 数组遍历顺序

      • 完全背包
    • 举例推导dp数组

      • 322.零钱兑换

3.2 代码

    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=0; i*i<=n; i++) {
            for(int j=i*i; j<=n; j++) {
                if(dp[j-i*i] != max) {
                    dp[j] = Math.min(dp[j], dp[j-i*i] + 1);
                }
            }
        }
        return dp[n];
    }

参考资料

代码随想录-70.爬楼梯 (进阶)

代码随想录-322. 零钱兑换

代码随想录-279.完全平方数