{ }codelper
贪心优先队列(堆)P0152

挑选能在截止前完成的 3D 打印订单

一台 3D 打印机收到了 n 个打印订单。
第 i 个订单打印需要 t 小时,必须在时刻 d 之前完成。
打印机从时刻 0 起可用,一次只打印一个订单,订单一旦开始就不停顿地打印到底。
订单恰好在时刻 d 完成也算按时。
从收到的订单中挑选一部分,要让挑出的订单全部按时完成。
挑出的订单可以按任意顺序打印,没被挑选的订单不打印。
让挑出的订单数尽可能多。
如果有多种挑法都能做到,选打印时间总和最小的那种。
把挑出的订单数和它们的打印时间总和用空格分隔输出在一行。
一个也挑不出时输出 0 0。

输入
第一行 n,接下来 n 行每行给出用空格分隔的 t 和 d。
1 ≤ n ≤ 10,000,1 ≤ t ≤ 1,000,000,1 ≤ d ≤ 10,000,000,000
输出
挑出的订单数和打印时间总和,用空格分隔输出在一行
样例 1 · stdin
5
4 4
1 5
1 5
1 6
1 6
stdout
4 4
样例 2 · stdin
2
5 3
7 6
stdout
0 0
main.py
输入(stdin)

运行时这些行会依次作为 input()/标准输入传入(运行中不会等待输入)。

$ 就绪

请在电脑上打开以编写和运行代码。在手机上可以查看题目和题解。

其他语言怎么写?查看各语言题解 · 8 种语言题解

挑出的订单最好按截止时刻从早到晚连续打印。
所以把订单按截止时刻排序后逐个加入,当累计时间超过这一单的截止时刻时,就从已挑的订单里去掉最耗时的一个(贪心——数量少一个,但剩下的时间减得最多)。
要快速取出最长的,需要最大堆。
这样剩下的就是数量最多、并且其中时间总和最小的挑法。
陷阱一:放不下的订单直接跳过是错的——第一个例子里先放进 4 小时的订单,就只能再放两个 1 小时的,共 3 个;去掉 4 小时的则是 4 个。
陷阱二:从短订单开始放,截止晚的短订单会抢走截止早的订单的位置。
陷阱三:时间总和可达 100 亿,截止时刻也超过 int,所以 C、C++ 要用 long long,Java、C# 要用 long(Java、C# 的 int 超过约 21 亿就会绕成负数。
Python、JavaScript、TypeScript、Craykon 直接就行——远小于 2^53)。
陷阱四:得到最大堆的方法在各语言中正好相反。
C++ 的 std::priority_queue 默认就是最大堆,直接用;Java 的 PriorityQueue 默认是最小堆,要传 Collections.reverseOrder()(不传就会去掉最短的订单)。
Python heapq 只有最小堆,所以放入 -t,取出后再把符号变回来;C# PriorityQueue<long, long> 把优先级设为 -t。
JavaScript、TypeScript、C、Craykon 用列表自己写堆,把比较方向反过来,让父节点大于等于子节点(父节点位置在 Craykon 中是 数学.商(i - 1, 2))。
排序规则的写法也不同:C 的 qsort 比较函数返回 int,所以不要把 long long 截止时刻相减,而是用 (a > b) - (a < b);Craykon 给 jobs.排序(byDeadline) 传一个返回 x[1] - y[1] 的函数(数是双精度,相减没问题)。

