[TJOI2013] 拯救小矮人

GitHub跳转原题关系图返回列表

把第 i 个小矮人逃走前的条件整理成“已逃走肩高前缀不超过 T+a_i+b_i-H”,再按 a+b 排序并用大根堆维护最多可行人数。

OJ: luogu

题目 ID: P4823

难度:提高+/省选-

标签:贪心排序优先队列调度

日期: 2026-06-21 08:50

题意

每个小矮人有:

  • 肩高 a_i
  • 手臂长度 b_i

如果当前还留在陷阱里的所有小矮人的肩高总和,再加上某个小矮人的手臂长度,已经够到陷阱口,那么这个小矮人就能逃走。

逃走之后,他就不会再给后面的人继续提供肩高。

要求最多能让多少个小矮人逃走。

思路

先看小数据暴力:

cpp
#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 排序后,尽量多选任务,使任意前缀耗时都不超过当前截止限制。

这正是经典的“最多任务数”贪心:

  1. 按限制顺序扫描
  2. 先把当前小矮人加入集合
  3. 如果当前已选肩高和超出限制,就删掉已选里 a 最大的那个

最后集合大小就是答案。

代码

cpp
#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;
}

复杂度

时间复杂度 O(nlogn)O(n log n),空间复杂度 O(n)O(n)

总结

这题最关键的一步是把条件从:

剩余肩高 + b_i >= H

改写成:

已逃走肩高前缀 <= T + a_i + b_i - H

一旦变成“前缀和不能超过限制”,它就和经典调度贪心完全一致了。

一图流解析

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

一图流解析