2025 United Kingdom and Ireland Programming Contest (UKIEPC 2025) 2025 英国 ICPC——D. Depot(仓库)(当以比较整体的视角去看待一个问题,发现无法解决的时候,应该要切换回比较个体的视角,就是要这个粒度要更加细一点,粒度更加细一点,可能有的时候就会有思路了)(可以去掉某些限制,看看是否可以想出来)(完全背包问题) · znzryb
## 题目大意
注意这道题目,其表述有点问题,应该要这么理解比较好。就是**说在任何情况下,这个仓库容量都不能爆**。
**核心目标**
在 $n$ 个顺序进行的精炼阶段中,利用初始物料 1 的数量 $s$ 和共享仓库容量 $k$,通过选择不同的转化机器(物料 $i \to i+1$),计算最终能获得的物料 $n$ 的最大数量。
**关键约束**
- **处理顺序**:必须严格按照 $1 \to 2 \to \dots \to n$ 的顺序处理。一旦开始处理下一阶段,就不能再返回处理上一阶段的物料。
- **机器转化**:每个阶段有若干机器可选,每台机器消耗 $M_I$ 单位物料 $i$ 并产出 $M_O$ 单位物料 $i+1$。同一阶段内可以多次、多种类地使用机器。
- **仓库容量限制**:仓库为所有物料共享。在进行第 $i \to i+1$ 阶段转化时,**仓库内剩余的物料** $i$ **数量与已产出的物料** $i+1$ **数量之和在任何时刻都不能超过** $k$。
- **物料处置**:为了腾出空间满足容量限制,可以随时丢弃仓库内的物料。
**样例解释**
- **样例 3 (**$s=4, k=10$**,机器 1: 消耗 2 产 3)**
1. 第一次运行机器:消耗 2 个物料 1,产出 3 个物料 2。此时仓库内有 2 个物料 1 和 3 个物料 2,总数 $2+3=5 \le 10$。
1. 第二次运行机器:再消耗 2 个物料 1,产出 3 个物料 2。此时仓库内有 0 个物料 1 和 6 个物料 2,总数 $0+6=6 \le 10$。
最终产出:6。
- **样例 4 (**$s=4, k=7$**,机器 1: 消耗 2 产 4)**
1. 第一次运行机器:消耗 2 个物料 1,产出 4 个物料 2。仓库总量为 $2+4=6 \le 7$。
1. 第二次尝试:如果再次运行机器,产出 4 个物料 2,则总量将变为 $0+8=8$。由于仓库上限 $k=7$,实际产出的物料 2 最终只能封顶为 7。
最终产出:7。
- **样例 5 (**$s=7, k=7$**,机器 1: 消耗 3 产 5)**
1. 直接转化尝试:若消耗 3 个物料 1,剩 4 个,产出 5 个,总量 $4+5=9 > 7$,操作非法。
1. 调整策略:必须先丢弃 2 个物料 1,使初始物料 1 降为 5。
1. 运行机器:消耗 3 个物料 1,剩 2 个,产出 5 个,总量 $2+5=7 \le 7$。此时剩余物料 1 不足以再次运行机器。
最终产出:5。
---
**样例解释**
| **样例** | **关键参数** | **过程说明** | **结果** |
| --- | --- | --- | --- |
| **样例 3** | $s=4, k=10$ 机器 (1, 2, 3) | 1. 消耗 2 个物料 1 $\to$ 产出 3 个物料 2。仓内存放:2 (剩) + 3 (新) = 5 $\le 10$。
2. 再消耗 2 个物料 1 $\to$ 产出 3 个物料 2。仓内存放:0 (剩) + 6 (新) = 6 $\le 10$。 | **6** |
| **样例 4** | $s=4, k=7$ 机器 (1, 2, 4) | 1. 消耗 2 个物料 1 $\to$ 产出 4 个物料 2。仓内存放:2 + 4 = 6 $\le 7$。
2. 计划再产出 4 个,但总量将达 $0+8=8 > 7$,受限于 $k=7$,实际产出封顶为 7。 | **7** |
| **样例 5** | $s=7, k=7$ 机器 (1, 3, 5) | 1. 若直接转化:消耗 3 个剩 4 个,产出 5 个,总量 $4+5=9 > 7$(非法)。
2. 必须先丢弃 2 个物料 1,剩 5 个。消耗 3 个剩 2 个,产出 5 个,总量 $2+5=7 \le 7$。
3. 剩余 2 个物料 1 不足消耗,停止。 | **5** |
## 思路讲解
::: 错误的预处理想法(预处理需要背包)(当以比较整体的视角去看待一个问题,发现无法解决的时候,应该要切换回比较个体的视角,就是要这个粒度要更加细一点,粒度更加细一点,可能有的时候就会有思路了。)
2025 United Kingdom and Ireland Programming Contest (UKIEPC 2025) 2025 英国 ICPC——L. Last Orders
就像这道题目一样,我们这道题目我们需要先预处理出来一个全源最短路吧。
上面一道题目之所以要预出一出来全源最短路就是为了防止如果说我们不是走到每个点都喝酒的话,会导致这个问题复杂化。那么如果有最短路了。你走到这个点,为什么不喝酒呢?肯定要喝酒,否则你你最短路走到这个点为了什么呢?
那么这道题目也一样,我们也需要**预处理**一下。预处理出来的一个产物吧,可以这么说,应该具有以下属性,以下属性就可以呃理解为是背包当中的**一个物品**。

