找啊找啊找GF

用二维 01 背包同时记录最多能泡到的 MM 数和对应的最少时间。

OJ: luogu

题目 ID: P1509

难度:普及/提高-

标签:动态规划01背包背包

日期: 2026-06-19 16:22

题意

n 个 MM,每个 MM 有三个属性:

  • 需要花的钱 rmb_i
  • 需要花的人品 rp_i
  • 搞定她需要的时间 time_i

sqybi 只有 m 块大洋和 r 点人品。 他想在不超过这两个限制的前提下,尽量多泡到 MM; 如果能泡到的数量一样多,就希望总时间更少。

这张表把题意翻成了背包模型:

原题对象 背包含义
一个 MM 一个 0/1 物品
钱和人品 双重容量
泡到的数量 第一优先级
总时间 第二优先级

思路

一句话本质:二维费用的 01 背包,状态是 (cnt,time)(cnt, time) 二元组,先比人数再比时间。

先看最直接的暴力:

py
import sys

data = list(map(int, sys.stdin.buffer.read().split()))
n = data[0]
mms = []
idx = 1
for _ in range(n):
    rmb, rp, time = data[idx], data[idx + 1], data[idx + 2]
    idx += 3
    mms.append((rmb, rp, time))
m, r = data[idx], data[idx + 1]

best_cnt = 0
best_time = 0
for mask in range(1 << n):
    cnt = 0
    tot_rmb = 0
    tot_rp = 0
    tot_time = 0
    for i in range(n):
        if mask >> i & 1:
            rmb, rp, time = mms[i]
            tot_rmb += rmb
            tot_rp += rp
            tot_time += time
            cnt += 1
    if tot_rmb <= m and tot_rp <= r:
        if cnt > best_cnt or (cnt == best_cnt and tot_time < best_time):
            best_cnt = cnt
            best_time = tot_time

print(best_time)

brute.py 枚举 2n2^n 种选择,每个 MM 选或不选,检查两个资源限制,记录最优 (cnt,time)(cnt, time)nn 最大 10010021002^{100} 不可能。

为什么一个人就对应一个 0/1 物品,却需要二维背包?

普通 01 背包只有一个容量(比如总钱数)。但这题有两个独立限制:钱 mm 和人品 rr。选一个 MM 同时消耗两种资源,所以状态必须有两个维度。

两个目标(数量和时间)怎么比较?

先最大化 MM 数量,数量相同时最小化时间。这和普通 max 不同——不能只用一个大整数比较。需要把状态写成一个带优先级的 pair:dp[j][k].cntdp[j][k].cnt 越大越好,cntcnt 相同时 dp[j][k].timedp[j][k].time 越小越好。

转移时怎么判断"更优"?

设当前 MM 花费 rmbirmb_irpirp_itimeitime_i。选她的候选状态是:

  • cnt=dp[jrmbi][krpi].cnt+1cnt' = dp[j-rmb_i][k-rp_i].cnt + 1
  • time=dp[jrmbi][krpi].time+timeitime' = dp[j-rmb_i][k-rp_i].time + time_i

如果 cntcnt' 比当前 dp[j][k].cntdp[j][k].cnt 大,或者相等但 timetime' 更小,就更新。两个容量 j,kj,k 都倒序枚举(因为每个 MM 只能选一次)。最终答案 =dp[m][r].time= dp[m][r].time

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXR = 105;

struct State {
    int cnt;
    int time;
    State() : cnt(0), time(0) {}
    State(int c, int t) : cnt(c), time(t) {}
    bool better_than(const State& o) const {
        if (cnt != o.cnt) return cnt > o.cnt;
        return time < o.time;
    }
};

int n, m, r;
int rmb[105], rp[105], tmm[105];
// dp[a][b] 表示花 a 元 RMB、b 点 RP 时,能泡到的最多 MM 数及最少时间。
State dp[MAXR][MAXR];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> rmb[i] >> rp[i] >> tmm[i]; // 花费 RMB、RP,所需时间
    }
    cin >> m >> r;                         // 总 RMB 和总 RP

    // 0/1 背包:二维费用,倒序枚举两维。
    for (int i = 1; i <= n; i++) {
        for (int a = m; a >= rmb[i]; a--) {
            for (int b = r; b >= rp[i]; b--) {
                State nxt(dp[a - rmb[i]][b - rp[i]].cnt + 1,
                          dp[a - rmb[i]][b - rp[i]].time + tmm[i]);
                if (nxt.better_than(dp[a][b])) {
                    dp[a][b] = nxt;
                }
            }
        }
    }

    cout << dp[m][r].time << '\n';
    return 0;
}

复杂度

  • 时间复杂度:O(nmr)O(nmr)
  • 空间复杂度:O(mr)O(mr)

总结

这题本质上是二维 0/1 背包,只是比较方式不是单纯的“最大值”:

  • 先比能选到的 MM 数量
  • 再比总时间

遇到这种“先最大化数量,再最小化代价”的题,就可以把状态写成一个带优先级的 pair,然后做标准倒序 0/1 背包。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析