[USACO08NOV] Time Management S

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

按截止时间从晚到早排序,倒着把每个任务贴到最晚可完成时刻,得到最迟开始时间。

OJ: luogu

题目 ID: P2920

难度:普及/提高-

标签:贪心排序模拟

日期: 2026-06-18 19:42

题意

N 个工作。第 i 个工作需要 T_i 时间完成,并且必须在时刻 S_i 或之前完成。

工作一旦开始就必须连续做完,不能中断。要求求出最晚可以从什么时候开始工作,使得所有工作都能按时完成。如果从 0 开始也无法完成,输出 -1

思路

先看一个可以直接验证想法的朴素解:

cpp
#include <bits/stdc++.h>
using namespace std;

struct Job {
    int t;
    int s;
};

int n;
Job job[10];
bool used[10];

bool dfs(int done, int now) {
    if (done == n) return true;

    for (int i = 1; i <= n; i++) {
        if (used[i]) continue;
        int finishTime = now + job[i].t;
        if (finishTime > job[i].s) continue;

        used[i] = true;
        if (dfs(done + 1, finishTime)) return true;
        used[i] = false;
    }
    return false;
}

bool canStart(int startTime) {
    memset(used, 0, sizeof(used));
    return dfs(0, startTime);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    int maxS = 0;
    for (int i = 1; i <= n; i++) {
        cin >> job[i].t >> job[i].s;
        maxS = max(maxS, job[i].s);
    }

    for (int startTime = maxS; startTime >= 0; startTime--) {
        if (canStart(startTime)) {
            cout << startTime << '\n';
            return 0;
        }
    }

    cout << -1 << '\n';
    return 0;
}

朴素解枚举开始时间,并用 DFS 尝试所有任务顺序。它适合小数据验证,但 N 最大是 1000,不能枚举排列。

这题可以反过来想:既然要求“最迟开始”,那就从最后往前安排任务。

截止时间越晚的任务越适合放在后面。于是把所有任务按截止时间 S 从大到小排序,然后倒推:

text
当前任务最晚完成时间 = min(后面任务留出的时间, 当前任务截止时间)
当前任务最晚开始时间 = 当前任务最晚完成时间 - 当前任务耗时

处理完所有任务后,得到的时间就是整套任务最晚可以开始的时间。

样例倒推过程

样例任务按截止时间从大到小排序:

任务耗时 T 截止时间 S 处理前 now 处理后 now
5 20 20 15
1 16 15 14
8 14 14 6
3 5 6 2

最后得到 2,表示最迟从时刻 2 开始。

这里读表时要注意:表格是从后往前安排任务。真正正向执行时,顺序会反过来。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

struct Job {
    int t;
    int s;
};

Job job[1005];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> job[i].t >> job[i].s;
    }

    sort(job + 1, job + n + 1, [](const Job &a, const Job &b) {
        return a.s > b.s;
    });

    int now = job[1].s;
    for (int i = 1; i <= n; i++) {
        // 当前任务必须不晚于自己的截止时间完成,也不能晚于后面任务留出的时间。
        if (now > job[i].s) now = job[i].s;
        now -= job[i].t;
    }

    if (now < 0) {
        cout << -1 << '\n';
    } else {
        cout << now << '\n';
    }

    return 0;
}

复杂度

  • 排序任务需要 O(NlogN)O(N log N)
  • 倒推扫描一次需要 O(N)O(N)
  • 总时间复杂度 O(NlogN)O(N log N)
  • 空间复杂度 O(N)O(N)

总结

这题虽然在二分训练单里,但最直接的做法是排序贪心。

“最迟开始”适合反向考虑:先把后面的任务尽量贴近截止时间,再一步步往前推。只要最终时间不小于 0,它就是答案;否则说明所有任务从 0 开始也无法按时完成。

一图流解析

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

一图流解析