中等
采购方案(背包计数)
动态规划背包组合数学
题目描述
爱丽丝正在为制作新的人偶准备素材。她需要购买 n 种不同的素材,每种素材的单价分别为 a₁, a₂, …, aₙ。为了保证每一种人偶都能顺利完成,她必须为每种素材至少购买一个。
现在爱丽丝手中恰好有 b 元,她希望知道有多少种不同的采购方案,能够恰好耗尽这 b 元预算。需要注意的是,即使两种素材的单价相同,它们也被视为不同的素材种类。
数据范围:
- 0 ≤ b ≤ 100
- 1 ≤ n ≤ 10
- 1 ≤ aᵢ ≤ 50
示例
输入:
10
1 2
输出: 4
说明:
- 每种素材至少买一个,消耗 1 + 2 = 3 元,剩余 7 元
- 使用单价 1 和 2 凑 7 元的方案:
- 7 个单价 1
- 5 个单价 1 + 1 个单价 2
- 3 个单价 1 + 2 个单价 2
- 1 个单价 1 + 3 个单价 2
- 共 4 种方案
解题思路
核心:完全背包求方案数
-
预扣最小成本:每种素材至少买一个,总最小成本 = Σ aᵢ。剩余预算 = b - Σ aᵢ。若剩余 < 0,直接返回 0。
-
完全背包计数:dp[j] 表示剩余预算中凑出金额 j 的方案数。
- 初始化:dp[0] = 1
- 转移:对于每种素材
coin,正序遍历 j:dp[j] += dp[j - coin] - 正序遍历保证每种素材可以无限次使用(完全背包特性)
-
为什么外层循环遍历素材? 因为素材是不同的种类。外层循环决定物品放入顺序,考虑的是“前 i 种素材”的子问题。
解法对比
| 版本 | 描述 | 空间 |
|---|---|---|
| 二维 DP | dp[i][j]:前 i 种素材凑 j 元,枚举 k 个 |
O(n·b) |
| 一维 DP | 空间优化,dp[j] += dp[j-coin] 正序 |
O(b) |
| 01 背包 | 每种素材最多买一个,倒序遍历 | O(b) |
代码实现
/**
* 一维完全背包(推荐)
* 每种素材至少买一个 → 预扣成本 + 完全背包
*/
function getNumOfBuy(coins, amount) {
const leastCost = coins.reduce((a, b) => a + b, 0);
const remain = amount - leastCost;
if (remain < 0) return 0;
const dp = new Array(remain + 1).fill(0);
dp[0] = 1;
for (const coin of coins) {
for (let j = coin; j <= remain; j++) {
dp[j] += dp[j - coin];
}
}
return dp[remain];
}
/**
* 二维完全背包(更好理解)
*/
function getNumOfBuy2D(coins, amount) {
const leastCost = coins.reduce((a, b) => a + b, 0);
const remain = amount - leastCost;
if (remain < 0) return 0;
const n = coins.length;
const dp = Array.from({ length: n + 1 }, () => new Array(remain + 1).fill(0));
dp[0][0] = 1;
for (let i = 1; i <= n; i++) {
const coin = coins[i - 1];
for (let j = 0; j <= remain; j++) {
for (let k = 0; k * coin <= j; k++) {
dp[i][j] += dp[i - 1][j - k * coin];
}
}
}
return dp[n][remain];
}
/**
* 01背包版本(每种素材最多买一个, 仅作对比)
*/
function getNumOfBuy01(coins, amount) {
const leastCost = coins.reduce((a, b) => a + b, 0);
const remain = amount - leastCost;
if (remain < 0) return 0;
const dp = new Array(remain + 1).fill(0);
dp[0] = 1;
for (const coin of coins) {
for (let j = remain; j >= coin; j--) { // 倒序是关键区别
dp[j] += dp[j - coin];
}
}
return dp[remain];
}
复杂度分析
- 时间复杂度: O(n × remainBudget),n ≤ 10,remainBudget ≤ 100,非常快。
- 空间复杂度: O(b),一维 dp 数组。
示例输入 / 输出
下面给出一组输入输出示例,便于对照题意与结果。
编号 purchase-plan难度 中等
输入
10 1 2
输出
4