🎯 一句话理解
-
求组合数(不计顺序) → 外层遍历物品,内层遍历背包容量
-
求排列数(计顺序) → 外层遍历背包容量,内层遍历物品
🎲 举例说明
假设有硬币 [1, 2, 3]
,目标金额是 4
,要求凑成的方法数:
1. 组合数(顺序无关):
比如:[1, 2, 1]
和 [2, 1, 1]
、[1, 1, 2]
视为同一种方案,因为它们元素一样,只是顺序不同。
for i := 0; i < len(nums); i++ { // 物品在外层
for j := nums[i]; j <= target; j++ { // 容量在内层
dp[j] += dp[j - nums[i]]
}
}
这种写法只会把每组物品组合一次,不考虑排列方式。
2. 排列数(顺序有关):
比如:[1, 2, 1]
、[2, 1, 1]
、[1, 1, 2]
视为3种不同方案。
for j := 0; j <= target; j++ { // 容量在外层
for i := 0; i < len(nums); i++ { // 物品在内层
if j >= nums[i] {
dp[j] += dp[j - nums[i]]
}
}
}
这样每次容量增加时,都会把所有可放的物品都考虑一遍 ⇒ 排列自然产生了。
✅ 总结对比
目标 | 外层循环 | 内层循环 | 特点 |
---|---|---|---|
组合数(无序) | 遍历物品 i | 遍历容量 j | 顺序不会重复 |
排列数(有序) | 遍历容量 j | 遍历物品 i | 会计算所有顺序 |
🧠 记忆口诀
🎯 组合外层物品,排列外层背包。
详细解释 组合外层物品,排列外层背包
🎯 问题背景
我们有硬币 coins = [1, 2, 3]
,目标是金额 4
。我们想知道有多少种方案凑出金额 4:
🧮 什么是组合数(不计顺序)?
-
组合数不区分顺序:
[1,1,2]
、[2,1,1]
、[1,2,1]
是同一种组合。 -
所以我们只想知道「有哪些组合方式」,不在意顺序。
✅ 为什么组合数要先遍历物品?
我们看以下代码:
dp := make([]int, target+1)
dp[0] = 1
for i := 0; i < len(coins); i++ { // 外层:物品(硬币)
for j := coins[i]; j <= target; j++ { // 内层:背包容量
dp[j] += dp[j - coins[i]]
}
}
🌱 核心思想:
每次选一个硬币coins[i]
,我们更新所有能放它的位置,但每个 dp[j]
只会累加来自当前物品之前的组合。
这样做的结果是:
-
所有组合只计算一次,避免了顺序重复。
✍️ 举个例子
第一层循环:i = 0(coin = 1)
更新 dp[1]
, dp[2]
, dp[3]
, dp[4]
,只用 1 元硬币。
此时:
dp = [1 1 1 1 1]
表示只用 1 元可以组成的方法数,只有一种 [1,1,1,1]
。
第二层循环:i = 1(coin = 2)
我们再用 2 元更新,但只能在原有的基础上增加。
-
dp[2] += dp[0] →
[2]
-
dp[3] += dp[1] →
[1,2]
-
dp[4] += dp[2] →
[2,2]
更新后:
dp = [1 1 2 2 3]
说明方法数是:[1,1,1,1]
, [1,1,2]
, [2,2]
, 不会重复 [2,1,1]
、[1,2,1]
等顺序。
📦 如果调换循环顺序会怎样?
比如改为这样:
for j := 0; j <= target; j++ {
for i := 0; i < len(coins); i++ {
if j >= coins[i] {
dp[j] += dp[j - coins[i]]
}
}
}
这时每个 j
都会尝试所有硬币,所以:
-
dp[3]
会通过[1,2]
,也会通过[2,1]
,两次都被算入。 -
所以是排列数(考虑顺序)。
🔁 总结
目标 | 外层循环 | 结果特性 | 会不会重复顺序 |
---|---|---|---|
组合数 | 物品在外 | 不计顺序 | 不会 |
排列数 | 背包在外 | 顺序不同都算 | 会 |