家庭作业

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

反悔贪心:作业按截止时间排序逐个接下,超出容量就丢弃学分最小的已选作业,小根堆维护 O(n log n)。

OJ: roj

题目 ID: 1430

难度:普及+/提高-

标签:贪心堆排序

创建: 2026-09-30 09:54

更新: 2026-10-07 11:01

形式化题目

给定 nn 个作业,第 ii 个作业有截止时间 did_i(正整数,天)和学分 wiw_i。每个作业恰好吃掉一整天,且必须在第 did_i 天结束前(即第 1∼di1 \sim d_i 天中的某一天)完成才能获得学分 wiw_i。同一时间只能做一个作业。求能获得的最大总学分。

样例:n=7n = 7,(d,w)(d, w) 依次为 (1,6),(1,7),(3,2),(3,1),(2,4),(2,5),(6,1)(1,6),(1,7),(3,2),(3,1),(2,4),(2,5),(6,1),答案是 1515(例如依次完成作业 2,6,3,1,7,5,42, 6, 3, 1, 7, 5, 4)。

正解

思路

朴素想法。 一个直观的贪心是:按学分从大到小做,每个作业安排在不晚于其截止时间的、尚未被占用的最晚一天(最晚空闲天)。这个贪心本身是正确的,但如果用"每个作业从头扫一天"的方式找空闲天,单次是 O(di)O(d_i),最坏总复杂度 O(n2)O(n^2),在 n=106n = 10^6 下无法接受;用并查集或链表可以优化到近线性,但还有另一种更容易写对的等价做法——反悔贪心。

反悔贪心的推导。 换一个视角:把作业按截止时间 dd 从小到大排序,依次"尝试接下"每个作业,维护当前已接下的作业集合 SS,它必须始终满足合法性:对任意前缀,已接作业总数不超过对应天数容量。

按 dd 升序处理时,处理完截止时间为 dd 的所有作业后,∣S∣⩽d|S| \leqslant d 就是完整的合法条件(更早的截止时间天然被更弱的条件覆盖)。于是每次把当前作业加入 SS 后检查:

  • 若 ∣S∣⩽d|S| \leqslant d,仍然合法,直接保留;
  • 若 ∣S∣>d|S| > d,必须丢弃一个作业。丢弃谁最优?丢弃当前这个或之前任何一个,效果都是让 ∣S∣|S| 减一;为了让总学分最大,应丢弃 SS 中学分最小的那个——这就是"反悔":之前接下的低学分作业让位给更有价值的选择。

正确性要点。 交换论证:设最优解 OPTOPT 与贪心解 GG。按截止时间排序逐位对齐,若在截止时间为 dd 的作业上两者选择不同,可证总能把 OPTOPT 中该位置的作业换成 GG 所选(或所丢)的作业而不劣——若 GG 选了 OPTOPT 没选的作业 xx,则 OPTOPT 里必有一个 GG 没选的 yy,且 wy⩽wxw_y \leqslant w_x(否则贪心在第 yy 批次容量冲突时会丢 xx 而不是留 xx),互换后 OPTOPT 不减;若 GG 丢了 OPTOPT 选的作业,同理被保留的替代者学分不小于它。归纳即得 w(G)⩾w(OPT)w(G) \geqslant w(OPT)。

实现。 用小根堆维护已接作业的学分:

  1. 所有作业按 (d,w)(d, w) 排序(dd 升序);
  2. 依次把 ww 入堆;若堆大小超过当前 dd,弹出堆顶(最小学分);
  3. 答案为最终堆中元素之和。

用样例演示该过程(堆按学分小根):

步骤 作业 (d,w)(d, w) 操作后堆内容(学分) 说明
1 (1,6)(1, 6) {6}\{6\} 大小 1⩽11 \leqslant 1,保留
2 (1,7)(1, 7) {7}\{7\} 大小 2>12 > 1,弹出最小的 66
3 (2,4)(2, 4) {7,4}\{7, 4\} 大小 2⩽22 \leqslant 2,保留
4 (2,5)(2, 5) {7,5}\{7, 5\} 大小 3>23 > 2,弹出最小的 44
5 (3,2)(3, 2) {7,5,2}\{7, 5, 2\} 大小 3⩽33 \leqslant 3,保留
6 (3,1)(3, 1) {7,5,2}\{7, 5, 2\} 大小 4>34 > 3,新来的 11 即最小,弹出
7 (6,1)(6, 1) {7,5,2,1}\{7, 5, 2, 1\} 大小 4⩽64 \leqslant 6,保留

