先枚举金额再枚举纸币做完全背包计数,不同支付顺序视为不同方案,dp[j]=(dp[j]+dp[j-v])%MOD。
OJ: luogu
题目 ID: P2840
难度:普及-
标签:动态规划完全背包背包计数
日期: 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; // 方案数
// remain:还需要凑的金额
// 每次递归选择一张纸币,继续凑剩余金额
void dfs(int remain) {
if (remain == 0) {
answer = (answer + 1) % MOD;
return;
}
for (int i = 1; i <= n; i++) {
if (remain >= a[i]) {
dfs(remain - a[i]);
}
}
}
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(w);
cout << answer << '\n';
return 0;
}这个暴力从
这个递归在反复计算什么?
对于同一个剩余金额 remain,无论之前经历了哪条路径到达这里,后续的支付序列选择是完全一样的。例如凑 10 元,先付 1 再付 9 和先付 9 再付 1,两种路径都会到达 remain=0,但"从 10 到 0"的所有序列被重复枚举了很多次。
怎么避免重复枚举,同时又要保留"顺序不同算不同方案"的要求?
设
这个求和就是先枚举金额
为什么不能先枚举纸币再枚举金额?
如果先枚举纸币再枚举金额(
DP 公式
设
先枚举
样例 DP 表格
以样例 2 为例:
| 初始 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| — | — | — | — | — | — | ||
| — | — | — | — | — | — | ||
| — | — | — | — | — | — | ||
| — | — | — | — | — | — | ||
| — | — | — | — | — | — | ||
| — | — | — | — | — | — | ||
| — | — | — | — | — | — |
最终
代码
#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 j = 1; j <= w; j++) {
for (int i = 1; i <= n; i++) {
if (j >= a[i]) {
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;
}复杂度
- 时间复杂度:
- 空间复杂度:
总结
这题的关键是区分"排列计数"与"组合计数"的循环顺序:
- 先金额后纸币(本题): 排列,不同顺序算不同方案
- 先纸币后金额(纸币问题 3): 组合,不同顺序合并为同一种
两者代码几乎一模一样,只是两层循环交换了位置。把这一对题目放在一起对比,是理解完全背包计数循环含义的最好方式。