每个好友打不打都获经验,按药水量做 01 背包变体——打输也得 lose_i 经验,dp[j]=max(dp[j]+lose_i,dp[j-use_i]+win_i)。
OJ: luogu
题目 ID: P1802
难度:普及-
标签:动态规划01背包背包
日期: 2026-08-08 23:13
题意
有
- 失败时获得经验
- 胜利时获得经验
,需要消耗至少 瓶药水
每瓶药水只能用一次。求最大经验值
| 原题对象 | 背包含义 |
|---|---|
| 一个好友 | 一个物品 |
| 药水消耗 |
物品重量 |
| 失败经验 |
不选时的"保底收益" |
| 胜利经验 |
选时的收益 |
| 药水总数 |
背包容量 |
思路
一句话本质: 01 背包变体——每个好友不打也有收益(
先看最直接的暴力:
// brute.cpp:小数据暴力解,使用 01 序列枚举每个好友打或不打。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int n; // 好友数量
int x; // 药水数量
int lose_exp[MAXN]; // 失败时获得的经验
int win_exp[MAXN]; // 胜利时获得的经验
int need[MAXN]; // 打过至少需要的药水数量
int choose_friend[MAXN]; // choose_friend[i] = 0/1,表示不打/打第 i 个好友
long long best_answer; // 最大经验值
// 检查当前选择方案的总药水消耗是否不超过 x
bool check() {
int total_need = 0;
for (int i = 1; i <= n; i++) {
if (choose_friend[i] == 1) total_need += need[i];
}
return total_need <= x;
}
// 计算当前选择方案的总经验
long long calc_answer() {
long long total = 0;
for (int i = 1; i <= n; i++) {
if (choose_friend[i] == 1)
total += win_exp[i];
else
total += lose_exp[i];
}
return total;
}
// 01 序列递归枚举
void dfs(int dep) {
if (dep == n + 1) {
if (check()) {
long long cur = calc_answer();
if (best_answer < cur) best_answer = cur;
}
return;
}
// 第 dep 个好友:0 不打,1 打
for (int i = 0; i <= 1; i++) {
choose_friend[dep] = i;
dfs(dep + 1);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> x;
for (int i = 1; i <= n; i++) {
cin >> lose_exp[i] >> win_exp[i] >> need[i];
}
best_answer = 0;
dfs(1);
cout << 5LL * best_answer << '\n';
return 0;
}这个暴力用 01 序列枚举每个好友打或不打,叶子节点检查总药水消耗是否超限,并统计总经验。复杂度
和标准 01 背包相比,这题多出了什么?
标准 01 背包:"不选"收益为 0,"选"收益为
不选也有收益,对 DP 状态有什么影响?
在 01 背包的一维倒序框架里,每轮处理物品时,无论如何都会经历"不选"的路径。所以可以把"不打"收益直接加到当前状态上:
为什么
倒序枚举
为什么 dp 需要用 long long?
DP 公式
设
对于
最终输出
样例 DP 表格
以官方样例为例:
| 处理好友 | 参数 | |
|---|---|---|
| 初始 | — | |
| 好友 1 | (21,52,1) | 每个 |
| 好友 2 | (21,70,5) | |
| 好友 3 | (21,48,2) | … |
| 好友 4 | (14,38,3) | … |
| 好友 5 | (14,36,1) | … |
| 好友 6 | (14,36,2) | … |
最终
答案
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
const int MAXX = 1005;
int n; // 好友数量
int x; // 药水数量
int lose_exp[MAXN]; // 失败时获得的经验
int win_exp[MAXN]; // 胜利时获得的经验
int need[MAXN]; // 打过至少需要的药水数量
long long dp[MAXX]; // dp[j] = 使用 j 瓶药水能获得的最大经验值
void read_input() {
cin >> n >> x;
for (int i = 1; i <= n; i++) {
cin >> lose_exp[i] >> win_exp[i] >> need[i];
}
}
void solve() {
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++) {
// 倒序枚举药水数,保证每个好友最多只打一次。
// 使用 long long 防止中间结果溢出。
for (int j = x; j >= 0; j--) {
// 不打这个好友 → 获得失败经验
dp[j] = dp[j] + lose_exp[i];
// 打这个好友 → 比较胜利经验是否更大
if (j >= need[i]) {
// dp[j - need[i]] 是本轮更新前的值,即上一轮的结果
dp[j] = max(dp[j], dp[j - need[i]] + win_exp[i]);
}
}
}
// 题目要求输出 5 × 最大经验
cout << 5LL * dp[x] << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
solve();
return 0;
}复杂度
- 时间复杂度:
- 空间复杂度:
总结
这题是 01 背包的一个常见变体——每个物品"不选"也有收益。处理方法是在每轮倒序转移前,给所有状态加上