Pythonmain.py · 17 行
import heapqimport sys data = sys.stdin.read().split()n = int(data[0])jobs = [(int(data[1 + 2 * i]), int(data[2 + 2 * i])) for i in range(n)]  # (걸리는 시간, 마감) jobs.sort(key=lambda job: job[1])  # 마감이 이른 것부터chosen = []  # 고른 주문의 시간들. heapq는 최소 힙이라 음수로 넣어 가장 긴 것을 꺼낸다total = 0  # 고른 주문을 마감 순으로 이어 뽑으면 마지막이 끝나는 시각for t, d in jobs:    heapq.heappush(chosen, -t)    total += t    if total > d:        total += heapq.heappop(chosen)  # 마감을 넘으면 고른 것 가운데 가장 긴 주문을 뺀다 print(len(chosen), total)
JavaScriptmain.js · 55 行
const data = require("fs").readFileSync(0, "utf8").trim().split(/\s+/).map(Number);const n = data[0]; // 최대 힙: heap[0]이 가장 크고, i의 자식은 2i+1, 2i+2class MaxHeap {  constructor() {    this.items = [];  }  get size() {    return this.items.length;  }  push(x) {    const a = this.items;    a.push(x);    let i = a.length - 1;    while (i > 0) {      const parent = (i - 1) >> 1;      if (a[parent] >= a[i]) break;      [a[parent], a[i]] = [a[i], a[parent]];      i = parent;    }  }  pop() {    const a = this.items;    const top = a[0];    const last = a.pop();    if (a.length > 0) {      a[0] = last;      let i = 0;      for (;;) {        const l = 2 * i + 1, r = l + 1;        let big = i;        if (l < a.length && a[l] > a[big]) big = l;        if (r < a.length && a[r] > a[big]) big = r;        if (big === i) break;        [a[big], a[i]] = [a[i], a[big]];        i = big;      }    }    return top;  }} const jobs = []; // [걸리는 시간, 마감]for (let i = 0; i < n; i++) jobs.push([data[1 + 2 * i], data[2 + 2 * i]]);jobs.sort((x, y) => x[1] - y[1]); // 마감이 이른 것부터 const chosen = new MaxHeap(); // 고른 주문의 시간들let total = 0; // 고른 주문을 마감 순으로 이어 뽑으면 마지막이 끝나는 시각for (const [t, d] of jobs) {  chosen.push(t);  total += t;  if (total > d) total -= chosen.pop(); // 마감을 넘으면 고른 것 가운데 가장 긴 주문을 뺀다}console.log(`${chosen.size} ${total}`);
TypeScriptmain.ts · 58 行
import { readFileSync } from "node:fs"; const data: number[] = readFileSync(0, "utf8").trim().split(/\s+/).map(Number);const n = data[0]; // 최대 힙: heap[0]이 가장 크고, i의 자식은 2i+1, 2i+2class MaxHeap {  private items: number[] = [];   get size(): number {    return this.items.length;  }   push(x: number): void {    const a = this.items;    a.push(x);    let i = a.length - 1;    while (i > 0) {      const parent = (i - 1) >> 1;      if (a[parent] >= a[i]) break;      [a[parent], a[i]] = [a[i], a[parent]];      i = parent;    }  }   pop(): number {    const a = this.items;    const top = a[0];    const last = a.pop()!; // size > 0일 때만 부른다    if (a.length > 0) {      a[0] = last;      let i = 0;      for (;;) {        const l = 2 * i + 1, r = l + 1;        let big = i;        if (l < a.length && a[l] > a[big]) big = l;        if (r < a.length && a[r] > a[big]) big = r;        if (big === i) break;        [a[big], a[i]] = [a[i], a[big]];        i = big;      }    }    return top;  }} const jobs: [time: number, deadline: number][] = [];for (let i = 0; i < n; i++) jobs.push([data[1 + 2 * i], data[2 + 2 * i]]);jobs.sort((x, y) => x[1] - y[1]); // 마감이 이른 것부터 const chosen = new MaxHeap(); // 고른 주문의 시간들let total = 0; // 고른 주문을 마감 순으로 이어 뽑으면 마지막이 끝나는 시각for (const [t, d] of jobs) {  chosen.push(t);  total += t;  if (total > d) total -= chosen.pop(); // 마감을 넘으면 고른 것 가운데 가장 긴 주문을 뺀다}console.log(`${chosen.size} ${total}`);
JavaMain.java · 30 行
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.Arrays;import java.util.Collections;import java.util.PriorityQueue;import java.util.StringTokenizer; public class Main {    public static void main(String[] args) throws IOException {        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));        int n = Integer.parseInt(in.readLine().trim());        long[][] jobs = new long[n][]; // {걸리는 시간, 마감}. 마감은 int를 넘는다        for (int i = 0; i < n; i++) {            StringTokenizer st = new StringTokenizer(in.readLine());            jobs[i] = new long[] {Long.parseLong(st.nextToken()), Long.parseLong(st.nextToken())};        }        Arrays.sort(jobs, (x, y) -> Long.compare(x[1], y[1])); // 마감이 이른 것부터         // PriorityQueue는 기본이 최소 힙이라 reverseOrder()로 가장 긴 것이 먼저 나오게 한다        PriorityQueue<Long> chosen = new PriorityQueue<>(Collections.reverseOrder());        long total = 0; // 고른 주문을 마감 순으로 이어 뽑으면 마지막이 끝나는 시각        for (long[] job : jobs) {            chosen.add(job[0]);            total += job[0];            if (total > job[1]) total -= chosen.poll(); // 마감을 넘으면 가장 긴 주문을 뺀다        }        System.out.println(chosen.size() + " " + total);    }}
C#Main.cs · 28 行
using System;using System.Collections.Generic; class Program{    static void Main()    {        int n = int.Parse(Console.ReadLine().Trim());        var jobs = new (long t, long d)[n]; // 마감은 int를 넘는다        for (int i = 0; i < n; i++)        {            string[] p = Console.ReadLine().Split(' ', StringSplitOptions.RemoveEmptyEntries);            jobs[i] = (long.Parse(p[0]), long.Parse(p[1]));        }        Array.Sort(jobs, (x, y) => x.d.CompareTo(y.d)); // 마감이 이른 것부터         // PriorityQueue는 우선순위가 작은 것부터 나온다 → 우선순위를 -t로 주면 가장 긴 것이 먼저        var chosen = new PriorityQueue<long, long>();        long total = 0; // 고른 주문을 마감 순으로 이어 뽑으면 마지막이 끝나는 시각        foreach (var (t, d) in jobs)        {            chosen.Enqueue(t, -t);            total += t;            if (total > d) total -= chosen.Dequeue(); // 마감을 넘으면 가장 긴 주문을 뺀다        }        Console.WriteLine($"{chosen.Count} {total}");    }}
C++main.cpp · 27 行
#include <algorithm>#include <iostream>#include <queue>#include <utility>#include <vector> int main() {    std::ios::sync_with_stdio(false);    std::cin.tie(nullptr);    int n;    std::cin >> n;    std::vector<std::pair<long long, long long>> jobs(n); // (마감, 걸리는 시간). 마감은 int를 넘는다    for (auto& [d, t] : jobs) std::cin >> t >> d;    std::sort(jobs.begin(), jobs.end()); // 마감이 이른 것부터     std::priority_queue<long long> chosen; // 기본이 최대 힙: top()이 가장 긴 주문    long long total = 0; // 고른 주문을 마감 순으로 이어 뽑으면 마지막이 끝나는 시각    for (auto [d, t] : jobs) {        chosen.push(t);        total += t;        if (total > d) { // 마감을 넘으면 가장 긴 주문을 뺀다            total -= chosen.top();            chosen.pop();        }    }    std::cout << chosen.size() << " " << total << "\n";}
Cmain.c · 63 行
#include <stdio.h>#include <stdlib.h> #define MAXN 10000 typedef struct {    long long t, d; /* 걸리는 시간, 마감(int를 넘는다) */} Job; static Job jobs[MAXN];static long long heap[MAXN]; /* 최대 힙: heap[0]이 가장 크다 */static int heap_size = 0; static int by_deadline(const void* x, const void* y) {    long long a = ((const Job*)x)->d, b = ((const Job*)y)->d;    return (a > b) - (a < b); /* 빼면 int를 넘을 수 있어 비교로 */} static void swap(long long* x, long long* y) {    long long t = *x;    *x = *y;    *y = t;} static void heap_push(long long x) {    int i = heap_size++;    heap[i] = x;    while (i > 0 && heap[(i - 1) / 2] < heap[i]) {        swap(&heap[(i - 1) / 2], &heap[i]);        i = (i - 1) / 2;    }} static long long heap_pop(void) {    long long top = heap[0];    heap[0] = heap[--heap_size];    int i = 0;    for (;;) {        int l = 2 * i + 1, r = l + 1, big = i;        if (l < heap_size && heap[l] > heap[big]) big = l;        if (r < heap_size && heap[r] > heap[big]) big = r;        if (big == i) break;        swap(&heap[big], &heap[i]);        i = big;    }    return top;} int main(void) {    int n;    scanf("%d", &n);    for (int i = 0; i < n; i++) scanf("%lld %lld", &jobs[i].t, &jobs[i].d);    qsort(jobs, n, sizeof(Job), by_deadline); /* 마감이 이른 것부터 */     long long total = 0; /* 고른 주문을 마감 순으로 이어 뽑으면 마지막이 끝나는 시각 */    for (int i = 0; i < n; i++) {        heap_push(jobs[i].t);        total += jobs[i].t;        if (total > jobs[i].d) total -= heap_pop(); /* 마감을 넘으면 가장 긴 주문을 뺀다 */    }    printf("%d %lld\n", heap_size, total);    return 0;}
Craykonmain.cray · 76 行
// 최대 힙(목록 heap): heap[0]이 가장 크고, i의 자식은 2i+1, 2i+2힙넣기(heap, x){    heap.添加(x);    i = heap.数量 - 1;    当 (i > 0)    {        부모 = 数学.商(i - 1, 2);        如果 (heap[부모] >= heap[i])        {            跳出;        }        t = heap[부모];        heap[부모] = heap[i];        heap[i] = t;        i = 부모;    }} 힙꺼내기(heap){    top = heap[0];    heap[0] = heap[heap.数量 - 1];    heap.按位置移除(heap.数量 - 1);    i = 0;    当 (真)    {        l = 2 * i + 1;        r = l + 1;        big = i;        如果 (l < heap.数量 && heap[l] > heap[big])        {            big = l;        }        如果 (r < heap.数量 && heap[r] > heap[big])        {            big = r;        }        如果 (big == i)        {            跳出;        }        t = heap[big];        heap[big] = heap[i];        heap[i] = t;        i = big;    }    返回 top;} // 정렬 기준: 마감이 이른 주문이 앞byDeadline(x, y){    返回 x[1] - y[1];} n = 控制台.读数字()[0];jobs = []; // [걸리는 시간, 마감]循环 (i = 0; i < n; i++){    jobs.添加(控制台.读数字());}jobs.排序(byDeadline); chosen = []; // 고른 주문의 시간들(최대 힙)total = 0; // 고른 주문을 마감 순으로 이어 뽑으면 마지막이 끝나는 시각遍历 (job 在 jobs){    힙넣기(chosen, job[0]);    total += job[0];    如果 (total > job[1])    {        total -= 힙꺼내기(chosen); // 마감을 넘으면 고른 것 가운데 가장 긴 주문을 뺀다    }}控制台.输出(chosen.数量 + " " + total);