5 倍经验日

每个好友打不打都获经验,按药水量做 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

题意

nn 个好友(n103n \leqslant 10^3),手上有 xx 瓶药水(x103x \leqslant 10^3)。对第 ii 个好友:

  • 失败时获得经验 loseilose_i
  • 胜利时获得经验 winiwin_i,需要消耗至少 useiuse_i 瓶药水

每瓶药水只能用一次。求最大经验值 ss,输出 5s5s

原题对象 背包含义
一个好友 一个物品
药水消耗 useiuse_i 物品重量
失败经验 loseilose_i 不选时的"保底收益"
胜利经验 winiwin_i 选时的收益
药水总数 xx 背包容量

思路

一句话本质: 01 背包变体——每个好友不打也有收益(loseilose_i),打需要消耗药水获得更多收益(winiwin_i)。

先看最直接的暴力:

cpp
// 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 序列枚举每个好友打或不打,叶子节点检查总药水消耗是否超限,并统计总经验。复杂度 O(2n)O(2^n),只适合小数据验证。

和标准 01 背包相比,这题多出了什么?

标准 01 背包:"不选"收益为 0,"选"收益为 valueivalue_i。本题:"不打"收益为 loseilose_i,"打"收益为 winiwin_i——不选也有收益。

不选也有收益,对 DP 状态有什么影响?

在 01 背包的一维倒序框架里,每轮处理物品时,无论如何都会经历"不选"的路径。所以可以把"不打"收益直接加到当前状态上:dp[j]=dp[j]+loseidp[j] = dp[j] + lose_i(表示无论药水够不够,都不打这个好友,拿走失败经验)。然后再考虑"打"的转移:如果 juseij \geqslant use_i,尝试 dp[j]=max(dp[j],dp[jusei]+wini)dp[j] = \max(dp[j], dp[j-use_i] + win_i)

为什么 dp[jusei]dp[j-use_i]jj 倒序枚举时是安全的?

倒序枚举 jj,计算 dp[j]dp[j]dp[jusei]dp[j-use_i] 还保留着上一轮(前 i1i-1 个好友)的旧值,没有被本轮更新覆盖。这保证了每个好友最多只打一次。

为什么 dp 需要用 long long?

winiwin_iloseilose_i 最大可达 10610^6n=103n = 10^3,总经验可达 10910^9 级别,超出 int 范围(23112.1×1092^{31}-1 \approx 2.1 \times 10^9 尚可,但加上中间累加使用 long long 更安全)。

DP 公式

dpjdp_j 表示使用不超过 jj 瓶药水的最大经验值。处理第 ii 个好友时(jj 倒序):

dpj=max(dpj+losei, dpjusei+wini)(jusei) dp_j = \max(dp_j + lose_i,\ dp_{j-use_i} + win_i) \quad (j \geqslant use_i)

对于 j<useij < use_i,只有不打一种选择:

dpj=dpj+losei dp_j = dp_j + lose_i

最终输出 5×dpx5 \times dp_x

样例 DP 表格

以官方样例为例:n=6,x=8n=6, x=8,好友数据(lose,win,use)(lose, win, use)依次为 (21,52,1),(21,70,5),(21,48,2),(14,38,3),(14,36,1),(14,36,2)(21,52,1), (21,70,5), (21,48,2), (14,38,3), (14,36,1), (14,36,2)

处理好友 参数 dp[8]dp[8] 变化说明
初始 dp[08]=0dp[0 \sim 8] = 0
好友 1 (21,52,1) 每个 jj+21+21(不打),j1j \geqslant 1 时尝试 dp[j1]+52dp[j-1]+52。例如 dp[8]=max(0+21,0+52)=52dp[8]=\max(0+21,0+52)=52
好友 2 (21,70,5) j<5j<5 只能 +21+21j5j \geqslant 5 尝试 dp[j5]+70dp[j-5]+70。例如 dp[8]=max(52+21,dp[3]+70)dp[8]=\max(52+21, dp[3]+70)
好友 3 (21,48,2)
好友 4 (14,38,3)
好友 5 (14,36,1)
好友 6 (14,36,2)

最终 dp[8]=212dp[8] = 212(不展示完整逐行表避免冗长;转移逻辑与标准 01 背包一致,区别仅在于每一行先加 loseilose_i)。

答案 5×212=10605 \times 212 = 1060

代码

cpp
#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;
}

复杂度

  • 时间复杂度:O(nx)O(nx)
  • 空间复杂度:O(x)O(x)

总结

这题是 01 背包的一个常见变体——每个物品"不选"也有收益。处理方法是在每轮倒序转移前,给所有状态加上 loseilose_i,再考虑选的情况。这种"保底收益+额外收益"的模式在竞赛中经常出现。