表后观察:最终堆为 {7,5,2,1}\{7, 5, 2, 1\},和为 1515,与样例一致。注意第 2、4 步的"弹出"就是反悔——第 2 步丢掉先来的 66 保住更大的 77,第 4 步丢掉 44 保住 55;而被丢的作业对应的天数名额总是可以被后来者补上的,因为堆大小约束始终对着当前最大的截止时间检查。

代码

Python 版:

python
# ROJ 1430 家庭作业:反悔贪心(大根学分 + 小根堆反悔)
# 按截止时间排序,逐个尝试接下作业;若当前前 i 个作业数超过第 i 个的截止时间,
# 弹掉学分最小的作业(反悔),保证任意时刻保留的方案合法且学分最大。
import sys
import heapq


def main() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    n = next(data)
    # (截止时间, 学分),按截止时间升序
    jobs = sorted((next(data), next(data)) for _ in range(n))
    heap: list[int] = []  # 已接作业的学分(小根堆,堆顶是最可能反悔的)
    for d, w in jobs:
        heapq.heappush(heap, w)
        if len(heap) > d:  # 前 d 天最多做 d 个作业,超了就丢弃学分最小的
            heapq.heappop(heap)
    print(sum(heap))


if __name__ == "__main__":
    main()

C++ 版(同一算法):

cpp
/**
 * 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-10-04 14:20
 * update_at: 2026-10-05 00:40
 */
#include <cstdio>
#include <algorithm>
#include <queue>
#include <vector>
using namespace std;

typedef long long ll;

// 反悔贪心:作业按截止时间排序逐个接下,容量超限时丢弃学分最小的已选作业
const int MAXN = 1e6 + 5;

struct Job {
    ll d; // 截止时间(第 d 天结束前完成)
    ll w; // 学分
};

Job jobs[MAXN]; // jobs[i]:第 i 个作业,下标从 1 开始
ll n;

// 小根堆维护已接作业的学分,堆顶 = 当前最"后悔"(学分最小)的作业
priority_queue<ll, vector<ll>, greater<ll> > heap;

bool cmp_job(const Job &a, const Job &b) {
    return a.d < b.d;
}

int main() {
    scanf("%lld", &n);
    for (ll i = 1; i <= n; i++) {
        scanf("%lld %lld", &jobs[i].d, &jobs[i].w);
    }

    // 按截止时间 d 升序排序,保证容量检查只针对当前最大的 d
    sort(jobs + 1, jobs + n + 1, cmp_job);

    ll ans = 0; // 最终堆中学分之和,即答案
    for (ll i = 1; i <= n; i++) {
        heap.push(jobs[i].w);
        ans += jobs[i].w;
        if ((ll)heap.size() > jobs[i].d) { // 已接作业数超过前 d 天容量
            ans -= heap.top();             // 反悔:丢弃学分最小的作业
            heap.pop();
        }
    }

    printf("%lld\n", ans);
    return 0;
}

复杂度

排序 O(nlog⁡n)O(n \log n),每个作业至多一次入堆一次出堆,总计 O(nlog⁡n)O(n \log n) 时间、O(n)O(n) 空间。n⩽106n \leqslant 10^6 时轻松通过(Python 下读入用 sys.stdin.buffer.read() 一次性完成)。

总结

本题是"反悔贪心"的经典模型:贪心策略允许撤回之前的选择,用堆快速定位"最后悔"的那个(学分最小者)。识别信号是——按某个维度(截止时间)排序逐个决策、有容量约束(每天一个作业)、目标最大化权值和且权值可比较。同类型题目还有"老鼠和猫的交易"" jq 集合"系列;与之等价的另一写法是"学分从大到小 + 并查集找最晚空闲天",两者复杂度同阶,反悔堆的代码量更短、也更不易写错。

图示解析

反悔贪心与"按学分贪心"的等价性

flowchart TD
    A[按截止时间 d 升序处理] --> B{堆大小 > 当前的 d?}
    B -- 否 --> C[保留全部已选作业<br/>继续下一批]
    B -- 是 --> D[弹出堆顶:<br/>学分最小的作业 = 反悔]
    D --> C
    C --> E[处理完所有作业<br/>答案 = 堆中元素之和]

图后观察:整条流程只有一个决策点——容量超限时丢谁。因为丢弃任何一个作业都能恢复合法,且各候选对未来的影响完全对称(都只是腾出一个名额),所以丢学分最小的即为最优,这正是与"按学分从大到小、占用最晚空闲天"的贪心等价的原因。