把每种菜看成只能选一次的物品,倒序做计数 0/1 背包,统计恰好花完 M 元的方案数。
OJ: luogu
题目 ID: P1164
难度:普及-
标签:动态规划01背包背包python
日期: 2026-06-19 14:47
题意
有 N 种菜,每种菜只有一份,价格为 a_i。手里有 M 元,问有多少种选菜方案,使总价格恰好等于 M。
思路
先看最直接的暴力:
// brute.cpp:小数据暴力解,使用 01 序列枚举每种菜点或不点。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 105;
int n; // 菜品种类数
int money; // 需要恰好花掉的钱
int a[MAXN]; // 每种菜的价格
int choose_dish[MAXN]; // choose_dish[i] = 0/1,表示第 i 种菜不点/点
int answer_count; // 当前方案数
int calc_money() {
int sum_money = 0;
for (int i = 1; i <= n; i++) {
if (choose_dish[i] == 1) sum_money += a[i];
}
return sum_money;
}
void dfs_choose(int dep) {
if (dep == n + 1) {
if (calc_money() == money) {
answer_count++;
}
return;
}
// 第 dep 种菜的 01 选择:0 不点,1 点。
for (int i = 0; i <= 1; i++) {
choose_dish[dep] = i;
dfs_choose(dep + 1);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> money;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
answer_count = 0;
dfs_choose(1);
cout << answer_count << '\n';
return 0;
}brute.cpp 用 01 序列枚举每种菜点或不点,叶子节点检查总价是否恰好等于
一句话本质: 01 背包的计数版本——dp[0]=1,dp[j] += dp[j-price],倒序保证每道菜只选一次。
和求最大价值的 01 背包比,计数版本需要改什么?
求最大值用的是 max,计数用的是加法。
怎么保证"恰好"而不是"不超过"?
初始化只有
为什么倒序?
每道菜只有一份。倒序保证
这是 0/1 背包的计数版本。
设 dp[money] 表示已经处理过若干道菜后,恰好花掉 money 元的方案数。
初始化:
dp[0] = 1表示什么都不点时,花 0 元有一种方案。
处理价格为 price 的菜时,如果点这道菜,那么所有原本恰好花 money - price 元的方案,都能扩展成恰好花 money 元的方案:
dp[money] += dp[money - price]因为每道菜只能点一次,所以 money 必须倒序枚举。
样例 DP 表格
样例价格为 1,1,2,2,目标 4:
| 处理菜品 | dp[0] | dp[1] | dp[2] | dp[3] | dp[4] |
|---|---|---|---|---|---|
| 初始 | 1 | 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 0 | 0 | 0 |
| 1 | 1 | 2 | 1 | 0 | 0 |
| 2 | 1 | 2 | 2 | 2 | 1 |
| 2 | 1 | 2 | 3 | 4 | 3 |
答案为 dp[4] = 3。
Python 知识
dp = [0] * (target + 1)创建一维背包数组。range(target, price - 1, -1)是倒序枚举金额,保证每道菜只用一次。- Python 整数能直接保存答案;题目也保证答案不超过 32 位整数。
参考笔记:
/home/rainboy/mycode/hugo-blog/content/program_language/python/input_output_and_strings.md/home/rainboy/mycode/hugo-blog/content/program_language/python/math_tools.md
代码
n, target = map(int, input().split())
prices = list(map(int, input().split()))
dp = [0] * (target + 1)
dp[0] = 1
for price in prices:
for money in range(target, price - 1, -1):
dp[money] += dp[money - price]
print(dp[target])C++ 解法
用 f[0] = 1 初始化,f[j] += f[j-w[i]] 倒序转移,统计恰好花完 M 元的方案数,和 Python 版思路完全一致:
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2024-07-13 11:10
* update_at: 2026-09-06 20:40
*/
//Author by [Rainboy](https://github.com/rainboylvx)
//date: 2024-07-13 11:10:15
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6+5;
int n,m;
int w[maxn];
int f[10005];
int main (int argc, char *argv[]) {
std::cin >> n >> m;
for(int i = 1;i <= n ;++i ) // i: 1->n
{
cin >> w[i];
}
f[0] = 1;
for(int i = 1;i <= n ;++i ) // i: 1->n
{
for(int j =m ;j>=w[i];j--)
{
f[j] += f[j-w[i]];
}
}
std::cout << f[m] << "\n";
return 0;
}复杂度
时间复杂度为
总结
看到“每个物品最多选一次,恰好凑出某个和,问方案数”,就可以往计数 0/1 背包上想。
一图流解析
保留已有一图流图片,作为读完正文后的复盘。