呃,然后我们问题就来到怎么样去预处理这个机器啊,其实你会发现预处理这个机器不是说那么简单的。嗯,毕竟可能会有中间产物剩余的这个情况,中间产物剩余这个情况还是挺棘手的。
:::
我们可以先想,如果我们把这个ware house的这个容量的这个限制去掉的话,是不是会好做很多?去掉的话,其实就是跑 n 个背包就好了(**注意,这个其实是完全背包**),只要我们能够最大化每个产品的数量,那么就一定可以贪心的得到最多的 N 的数量。
::: 题目读错了的时候产生的这个思路,误以为就是说我们还可以返回原来那个呃状态,就误以为我们还能够返回原来那个生产顺序吧。实际上题目里面说的是不允许返回前面一个,或者说\*\*不允许返回前面的这个生产顺序的\*\*。
应该来说,我们在有这个容量限制的情况下,我们依然采取贪心的策略,让每一次得到零件都最多应该是没什么问题,应该是对的。
那么这个完全背包如果说了 k 个这个零件,它就只能转换为这么多的话,那么应该来说是没有更加好的这个方案了。现在呢就是说如果说 k 个零件它能够转换为更多的这个零件超过了这个容量限制啊,这个时候呃,怎么操作呢?呃这就是可能的问题。
:::
为什么是外层W内层机器呢?这个的原因其实是这样子,完全背包我们不是说那么在意这个顺序,我们可以呃外层是物品,内层是W。但是是这样子,完全背包之所以不在意乎这个顺序,是因为你**放入背包当中的物品**,**无论以什么顺序放入都无所谓**。但是这道题目由于其能够所产生的利益是与原来的这个 dp 是相关的,如果说你先用一个机器,这个机器的效率比较高,产生的DP比较多,就产生的这个值比较多的话,然后你后运行一个机器。它也是一个效率比较高的机器的话,那么可能就你就超出了题目的仓库容量的限制。呃,说白了就是这道题目**先运行哪个机器,后运行哪个机器是非常重要的**。我们**不能够忽略这个顺序**。(不能够忽略的原因已在上面给出。)
```cpp
ll cur_material=St_material;
for (int i=2;i<=N;++i) {
for (int w=1;w<=cur_material;++w) {
// 顺序遍历,利用完全背包的这个性质。
// 呃,理论上来说
// W它有的时候不是每次都是每个输入物料都是有用的嘛。所以说它相当于也是有这个丢弃的这个作用。
// 外层w内层机器,保证B→A交替序列能被捕获
for (auto [input,output]:material_machine_mtx[i]) {
if (w-input<0) {
continue;
}
if (cur_material-w+dp_knapsack[i][w-input]+output <= K_depot) {
dp_knapsack[i][w]=max(dp_knapsack[i][w],dp_knapsack[i][w-input]+output);
}else {
// 黄色荧光的是常量,
ll output_rem=K_depot-output-cur_material+w;
if (output_rem<0) {
continue;
}
dp_knapsack[i][w]=max(dp_knapsack[i][w],output_rem+output);
}
}
}
cur_material=*max_element(all(dp_knapsack[i]));
}
```
## AC代码
AC
[https://qoj.ac/submission/2019893](https://qoj.ac/submission/2019893)
AC
[https://codeforces.com/gym/106157/submission/362107850](https://codeforces.com/gym/106157/submission/362107850)
::: 源代码
```cpp
/**
* Problem: D. Depot
* Contest: 2025 United Kingdom and Ireland Programming Contest (UKIEPC 2025)
* Judge: Codeforces
* URL: https://codeforces.com/gym/106157/problem/D
* Created: 2026-02-08 22:24:30
* Author: Gospel_rock
*
* Powered by AutoCp https://github.com/Pushpavel/AutoCp
*/
#include <bits/stdc++.h>
#define all(vec) vec.begin(),vec.end()
#define lson(o) (o<<1)
#define rson(o) (o<<1|1)
#define SZ(a) ((long long) a.size())
#define debug(var) cerr << #var <<" = ["<<var<<"]"<<"\n";
#define debug1d(a) \
cerr << #a << " = ["; \
for (int i = 0; i < (int)(a).size(); i++) \
cerr << (i ? ", " : "") << a[i]; \
cerr << "]\n";
#define debug2d(a) \
cerr << #a << " = [\n"; \
for (int i = 0; i < (int)(a).size(); i++) \
{ \
cerr << " ["; \
for (int j = 0; j < (int)(a[i]).size(); j++) \
cerr << (j ? ", " : "") << a[i][j]; \
cerr << "]\n"; \
} \
cerr << "]\n";
#define debug3d(a) \
cerr << #a << " = [\n"; \
for (int i = 0; i < (int)(a).size(); i++) \
{ \
cerr << " [\n"; \
for (int j = 0; j < (int)(a[i]).size(); j++) \
{ \
cerr << " ["; \
for (int k = 0; k < (int)(a[i][j]).size(); k++) \
cerr << (k ? ", " : "") << a[i][j][k]; \
cerr << "]\n"; \
} \
cerr << " ]\n"; \
} \
cerr << "]\n";
#define cend cerr<<"\n-----------\n"
#define fsp(x) fixed<<setprecision(x)
using namespace std;
using ll = long long;
using ull = unsigned long long;
using DB = double;
// using i128 = __int128;
using CD = complex<double>;
static constexpr ll MAXN = (ll)1e6+10, INF = (1ll<<61)-1;
static constexpr ll mod = 998244353; // (ll)1e9+7;
static constexpr double eps = 1e-8;
const double pi = acos(-1.0);
ll lT,testcase;
/*
*
*/
struct Machine {
ll input,output;
};
void Solve() {
ll N,M;
cin >> N >> M;
vector<vector<Machine>> material_machine_mtx(N+2);
ll St_material,K_depot;
cin>>St_material>>K_depot;
for (int i=1;i<=M;++i) {
ll idx,input,output;
cin>>idx>>input>>output;
material_machine_mtx[idx+1].push_back({input,output});
}
vector<vector<ll>> dp_knapsack(N+2,vector<ll>(K_depot+2));
ll cur_material=St_material;
for (int i=2;i<=N;++i) {
for (int w=1;w<=cur_material;++w) {
// 顺序遍历,利用完全背包的这个性质。
// 呃,理论上来说
// W它有的时候不是每次都是每个输入物料都是有用的嘛。所以说它相当于也是有这个丢弃的这个作用。
// 外层w内层机器,保证B→A交替序列能被捕获
for (auto [input,output]:material_machine_mtx[i]) {
if (w-input<0) {
continue;
}
if (cur_material-w+dp_knapsack[i][w-input]+output <= K_depot) {
dp_knapsack[i][w]=max(dp_knapsack[i][w],dp_knapsack[i][w-input]+output);
}else {
ll output_rem=K_depot-output-cur_material+w;
if (output_rem<0) {
continue;
}
dp_knapsack[i][w]=max(dp_knapsack[i][w],output_rem+output);
}
}
}
cur_material=*max_element(all(dp_knapsack[i]));
}
// debug2d(dp_knapsack);
cout<<cur_material<<"\n";
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
#ifdef LOCAL
cout.setf(ios::unitbuf); // 无缓冲流,方便我们调试
#endif
// cin >> lT;
// for (testcase=1;testcase<=lT;++testcase)
Solve();
return 0;
}
/*
AC
https://qoj.ac/submission/2019893
AC
https://codeforces.com/gym/106157/submission/362107850
*/
```
:::
## 心路历程(WA,TLE,MLE……)
::: AI 的这个回答(回答我关于为什么需要外层w内层机器,而不是类似于像完全背包一样)
[1 tool called]
你代码本身确实很好,核心逻辑(容量公式、丢弃机制)都是对的。循环顺序是最后一个坑。
你问的问题特别好:这题和标准完全背包的**本质区别**在哪?
---
### 标准完全背包:物品的"价值"是固定的
```cpp
dp[w] = max(dp[w], dp[w-cost] + value)
```
物品 A 价值 8,不管你之前用了什么物品,A 永远贡献 8。所以:
- 序列 B→A→B 的总价值 = 1+8+1 = 10
- 序列 A→B→B 的总价值 = 8+1+1 = 10
**加法满足交换律**,顺序不影响结果。所以先枚举 A 再枚举 B 和反过来一样。
---
### 这道题:机器的"实际产出"是状态相关的
因为有容量限制,机器的实际贡献**取决于你用它的时候仓库已经有多少东西**。具体来说就是你代码里的 else 分支:
```cpp
ll output_rem=K_depot-output-cur_material+w;
```
用机器 A(产 8)的时候,如果仓库快满了,你实际只能留 `output_rem` 个旧产品再加上 8,不是无脑 `dp[w-3]+8`。
回到那个测试样例 `(3,8)` 和 `(5,1)`,`K=10`:
- **先用 B 再用 A(w=6→w=9)**:dp[6]=1,仓库还宽松,A 全额生效:`1+8=9`
- **先用 A 再用 B(w=3→w=8)**:A 已经塞了 8,仓库满了,B 只能在旁边加 1,dp[9] 还是 8
同样的两台机器,**使用顺序不同,实际产出不同**。因为容量限制让"加法"变成了"加法 + 截断",**截断破坏了交换律**。
---
### 一句话总结
**标准完全背包**:`dp[w] = dp[w-cost] + value`,加法可交换,顺序无所谓。
**这道题**:`dp[w] = min(dp[w-MI], 容量限制) + MO`,有个 `min` 截断,截断量取决于当前状态,导致**不同机器的使用顺序会互相影响可行性**。所以必须在每个 `w` 处同时看到所有机器,让 DP 自己选最优的交替顺序。
:::