把相邻两天的价格差当作收益,按每天做一次完全背包,逐步更新手里的金币。
OJ: luogu
题目 ID: P5662
难度:普及+/提高
标签:动态规划完全背包背包
日期: 2026-06-19 17:24
题意
有 T 天、N 种纪念品,初始有 M 枚金币。
第 i 天,第 j 种纪念品的价格是 P[i][j]。每天可以无限次买卖任意纪念品,同一天里卖掉得到的金币可以立刻继续买别的纪念品。
第 T 天结束后,所有纪念品都必须卖掉。
要求最后拥有的金币数尽量多。
这张表把题意翻成了背包模型:
| 原题对象 | 背包含义 |
|---|---|
| 当前金币数 | 背包容量 |
| 纪念品的买入价 | 物品重量 |
| 相邻两天的价格差 | 物品价值 |
| 每天可无限次买卖 | 完全背包 |
思路
先看一个可以直接验证正确性的朴素解:
#include <bits/stdc++.h>
using namespace std;
static int T, N;
static vector<vector<int>> price;
static vector<vector<int>> memo_day;
static vector<vector<vector<int>>> memo_pick;
static int pick_profit(int day, int idx, int money) {
if (idx == N) {
return 0;
}
int &res = memo_pick[day][idx][money];
if (res != -1) {
return res;
}
// 不选当前纪念品,直接看后面的种类。
res = pick_profit(day, idx + 1, money);
int cost = price[day][idx];
int gain = price[day + 1][idx] - price[day][idx];
// 枚举当前纪念品买 1 件、2 件、... 的情况。
for (int cnt = 1; cnt * cost <= money; ++cnt) {
res = max(res, pick_profit(day, idx + 1, money - cnt * cost) + cnt * gain);
}
return res;
}
static int solve(int day, int money) {
if (day == T - 1) {
return money;
}
int &res = memo_day[day][money];
if (res != -1) {
return res;
}
// 当前这一天能赚到的最大额外金币。
int gain = pick_profit(day, 0, money);
res = solve(day + 1, money + gain);
return res;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int M;
cin >> T >> N >> M;
price.assign(T, vector<int>(N));
for (int i = 0; i < T; ++i) {
for (int j = 0; j < N; ++j) {
cin >> price[i][j];
}
}
memo_day.assign(T, vector<int>(10005, -1));
memo_pick.assign(T, vector<vector<int>>(N + 1, vector<int>(10005, -1)));
cout << solve(0, M) << '\n';
return 0;
}下面是另一种「01 序列」风格的暴力写法。它把每一个小选择拆成
另一种暴力写法:01 序列
// brute_01_style.cpp:01 序列风格暴力,把每天能买的每一件纪念品看成选/不选。
#include <bits/stdc++.h>
using namespace std;
struct Choice {
int cost;
int gain;
};
const int MAXT = 105;
const int MAXN = 105;
int T, N, start_money;
int price[MAXT][MAXN];
vector<Choice> choices;
vector<int> choose_buy; // choose_buy[i] = 0/1,表示第 i 件候选纪念品不买/买
int current_money;
int best_gain;
bool check() {
int cost = 0;
for (int i = 0; i < (int)choices.size(); i++) {
if (choose_buy[i] == 1) cost += choices[i].cost;
}
return cost <= current_money;
}
int calc_gain() {
int gain = 0;
for (int i = 0; i < (int)choices.size(); i++) {
if (choose_buy[i] == 1) gain += choices[i].gain;
}
return gain;
}
void dfs_choice(int dep) {
if (dep == (int)choices.size()) {
if (check()) {
int value = calc_gain();
if (best_gain < value) best_gain = value;
}
return;
}
// 第 dep 件候选纪念品的 01 选择:0 不买,1 买。
for (int i = 0; i <= 1; i++) {
choose_buy[dep] = i;
dfs_choice(dep + 1);
}
}
int best_gain_one_day(int day, int money) {
choices.clear();
for (int item = 1; item <= N; item++) {
int cost = price[day][item];
int gain = price[day + 1][item] - price[day][item];
if (gain <= 0) {
continue;
}
// 同一种纪念品可以买多件,这里把每一件展开成一个 01 选择。
for (int cnt = 1; cnt * cost <= money; cnt++) {
choices.push_back({cost, gain});
}
}
choose_buy.assign(choices.size(), 0);
current_money = money;
best_gain = 0;
dfs_choice(0);
return best_gain;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> T >> N >> start_money;
for (int day = 1; day <= T; day++) {
for (int item = 1; item <= N; item++) {
cin >> price[day][item];
}
}
int money = start_money;
for (int day = 1; day < T; day++) {
money += best_gain_one_day(day, money);
}
cout << money << '\n';
return 0;
}brute.cpp 直接枚举每天能买多少件、怎么组合,能正确反映题意,但只适合小数据。
这题最关键的观察是:我们不需要关心“纪念品一直持有了几天”,只需要关心“在某一天和下一天之间,哪些纪念品值得持有”。
也就是说,考虑相邻两天 i 和 i + 1:
- 如果某个纪念品今天买入、明天卖出,那么每件的收益就是
P[i+1][j] - P[i][j] - 买入时消耗的是
P[i][j] - 同一天可以买很多件,所以是完全背包
这张表说明一个日间转移的含义:
| 状态 | 含义 |
|---|---|
money |
当前手里的金币 |
dp[x] |
用不超过 x 枚金币,在这一段时间里能额外赚到的最大金币 |
P[i][j] |
今天买入这一件的代价 |
P[i+1][j] - P[i][j] |
持有到明天再卖出的净收益 |
于是每天都可以做一次完全背包:
- 以当天手里的金币
money作为容量 - 对每个纪念品,重量是今天的价格,价值是明天比今天多赚的钱
- 跑完完全背包后,
dp[money]就是这一晚最多能多赚的金币 - 令
money += dp[money],进入下一天
如果某个纪念品的净收益不是正数,就不选它。 因为完全背包本来就允许“什么都不买”,所以这种情况直接跳过即可。
DP 公式
处理第
若
这一晚结束后:
公式解释:一天到下一天之间,买入价是容量消耗,价格差是收益。每天用当前金币做一次完全背包,算出这一晚最多能赚多少,再把收益加入金币进入下一天。
样例 1 DP 表格
以样例 1 为例:
| 天 |
买入价 |
价格差 |
当前金币 |
完全背包结果 |
更新后金币 |
|---|---|---|---|---|---|
| 1 → 2 | 50 | 100 | 0( |
100 | |
| 2 → 3 | 20 | 100 | 125 | ||
| 3 → 4 | 25 | 125 | 0( |
125 | |
| 4 → 5 | 20 | 125 | 155 | ||
| 5 → 6 | 25 | 155 | 305 |
最终金币为
代码
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T, N, money;
cin >> T >> N >> money;
vector<vector<int>> price(T, vector<int>(N));
for (int i = 0; i < T; ++i) {
for (int j = 0; j < N; ++j) {
cin >> price[i][j];
}
}
// 逐天处理相邻两天之间的收益,把金币滚动更新。
for (int day = 0; day + 1 < T; ++day) {
vector<int> dp(money + 1, 0);
for (int item = 0; item < N; ++item) {
int cost = price[day][item];
int gain = price[day + 1][item] - price[day][item];
if (gain <= 0 || cost > money) {
continue;
}
// 完全背包:同一种纪念品可以买很多件。
for (int cap = cost; cap <= money; ++cap) {
dp[cap] = max(dp[cap], dp[cap - cost] + gain);
}
}
money += dp[money];
}
cout << money << '\n';
return 0;
}复杂度
- 时间复杂度:
- 空间复杂度:
这里
总结
这题表面上是“买纪念品”,本质上是“每天都做一次完全背包”。
真正要抓住的是两点:
- 物品重量是今天的买入价
- 物品价值是明天和今天的价格差
把每天的最优增益算出来,再把金币滚动更新,就能得到最终答案。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
