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]的顺序出现,不会出现2在1前面
排列(外层金额)
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]); } }}
解题:
这是一个完全背包问题+排列问题。