动态规划 | part05

518. 零钱兑换 II - 力扣(LeetCode)

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

示例 1:

  • 输入: amount = 5, coins = [1, 2, 5]
  • 输出: 4

解释: 有四种方式可以凑成总金额:

  • 5=5
  • 5=2+2+1
  • 5=2+1+1+1
  • 5=1+1+1+1+1

示例 2:

  • 输入: amount = 3, coins = [2]
  • 输出: 0
  • 解释: 只用面额2的硬币不能凑成总金额3。

示例 3:

  • 输入: amount = 10, coins = [10]
  • 输出: 1

注意,你可以假设:

  • 0 <= amount (总金额) <= 5000

  • 1 <= coin (硬币面额) <= 5000

  • 硬币种类不超过 500 种

  • 结果符合 32 位符号整数

    public int change(int amount, int[] coins) {
    // dp[i] 表示凑成金额 i 的组合数
    int[] dp = new int[amount + 1];
    dp[0] = 1;

    复制代码
      // 外层循环遍历硬币(物品)
      for (int i = 0; i < coins.length; i++) {
          // 内层循环遍历金额(背包容量),正序遍历
          for (int j = coins[i]; j <= amount; j++) {
              // 状态转移:使用当前硬币coins[i]的组合数
              dp[j] += dp[j - coins[i]];
          }
      }
      return dp[amount];

    }

解题:

dp数组代表装满背包的方式种类。我们的初始化dp[0]设定为1,这是后续递推要的。我们的递推公式如下:dp[j] += dp[j - coins[i]],类似于组合和那道题目。

这是一个完全背包问题 :硬币可以无限使用 → 完全背包求组合数 → 计数类DP,注意:这里求的是组合(不考虑顺序),不是排列。

关键! 外层循环硬币、内层循环金额,得到的是组合数(不考虑顺序):

  • 例如 coins=[1,2]amount=3
  • 只会统计 (1,1,1)(1,2),不会把 (2,1) 算作另一种

如果外层循环金额、内层循环硬币,得到的是排列数(考虑顺序)。


我来详细解释为什么交换遍历顺序会产生排列(会出现(2,1)这种情况)。

核心区别:组合 vs 排列

|----------|------------------------------|----------------------------|
| | 组合 | 排列 |
| 定义 | 不考虑顺序,{1,2}{2,1} 算同一种 | 考虑顺序,[1,2][2,1] 算不同 |
| 外层循环 | 硬币 | 金额 |
| 内层循环 | 金额 | 硬币 |


图示对比

假设 coins = [1, 2]amount = 3

✅ 外层循环硬币(组合)------ 代码原写法

复制代码
for (int coin : coins) {        // 先固定硬币1,再硬币2
    for (int j = coin; j <= amount; j++) {
        dp[j] += dp[j - coin];
    }
}

执行过程:

|------|---------------------------|-------------|---------------|
| 步骤 | 操作 | dp数组变化 | 含义 |
| 初始 | - | [1,0,0,0] | 只有金额0有1种方法 |
| 用硬币1 | 更新dp[1],dp[2],dp[3] | [1,1,1,1] | 全用1:(1,1,1) |
| 用硬币2 | 更新dp[2],dp[3] | [1,1,2,2] | 加(1,2) |

结果 = 2种组合(1,1,1)(1,2)

关键 :硬币1处理完才处理硬币2,不可能出现2在1前面


❌ 外层循环金额(排列)

复制代码
for (int j = 1; j <= amount; j++) {     // 先固定金额
    for (int coin : coins) {            // 再遍历硬币
        if (j >= coin) dp[j] += dp[j - coin];
    }
}

执行过程:

|-----|------|-----------------------------------|---------------------------|
| 金额j | 遍历硬币 | 计算dp[j] | 得到的方式 |
| 1 | 1,2 | dp[1]=dp[0]=1 | (1) |
| 2 | 1,2 | dp[2]=dp[1]+dp[0]=1+1=2 | (1,1)(2) |
| 3 | 1,2 | dp[3]=dp[2]+dp[1]=2+1=3 | (1,1,1)(1,2)(2,1) |

结果 = 3种排列(1,1,1)(1,2)(2,1)


根本原因:状态定义不同

组合(外层硬币)

