先枚举纸币再枚举金额做完全背包计数,不同支付顺序合并为同一种组合,dp[j]=(dp[j]+dp[j-v])%MOD。
OJ: luogu
题目 ID: P2834
难度:普及-
标签:动态规划完全背包背包计数
日期: 2026-08-08 23:13
题意
有
| 原题对象 | 背包含义 |
|---|---|
| 一种纸币 | 一个可以重复使用的物品 |
| 面额 |
物品体积 |
| 纸币组合 | 无序组合(多重集) |
| 凑出金额 |
恰好装满容量 |
思路
一句话本质: 求完全背包的组合计数——先枚举纸币再枚举金额,强制编号顺序,把不同排列合并为同一种组合。
先看最直接的暴力:
// brute.cpp:小数据暴力解,递归枚举每种纸币的使用张数,统计组合数(顺序不同算同一种)。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
const int MOD = 1000000007;
int n; // 纸币种类数
int w; // 要凑出的金额
int a[MAXN]; // 每种纸币的面额
int answer; // 方案数
// dep:当前正在决定第 dep 种纸币的使用张数
// remain:剩余需要凑出的金额
void dfs(int dep, int remain) {
if (dep == n + 1) {
if (remain == 0)
answer = (answer + 1) % MOD;
return;
}
// 枚举第 dep 种纸币用 cnt 张
int max_cnt = remain / a[dep];
for (int cnt = 0; cnt <= max_cnt; cnt++) {
dfs(dep + 1, remain - cnt * a[dep]);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> w;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
answer = 0;
dfs(1, w);
cout << answer << '\n';
return 0;
}这个暴力按纸币编号顺序,对每种纸币枚举使用张数。递归到叶子如果剩余金额恰好为 0,就找到一种组合。因为纸币是按编号顺序处理的,同一种组合只会被枚举一次。
这个暴力和纸币问题 2 的暴力有什么不同?
纸币问题 2 的暴力每次任选一张纸币继续递归,同一个组合会因支付顺序不同被枚举多次。本题暴力按编号顺序依次决定每种纸币的张数,天然保证了每种组合只被计数一次。但复杂度仍然是每种纸币枚举张数,对于
按编号顺序处理纸币这个约束,怎么在 DP 中体现?
设
为什么正序枚举
每种纸币无限张,
这和纸币问题 2 的循环顺序有什么本质区别?
- 先枚举金额
,再枚举纸币 :最后一步可以是任意纸币, 中包含以任何纸币结尾的方案,不同顺序自然被认为是不同方案 → 排列 - 先枚举纸币
,再枚举金额 :纸币按编号顺序被依次使用,最后一步只能是当前或编号更小的纸币,不同顺序被合并 → 组合
DP 公式
设
先枚举纸币
样例 DP 表格
以样例 2 为例:
| 处理纸币 | 面额 | ||||||||
|---|---|---|---|---|---|---|---|---|---|
| 初始 | — | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 纸币 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| 纸币 2 | 5 | 1 | 1 | ||||||
| 纸币 3 | 11 | 1 | 1 | 2 | 2 | 2 |
答案
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
const int MAXW = 10005;
const int MOD = 1000000007;
int n; // 纸币种类数
int w; // 要凑出的金额
int a[MAXN]; // 每种纸币的面额
int dp[MAXW]; // dp[j] = 凑出金额 j 的组合数(组合,不同顺序算同一种)
void read_input() {
cin >> n >> w;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
}
void solve() {
memset(dp, 0, sizeof(dp));
dp[0] = 1;
// 先枚举纸币再枚举金额,这样不同顺序会被合并为同一种组合。
for (int i = 1; i <= n; i++) {
for (int j = a[i]; j <= w; j++) {
dp[j] = (dp[j] + dp[j - a[i]]) % MOD;
}
}
cout << dp[w] << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
solve();
return 0;
}复杂度
- 时间复杂度:
- 空间复杂度:
总结
纸币问题 2 和 3 对比了完全背包计数中循环顺序的决定性作用:
| 纸币问题 2(排列) | 纸币问题 3(组合) | |
|---|---|---|
| 外层循环 | 金额 |
纸币 |
| 内层循环 | 纸币 |
金额 |
| 首项 | ||
| 不同顺序 | 算不同方案 | 合并为一种 |
建议把这两题放在一起反复练习,直到能独立解释为什么交换循环顺序就改变了计数语义。