把第 i 个小矮人逃走前的条件整理成“已逃走肩高前缀不超过 T+a_i+b_i-H”,再按 a+b 排序并用大根堆维护最多可行人数。
OJ: luogu
题目 ID: P4823
难度:提高+/省选-
标签:贪心排序优先队列调度
日期: 2026-06-21 08:50
题意
每个小矮人有:
- 肩高
a_i - 手臂长度
b_i
如果当前还留在陷阱里的所有小矮人的肩高总和,再加上某个小矮人的手臂长度,已经够到陷阱口,那么这个小矮人就能逃走。
逃走之后,他就不会再给后面的人继续提供肩高。
要求最多能让多少个小矮人逃走。
思路
先看小数据暴力:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 15;
struct Dwarf {
int a, b;
} d[MAXN], ord[MAXN];
int n, H;
int best_answer;
int total_a;
bool cmp_dwarf(const Dwarf &lhs, const Dwarf &rhs) {
return lhs.a + lhs.b < rhs.a + rhs.b;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
// 做法是枚举所有子集,把选中的人按 a+b 排序,
// 再逐个检查“已逃走肩高前缀 <= T+a_i+b_i-H”是否始终成立。
cin >> n;
for (int i = 0; i < n; i++) {
cin >> d[i].a >> d[i].b;
total_a += d[i].a;
}
cin >> H;
best_answer = 0;
for (int mask = 0; mask < (1 << n); mask++) {
int cnt = 0;
for (int i = 0; i < n; i++) {
if (mask & (1 << i)) {
ord[cnt++] = d[i];
}
}
sort(ord, ord + cnt, cmp_dwarf);
bool ok = true;
int prefix_escape = 0;
for (int i = 0; i < cnt; i++) {
prefix_escape += ord[i].a;
if (prefix_escape > total_a + ord[i].a + ord[i].b - H) {
ok = false;
break;
}
}
if (ok) {
best_answer = max(best_answer, cnt);
}
}
cout << best_answer << '\n';
return 0;
}关键是把条件改写成前缀限制。
如果两个小矮人的 a_i + b_i 分别是 x < y,那么把 x 更小的那个更早安排逃走不会更差。
通过交换论证,可以先按 a_i + b_i 升序排序。
设所有小矮人的肩高总和为 T。
如果某个小矮人是在当前顺序中第 k 个逃走的,设到他为止已经逃走的小矮人肩高总和为 P。
那么在他逃走时,仍留在下面搭梯子的小矮人肩高和是:
T - (P - a_i)
因此他能逃走当且仅当:
T - (P - a_i) + b_i >= H
整理得:
P <= T + a_i + b_i - H
于是问题就变成:
每个小矮人是一项任务,耗时是
a_i,截止时间是T + a_i + b_i - H。
按a_i+b_i排序后,尽量多选任务,使任意前缀耗时都不超过当前截止限制。
这正是经典的“最多任务数”贪心:
- 按限制顺序扫描
- 先把当前小矮人加入集合
- 如果当前已选肩高和超出限制,就删掉已选里
a最大的那个
最后集合大小就是答案。
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2005;
struct Dwarf {
int a, b; // a 表示肩高,b 表示手臂长度
} d[MAXN];
int n, H;
long long total_a; // 所有小矮人肩高总和 T
bool cmp_dwarf(const Dwarf &lhs, const Dwarf &rhs) {
return lhs.a + lhs.b < rhs.a + rhs.b;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> d[i].a >> d[i].b;
total_a += d[i].a;
}
cin >> H;
// 按 a+b 从小到大排序。
// 交换论证可以证明:如果两个人都要逃走,让 a+b 更小的人更早逃走不会更差。
sort(d + 1, d + n + 1, cmp_dwarf);
priority_queue<int> heap; // 已选小矮人的肩高,超限制时删掉最大的
long long used = 0; // 当前已选小矮人的肩高和
for (int i = 1; i <= n; i++) {
// 如果第 i 个小矮人作为当前顺序里最后一个逃走的人,
// 那么已逃走肩高前缀 P 需要满足:
// P <= T + a_i + b_i - H
long long limit = total_a + d[i].a + d[i].b - H;
if (limit < 0) {
continue;
}
// 先尝试把当前小矮人加入答案集合。
used += d[i].a;
heap.push(d[i].a);
// 如果当前前缀肩高和超过限制,
// 贪心地删掉已选中肩高最大的那个人,最有利于保留更多人数。
while (!heap.empty() && used > limit) {
used -= heap.top();
heap.pop();
}
}
cout << (int) heap.size() << '\n';
return 0;
}复杂度
时间复杂度
总结
这题最关键的一步是把条件从:
剩余肩高 + b_i >= H
改写成:
已逃走肩高前缀 <= T + a_i + b_i - H
一旦变成“前缀和不能超过限制”,它就和经典调度贪心完全一致了。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
