单窗口内按 b 降序排队可证最优,全体排序后退化为 0/1 分配问题,用背包式 DP 求最小完成时间。
OJ: roj
题目 ID: 20019
难度:普及+/提高-
标签:贪心背包动态规划排序
日期: 2026-08-28 22:10
形式化题目
有
思路
一句话本质:队内顺序与窗口分配可以分离——单窗口内按
先看一个不做任何优化的朴素解,它直接枚举所有方案:
/**
* Author by Rainboy blog: https://rainboylv.com 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-28 21:52
* update_at: 2026-08-28 21:52
*/
#include <bits/stdc++.h>
using namespace std;
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
// 完全不用任何贪心结论,直接枚举所有方案:
// 1) 01 序列 choose[] 枚举每个人分到 1 号窗口还是 2 号窗口;
// 2) 叶子节点里,对每个窗口内部的所有排队顺序(全排列)都试一遍。
// 取所有方案里"最后吃完时刻"的最小值,这就是最优答案。
// 复杂度约 O(2^N * (N+1)! * N),只适合 N <= 8 的小数据。
const int MAXN = 10;
const int INF = 0x3f3f3f3f;
int n;
int a[MAXN], b[MAXN]; // 输入:打饭耗时、吃饭耗时
int choose[MAXN]; // choose[i] = 0/1 表示第 i 人分到 1 号 / 2 号窗口
int ans;
// 计算一个窗口内的人按 ids 给出的顺序排队时,最晚吃完的时刻。
int calc_one_window(int ids[], int cnt) {
int cur = 0; // 窗口累计打饭时间
int mx = 0; // 该窗口最晚吃完时刻
for (int k = 0; k < cnt; k++) {
int id = ids[k];
cur += a[id]; // 第 id 人打完饭的时刻
mx = max(mx, cur + b[id]);
}
return mx;
}
// 叶子节点:当前 choose[] 已把所有人分好窗口,再枚举每个窗口内部的全排列。
void check() {
int w1[MAXN], w2[MAXN];
int c1 = 0, c2 = 0;
for (int i = 1; i <= n; i++) {
if (choose[i] == 0) w1[c1++] = i;
else w2[c2++] = i;
}
sort(w1, w1 + c1); // 从任意固定顺序开始做全排列
sort(w2, w2 + c2);
do {
do {
int cur = max(calc_one_window(w1, c1), calc_one_window(w2, c2));
if (cur < ans) ans = cur;
} while (next_permutation(w2, w2 + c2));
} while (next_permutation(w1, w1 + c1));
}
// 每一层决定第 dep 个人分到哪个窗口,dep == n+1 时得到一条完整 01 序列。
void dfs(int dep) {
if (dep == n + 1) {
check();
return;
}
for (int v = 0; v <= 1; v++) { // 这一层选择第 dep 人的窗口
choose[dep] = v;
dfs(dep + 1);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i] >> b[i];
}
ans = INF;
dfs(1);
cout << ans << '\n';
return 0;
}这个暴力把问题看成两段选择序列:choose[i] = 0/1 决定第 i 人进 1 号还是 2 号窗口;叶子节点再对每个窗口内部的排队顺序做全排列。它枚举了"所有分法 × 所有队内顺序",答案一定正确,但复杂度约
问题? 分队、两条队各自的顺序三个自由度缠在一起,先处理哪一个?
先固定"谁在哪个窗口",只看单条窗口内部。窗口串行打饭,一个人的吃完时刻 = 他前面所有人的打饭时间之和(前缀和)+
问题? 单窗口里两个相邻的人顺序"反了",会不会更差?
用交换论证。设相邻两人
| 排队顺序 | ||
|---|---|---|
交换后两个时刻都不超过交换前的最大值(
问题? 两个窗口都要
对全体按
问题? 窗口分配还有
注意到关键结构:按全局顺序处理到第
问题? 状态和转移具体怎么定?
设
- 进 1 号窗口(需
):他打完饭的时刻就是 (含他),吃完时刻 ,从 继承: ; - 进 2 号窗口:2 号窗口累计打饭
,吃完时刻 ,从 继承: 。
两者取 min。初始
样例 DP 表格
这张表用题面样例 5 人
| 0 | 6 | 7 | 8 | 13 | 14 | 15 | |
|---|---|---|---|---|---|---|---|
| 0 | 0 | ||||||
| 1 | 14 | 14 | |||||
| 2 | 20 | 14 | 14 | 20 | |||
| 3 | 25 | 20 | 18 | 17 | 17 | 18 | 20 |
| 4 | 25 | 20 | 18 | 17 | 17 | 17 | 18 |
| 5 | 26 | 20 | 19 | 18 | 17 | 17 | 17 |
看两个具体转移:第 3 行
代码
/**
* 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-28 21:52
* update_at: 2026-08-28 21:52
*/
#include <bits/stdc++.h>
using namespace std;
// D. 食堂(Meal)
// 思路:
// 1) 每个窗口内部按 b 降序排队最优(相邻交换论证),所以全局按 b 降序排序后,
// 任意窗口的队列(子序列)自动保持 b 降序,队内顺序这一自由度被消掉。
// 2) 剩下唯一决策:每人进 1 号窗口还是 2 号窗口。
// 按 b 降序依次处理每个人 i,此时第 i 人进某窗口一定排在该窗口队尾,
// 其吃完时刻 = 该窗口累计打饭时间 + b[i]。
// 3) 背包式 DP:f[j] 表示处理完当前前缀后、1 号窗口累计打饭时间为 j 时,
// 已经处理的这些人里最晚吃完时刻的最小值。
// 转移(第 i 人,s_i 为 a 的前缀和):
// - 进 1 号窗口:f[j] = min(f[j], max(f[j-a[i]], j + b[i]))
// - 进 2 号窗口:f[j] = max(f[j], (s_i - j) + b[i])
// 答案 = min f[j]。
const int MAXN = 505;
const int INF = 0x3f3f3f3f;
struct Person {
int a, b; // 打饭耗时、吃饭耗时
} p[MAXN];
int n;
int f[250005]; // 滚动数组:1 号窗口累计打饭时间为 j 时的最小"最晚吃完时刻"
bool cmp(const Person &x, const Person &y) {
return x.b > y.b; // 按 b 降序
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> p[i].a >> p[i].b;
}
sort(p + 1, p + n + 1, cmp);
memset(f, 0x3f, sizeof(f));
f[0] = 0;
int s = 0; // a 的前缀和
for (int i = 1; i <= n; i++) {
s += p[i].a;
// j 从大到小:保证下面的 f[j - p[i].a] 还是上一层的值(0/1 背包式)
for (int j = s; j >= 0; j--) {
// 第 i 人进 2 号窗口:2 号窗口累计打饭 = s - j
f[j] = max(f[j], s - j + p[i].b);
// 第 i 人进 1 号窗口:打完饭时刻 = j,吃完时刻 = j + b[i]
if (j >= p[i].a) {
f[j] = min(f[j], max(f[j - p[i].a], j + p[i].b));
}
}
}
int ans = INF;
for (int j = 0; j <= s; j++) {
ans = min(ans, f[j]);
}
cout << ans << '\n';
return 0;
}复杂度
- 排序:
。 - DP:第
轮内层循环 次(含 ),总迭代 ,其中 。即 ,最坏全部 时约 次迭代(上界 ),实测 < 0.2s。 - 空间:滚动数组
。
注:官方 sol.md 写的
总结
本题的思考路线是"先消自由度,再压状态":
- 单窗口内用交换论证证明
降序最优(这是带后续时长的单机调度的经典 Jackson 规则); - 全局排序后队内顺序自动确定,
种分法用背包式 DP 压缩; - DP 的状态设计利用了"第
人总是队尾,吃完时刻只依赖所在窗口累计打饭时间"以及"两窗口累计打饭之和 = 前缀和"这两个结构,一维状态描述两边。
这个"贪心消序 + 背包分人"的组合在双机调度类问题里很常见:先证明局部顺序与分配无关,再把分配当成 0/1 背包做。暴力版全排列枚举也提醒我们:当两个自由度纠缠时,先证明一个自由度可以贪心固定,另一个就能用 DP 处理。
图示解析
这张 ASCII 图展示整条解题路线:
目标:最小化最后一人吃完时刻(两台窗口,队内可自由排序)
|
|-- 固定窗口分配,只看单窗口
| max(前缀和 a + b) 最小化
| 交换论证:b 小者在前不优 → 窗口内 b 降序最优
|
|-- 全体按 b 降序排序 → 子序列保持降序 → 队内顺序自由度消失
|
|-- 唯一决策:每人进 1 号 / 2 号窗口
| 第 i 人总是队尾,吃完时刻只依赖该窗口累计打饭时间
|
`-- 0/1 背包式 DP:f[j] = 1 号窗口累计打饭 j 时的最小最晚吃完时刻
答案 = min f[j]从下往上看:先通过交换论证固定队内顺序(第一步的证明保证贪心安全),再让"全局排序"把两个窗口的顺序问题合二为一,最后用一维状态