复制代码
dp[j] = 用[已遍历的硬币]凑成j的组合数
  • 第1轮:dp[j] = 只用硬币1凑成j
  • 第2轮:dp[j] = 用硬币1和2凑成j(但2只能在1后面添加)

强制顺序 :硬币必须按[1,2,5]的顺序出现,不会出现21前面

排列(外层金额)

复制代码
dp[j] = 凑成j的排列数(任意硬币都可以作为最后一个)
  • 凑成3时,可以是[1]+凑成2的任意排列,也可以是[2]+凑成1的任意排列
  • 不关心前面用了什么硬币,所以[1,2][2,1]都算

形象比喻

想象你在搭积木凑高度3:

|----|----------------------|-------------|
| 方式 | 组合视角 | 排列视角 |
| 规则 | 按顺序拿积木(先拿1,再拿2) | 每次随便拿,只要高度够 |
| 过程 | 必须先拿1,才能拿2 | 第1步可以拿1或2 |
| 结果 | 只有 (1,1,1)(1,2) | 还有 (2,1) |


总结公式

|-------------|---------------------------|
| 目标 | 代码结构 |
| 组合数(本题) | for coin : for amount ✅ |
| 排列数 | for amount : for coin ❌ |

这就是为什么LeetCode 518(零钱兑换II)要求组合数,必须用外层循环硬币


377. 组合总和 Ⅳ - 力扣(LeetCode)

给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。

示例:

  • nums = [1, 2, 3]
  • target = 4

所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1)

请注意,顺序不同的序列被视作不同的组合。

因此输出为 7。

复制代码
public int combinationSum4(int[] nums, int target) {
    int[] dp = new int[target + 1];
    dp[0] = 1;
    for (int i = 1; i <= target; i++) {
        for (int j = 0; j < nums.length; j++) {
            if (i >= nums[j]) {
                dp[i] += dp[i - nums[j]];
            }
        }
    }
    return dp[target];
}

解题:

本题与上一题目的区别在于本题在求排列数,所以只需要交换遍历顺序即可,先遍历背包,再遍历物品。

57. 爬楼梯(第八期模拟笔试)

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

每次你可以爬至多m (1 <= m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

输入描述:输入共一行,包含两个正整数,分别表示n, m

输出描述:输出一个整数,表示爬到楼顶的方法数。

输入示例:3 2

输出示例:3

提示:

当 m = 2,n = 3 时,n = 3 这表示一共有三个台阶,m = 2 代表你每次可以爬一个台阶或者两个台阶。

此时你有三种方法可以爬到楼顶。

  • 1 阶 + 1 阶 + 1 阶段

  • 1 阶 + 2 阶

  • 2 阶 + 1 阶

    import java.util.Scanner;
    class climbStairs{
    public static void main(String [] args){
    Scanner sc = new Scanner(System.in);
    int m, n;
    while (sc.hasNextInt()) {
    // 从键盘输入参数,中间用空格隔开
    n = sc.nextInt();
    m = sc.nextInt();

    复制代码
              // 求排列问题,先遍历背包再遍历物品
              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 >= 0) dp[j] += dp[j - i];
                  }
              }
              System.out.println(dp[n]);
          }
      }

    }

解题:

这是一个完全背包问题+排列问题。

相关推荐
GuangHeAI_ATing1 小时前
国密算法SSD怎么选?这3款国产固态硬盘安全又高速
算法
雨泪丶2 小时前
代码随想录算法训练营-Day34
算法
Yzzz-F2 小时前
牛客寒假算法训练营2
算法
甄心爱学习2 小时前
【python】获取所有长度为 k 的二进制字符串
python·算法
iAkuya3 小时前
(leetcode)力扣100 76数据流的中位数(堆)
算法·leetcode·职场和发展
键盘鼓手苏苏3 小时前
Flutter for OpenHarmony: Flutter 三方库 ntp 精准同步鸿蒙设备系统时间(分布式协同授时利器)
android·分布式·算法·flutter·华为·中间件·harmonyos
董董灿是个攻城狮3 小时前
AI 视觉连载5:传统 CV 之均值滤波
算法
多恩Stone3 小时前
【3D-AICG 系列-11】Trellis 2 的 Shape VAE 训练流程梳理
人工智能·pytorch·算法·3d·aigc
lintax4 小时前
计算pi值-积分法
python·算法·计算π·积分法