榨取kkksc03

把每个愿望看成价值为 1 的物品,用金钱和时间作为两维容量,做二维费用 0/1 背包求最多能完成多少个愿望。

OJ: luogu

题目 ID: P1855

难度:普及/提高-

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

日期: 2026-06-19 14:15

题意

n 个愿望。

i 个愿望需要:

  • m_i 的金钱
  • t_i 的时间

kkksc03 一共只有 M 元和 T 分钟。每个愿望最多完成一次,要求在总金钱和总时间都不超限的前提下,最多完成多少个愿望。

思路

一句话本质:每个愿望价值固定为 1,同时消耗金钱和时间两维资源——二维费用 01 背包,把"价值最大化"替换为"数量最大化"。

先看最直接的暴力:

cpp
// brute.cpp:小数据暴力解,使用 01 序列枚举每个愿望做或不做。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 105;

int n;                       // 愿望数量
int limit_money, limit_time; // 可用资源上限
int cost_money[MAXN];        // 每个愿望需要的金钱
int cost_time[MAXN];         // 每个愿望需要的时间
int choose_wish[MAXN];       // choose_wish[i] = 0/1,表示第 i 个愿望不做/做
int best_answer;             // 当前找到的最优答案

bool check() {
    int used_money = 0;
    int used_time = 0;
    for (int i = 1; i <= n; i++) {
        if (choose_wish[i] == 1) {
            used_money += cost_money[i];
            used_time += cost_time[i];
        }
    }
    return used_money <= limit_money && used_time <= limit_time;
}

int calc_answer() {
    int cnt = 0;
    for (int i = 1; i <= n; i++) {
        if (choose_wish[i] == 1) cnt++;
    }
    return cnt;
}

void dfs_choose(int dep) {
    if (dep == n + 1) {
        if (check()) {
            int value = calc_answer();
            if (best_answer < value) best_answer = value;
        }
        return;
    }

    // 第 dep 个愿望的 01 选择:0 不做,1 做。
    for (int i = 0; i <= 1; i++) {
        choose_wish[dep] = i;
        dfs_choose(dep + 1);
    }
}

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

    cin >> n >> limit_money >> limit_time;
    for (int i = 1; i <= n; i++) {
        cin >> cost_money[i] >> cost_time[i];
    }

    best_answer = 0;
    dfs_choose(1);

    cout << best_answer << '\n';
    return 0;
}

brute.cpp 把每个愿望看成一个 01 选择:choose_wish[i] = 0/1 表示不做或做。递归先生成完整选择,叶子节点再检查总金钱和总时间是否超限,并统计能完成的愿望数量。

这个做法容易理解,但复杂度是 O(2n)O(2^n),只能用于小数据验证。

和 P1507 NASA 食物计划相比,这题有什么不同?

两题都是二维费用 01 背包,区别在于"价值":

  • P1507:每个食品有独立的卡路里值(价值各不同),需要额外维护重量、质量、卡路里三个量
  • P1855:每个愿望的价值统一为 1(完成一个就算一个),物品的价值就是"1 个愿望",不需要额外数组

后者更简洁——dp[j][k] 直接存储"在当前资源限制下最多能完成几个愿望",转移时 +1 即可。

转移怎么写?

设 dp[j][k] 表示金钱不超过 j、时间不超过 k 时,最多能完成的愿望数。

加入愿望需要 m 金钱、t 时间:

  • 不选:dp[j][k] 不变
  • 选:dp[j][k] = max(dp[j][k], dp[j - m][k - t] + 1)

两维容量都倒序枚举,保证每个愿望最多完成一次。

状态表

这张表说明状态的含义:

状态 含义
dp[j][k] 金钱不超过 j、时间不超过 k 时,最多能完成多少个愿望

从这个状态定义可以看出,这题记录的是"资源上限下的最优值",而不是具体选了哪几个愿望。 也正因为如此,二维表已经足够表达最优解。

最后直接输出 dp[M][T] 即可。

DP 公式

dpj,kdp_{j,k} 表示金钱不超过 jj、时间不超过 kk 时,最多能完成多少个愿望。处理第 ii 个愿望时:

dpj,k=max(dpj,k, dpjmi,kti+1) dp_{j,k}=\max(dp_{j,k},\ dp_{j-m_i,k-t_i}+1)

其中 jmij\geqslant m_iktik\geqslant t_i。两个容量都倒序枚举,最终答案为:

dpM,T dp_{M,T}

公式解释:每个愿望最多做一次,同时消耗金钱和时间两个资源。选当前愿望时,必须从两个资源都扣掉后的状态转移过来,并让完成数量加一。

代码

cpp
/**
 * 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: 2026-08-08 23:13
 * update_at: 2026-08-08 23:13
 * 二维费用01背包,价值为1(愿望数)
 */
#include <bits/stdc++.h>
using namespace std;

const int maxv = 205;
int n, M, T;
int dp[maxv][maxv];

int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> M >> T;
    for (int i = 1; i <= n; ++i) {
        int m, t;
        cin >> m >> t;
        for (int j = M; j >= m; --j)
            for (int k = T; k >= t; --k)
                dp[j][k] = max(dp[j][k], dp[j - m][k - t] + 1);
    }
    cout << dp[M][T] << "\n";
    return 0;
}

复杂度

  • 时间复杂度:O(nMT)O(nMT)
  • 空间复杂度:O(MT)O(MT)

总结

看到下面这种结构时,可以直接往二维费用背包上靠:

  • 每个物品只能选一次
  • 同时受两种资源限制
  • 目标是最大化总收益

这题里"收益"就是完成愿望的个数,所以每个愿望的价值都看成 1,问题就变成一个标准模板。

一图流解析

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

一图流解析