{ }codelper
贪心优先队列(堆)自定义排序P0310

每个时段都要有k人的通宵值班

村里的节庆要安排通宵值班。
夜晚被分成编号1到L的L个时段。
n名志愿者各自报了能值班的区间。
第i名志愿者报的是从第sᵢ个时段到第eᵢ个时段,两端的时段都包括在内。
选中一名志愿者后,他要值完自己报的整个区间。
不能只用区间的一部分。
每个时段都必须有至少k名被选中的人在值班。
报了相同区间的志愿者是不同的人,要分别计数。
请输出最少要选多少人。
如果把志愿者全部选上仍有时段不足k人,请输出这类时段中编号最小的一个。

输入
第一行给出志愿者数n(1 ≤ n ≤ 10,000)、时段数L(1 ≤ L ≤ 1,000,000,000)和每个时段需要的人数k(1 ≤ k ≤ 5),用一个空格分隔。
接下来n行的第i行给出整数sᵢ和eᵢ(1 ≤ sᵢ ≤ eᵢ ≤ L),用一个空格分隔。
志愿者的给出顺序没有规律。
输出
如果每个时段都能安排至少k人,在一行输出最少要选的人数。
否则在一行输出-1和把志愿者全部选上仍不足k人的时段中最小的编号,用一个空格分隔。
数字不带逗号。
样例 1 · stdin
6 10 2
1 4
1 6
3 10
5 8
2 3
7 10
stdout
4
样例 2 · stdin
4 8 2
1 5
1 3
4 8
7 8
stdout
-1 6
main.py
输入(stdin)

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

$ 就绪

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

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

这是一道贪心题。
看还没凑够k人的最靠前的时段x,能在那里值班的只有在第x个时段或之前开始的人。
x之前已经都满了,所以无论选谁,选值班到最晚的人都不会吃亏。
因此把志愿者按开始时段排序,把开始时段不超过x的人的结束时段放进最大堆。
x缺几个人就从堆里取出几个。
在被选中的人里最早结束的那位离开之前,k个人不变,所以把x跳到那个结束时段 + 1。
L有10亿,不能一个时段一个时段地看。
第一个示例中x = 1时候选是 (1, 4) 和 (1, 6),两个都选。
较早结束的是4,所以x = 5。
第5个时段只剩在6结束的人,缺一人,在候选 (3, 10)、(5, 8)、(2, 3) 中选结束最晚的 (3, 10)。
x = 7时又缺一人,选 (7, 10),之后一直到最后都有两人,答案是4。
第二个示例中能在第6个时段值班的只有 (4, 8) 一人,所以是-1 6。
堆顶的结束时段小于x,就说明没有别人能在x值班了。
陷阱一:不能先选开始得早的人。
在第一个示例的第5个时段选 (2, 3) 或 (5, 8) 会用更多的人。
陷阱二:只看堆顶而不取出,会把一个人数好几次。
一个人只能选一次。
陷阱三:只覆盖一层的常见解法(只记住伸得最远的一个区间)在k不小于2时是错的。
被放过的候选以后还要用,必须留在堆里。
陷阱四:下一个要看的时段是被选中的人里最早的结束时段的下一个。
跳到最晚的结束时段会漏掉中间人数不足的时段。
陷阱五:结束时段也要值班。
把结束时段等于x的人当作已经结束,就会多选人。
陷阱六:输入不是按开始顺序给的。
不排序就会漏掉写在后面的较早的志愿者。
最大堆在各语言中不一样。
C++的priority_queue一开始就是最大堆。
Python的heapq只有最小堆,所以取相反数再放入;Java的PriorityQueue传入Comparator.reverseOrder()。
C#的PriorityQueue<TElement, TPriority>先取出优先级小的,所以把“−结束时段”作为优先级。
JavaScript、TypeScript、C和Craykon没有堆,要用数组(列表)自己实现。
Craykon用添加和按位置移除在列表末尾增减。
排序也要小心。
JavaScript的sort()没有比较函数时按字符串比较,10会排在9前面。
C的qsort比较函数写成(x > y) - (x < y)这样。
Craykon把编号列表交给排序(比较函数),按开始时段比较。

Pythonmain.py · 34 行
import heapqimport sys data = sys.stdin.buffer.read().split()n, length, k = int(data[0]), int(data[1]), int(data[2])shifts = sorted((int(data[3 + 2 * i]), int(data[4 + 2 * i])) for i in range(n)) cand = []     # 아직 뽑지 않았고 x칸 이전에 시작하는 사람들의 끝 칸(최대 힙: 부호를 바꿔 넣는다)active = []   # 뽑은 사람 가운데 x칸을 지키는 사람들의 끝 칸(많아야 k개)nxt = 0       # 아직 후보에 넣지 않은 첫 사람chosen = 0x = 1         # 아직 k명이 채워지지 않은 가장 앞 칸fail = 0while x <= length:    while nxt < n and shifts[nxt][0] <= x:        heapq.heappush(cand, -shifts[nxt][1])        nxt += 1    active = [e for e in active if e >= x]    # 모자란 만큼, 가장 늦게까지 서는 사람부터 뽑는다    while len(active) < k:        if not cand or -cand[0] < x:            fail = x            break        active.append(-heapq.heappop(cand))        chosen += 1    if fail:        break    # 가장 먼저 끝나는 사람이 빠지는 칸까지는 k명이 그대로 있다    x = min(active) + 1 if fail:    print(-1, fail)else:    print(chosen)
JavaScriptmain.js · 60 行
const data = require("fs").readFileSync(0, "utf8").trim().split(/\s+/).map(Number);const [n, length, k] = data;const shifts = [];for (let i = 0; i < n; i++) shifts.push([data[3 + 2 * i], data[4 + 2 * i]]);shifts.sort((a, b) => a[0] - b[0]); // 비교 함수가 없으면 문자열로 견주어 10이 9보다 앞에 온다 // JavaScript에는 힙이 없어서 배열로 최대 힙을 만든다. i의 자식은 2i+1, 2i+2function heapPush(heap, x) {  heap.push(x);  let i = heap.length - 1;  while (i > 0) {    const parent = (i - 1) >> 1;    if (heap[parent] >= heap[i]) break;    [heap[parent], heap[i]] = [heap[i], heap[parent]];    i = parent;  }} function heapPop(heap) {  const top = heap[0];  const last = heap.pop();  if (heap.length > 0) {    heap[0] = last;    let i = 0;    for (;;) {      const l = 2 * i + 1;      const r = l + 1;      let big = i;      if (l < heap.length && heap[l] > heap[big]) big = l;      if (r < heap.length && heap[r] > heap[big]) big = r;      if (big === i) break;      [heap[big], heap[i]] = [heap[i], heap[big]];      i = big;    }  }  return top;} const cand = []; // 아직 뽑지 않았고 x칸 이전에 시작하는 사람들의 끝 칸(최대 힙)let active = []; // 뽑은 사람 가운데 x칸을 지키는 사람들의 끝 칸(많아야 k개)let next = 0; // 아직 후보에 넣지 않은 첫 사람let chosen = 0;let x = 1; // 아직 k명이 채워지지 않은 가장 앞 칸let fail = 0;while (x <= length && fail === 0) {  while (next < n && shifts[next][0] <= x) heapPush(cand, shifts[next++][1]);  active = active.filter((e) => e >= x);  // 모자란 만큼, 가장 늦게까지 서는 사람부터 뽑는다  while (active.length < k) {    if (cand.length === 0 || cand[0] < x) {      fail = x;      break;    }    active.push(heapPop(cand));    chosen++;  }  // 가장 먼저 끝나는 사람이 빠지는 칸까지는 k명이 그대로 있다  if (fail === 0) x = Math.min(...active) + 1;}console.log(fail > 0 ? `-1 ${fail}` : String(chosen));
TypeScriptmain.ts · 62 行
import { readFileSync } from "node:fs"; const data: number[] = readFileSync(0, "utf8").trim().split(/\s+/).map(Number);const [n, length, k] = data;const shifts: [number, number][] = [];for (let i = 0; i < n; i++) shifts.push([data[3 + 2 * i], data[4 + 2 * i]]);shifts.sort((a, b) => a[0] - b[0]); // 비교 함수가 없으면 문자열로 견주어 10이 9보다 앞에 온다 // TypeScript에도 힙이 없어서 배열로 최대 힙을 만든다. i의 자식은 2i+1, 2i+2function heapPush(heap: number[], x: number): void {  heap.push(x);  let i = heap.length - 1;  while (i > 0) {    const parent = (i - 1) >> 1;    if (heap[parent] >= heap[i]) break;    [heap[parent], heap[i]] = [heap[i], heap[parent]];    i = parent;  }} function heapPop(heap: number[]): number {  const top: number = heap[0];  const last = heap.pop();  if (last !== undefined && heap.length > 0) {    heap[0] = last;    let i = 0;    for (;;) {      const l = 2 * i + 1;      const r = l + 1;      let big = i;      if (l < heap.length && heap[l] > heap[big]) big = l;      if (r < heap.length && heap[r] > heap[big]) big = r;      if (big === i) break;      [heap[big], heap[i]] = [heap[i], heap[big]];      i = big;    }  }  return top;} const cand: number[] = []; // 아직 뽑지 않았고 x칸 이전에 시작하는 사람들의 끝 칸(최대 힙)let active: number[] = []; // 뽑은 사람 가운데 x칸을 지키는 사람들의 끝 칸(많아야 k개)let next = 0; // 아직 후보에 넣지 않은 첫 사람let chosen = 0;let x = 1; // 아직 k명이 채워지지 않은 가장 앞 칸let fail = 0;while (x <= length && fail === 0) {  while (next < n && shifts[next][0] <= x) heapPush(cand, shifts[next++][1]);  active = active.filter((e: number) => e >= x);  // 모자란 만큼, 가장 늦게까지 서는 사람부터 뽑는다  while (active.length < k) {    if (cand.length === 0 || cand[0] < x) {      fail = x;      break;    }    active.push(heapPop(cand));    chosen++;  }  // 가장 먼저 끝나는 사람이 빠지는 칸까지는 k명이 그대로 있다  if (fail === 0) x = Math.min(...active) + 1;}console.log(fail > 0 ? `-1 ${fail}` : String(chosen));
JavaMain.java · 40 行
import java.io.*;import java.util.*; public class Main {    public static void main(String[] args) throws IOException {        StreamTokenizer st = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));        st.nextToken(); int n = (int) st.nval;        st.nextToken(); int length = (int) st.nval;        st.nextToken(); int k = (int) st.nval;        int[][] shifts = new int[n][2];        for (int i = 0; i < n; i++) {            st.nextToken(); shifts[i][0] = (int) st.nval;            st.nextToken(); shifts[i][1] = (int) st.nval;        }        Arrays.sort(shifts, (a, b) -> Integer.compare(a[0], b[0]));         // PriorityQueue는 최소 힙이라 차례를 뒤집어 최대 힙으로 쓴다        PriorityQueue<Integer> cand = new PriorityQueue<>(Comparator.reverseOrder()); // 아직 뽑지 않은 후보들의 끝 칸        List<Integer> active = new ArrayList<>(); // 뽑은 사람 가운데 x칸을 지키는 사람들의 끝 칸(많아야 k개)        int next = 0, chosen = 0, fail = 0;        int x = 1; // 아직 k명이 채워지지 않은 가장 앞 칸        while (x <= length && fail == 0) {            while (next < n && shifts[next][0] <= x) cand.add(shifts[next++][1]);            final int now = x;            active.removeIf(e -> e < now);            // 모자란 만큼, 가장 늦게까지 서는 사람부터 뽑는다            while (active.size() < k) {                if (cand.isEmpty() || cand.peek() < x) {                    fail = x;                    break;                }                active.add(cand.poll());                chosen++;            }            // 가장 먼저 끝나는 사람이 빠지는 칸까지는 k명이 그대로 있다            if (fail == 0) x = Collections.min(active) + 1;        }        System.out.println(fail > 0 ? "-1 " + fail : String.valueOf(chosen));    }}
C#Main.cs · 44 行
using System;using System.Collections.Generic;using System.Linq; class Program{    static void Main()    {        var data = Console.In.ReadToEnd().Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);        int n = int.Parse(data[0]), length = int.Parse(data[1]), k = int.Parse(data[2]);        var shifts = new (int Start, int End)[n];        for (int i = 0; i < n; i++) shifts[i] = (int.Parse(data[3 + 2 * i]), int.Parse(data[4 + 2 * i]));        Array.Sort(shifts, (a, b) => a.Start.CompareTo(b.Start));         // PriorityQueue는 우선순위가 작은 것부터 꺼내니, 우선순위에 -끝 칸을 넣어 최대 힙으로 쓴다        var cand = new PriorityQueue<int, int>(); // 아직 뽑지 않은 후보들의 끝 칸        var active = new List<int>();             // 뽑은 사람 가운데 x칸을 지키는 사람들의 끝 칸(많아야 k개)        int next = 0, chosen = 0, fail = 0;        int x = 1; // 아직 k명이 채워지지 않은 가장 앞 칸        while (x <= length && fail == 0)        {            while (next < n && shifts[next].Start <= x)            {                cand.Enqueue(shifts[next].End, -shifts[next].End);                next++;            }            active.RemoveAll(e => e < x);            // 모자란 만큼, 가장 늦게까지 서는 사람부터 뽑는다            while (active.Count < k)            {                if (cand.Count == 0 || cand.Peek() < x)                {                    fail = x;                    break;                }                active.Add(cand.Dequeue());                chosen++;            }            // 가장 먼저 끝나는 사람이 빠지는 칸까지는 k명이 그대로 있다            if (fail == 0) x = active.Min() + 1;        }        Console.WriteLine(fail > 0 ? $"-1 {fail}" : chosen.ToString());    }}
C++main.cpp · 39 行
#include <algorithm>#include <iostream>#include <queue>#include <vector>using namespace std; int main() {    ios::sync_with_stdio(false);    cin.tie(nullptr);    int n, length, k;    cin >> n >> length >> k;    vector<pair<int, int>> shifts(n); // (시작 칸, 끝 칸)    for (auto &s : shifts) cin >> s.first >> s.second;    sort(shifts.begin(), shifts.end());     priority_queue<int> cand; // 아직 뽑지 않은 후보들의 끝 칸. priority_queue는 처음부터 최대 힙이다    vector<int> active;       // 뽑은 사람 가운데 x칸을 지키는 사람들의 끝 칸(많아야 k개)    int next = 0, chosen = 0, fail = 0;    int x = 1; // 아직 k명이 채워지지 않은 가장 앞 칸    while (x <= length && fail == 0) {        while (next < n && shifts[next].first <= x) cand.push(shifts[next++].second);        active.erase(remove_if(active.begin(), active.end(), [x](int e) { return e < x; }), active.end());        // 모자란 만큼, 가장 늦게까지 서는 사람부터 뽑는다        while ((int)active.size() < k) {            if (cand.empty() || cand.top() < x) {                fail = x;                break;            }            active.push_back(cand.top());            cand.pop();            chosen++;        }        // 가장 먼저 끝나는 사람이 빠지는 칸까지는 k명이 그대로 있다        if (fail == 0) x = *min_element(active.begin(), active.end()) + 1;    }    if (fail > 0) cout << "-1 " << fail << '\n';    else cout << chosen << '\n';    return 0;}
Cmain.c · 87 行
#include <stdio.h>#include <stdlib.h> #define MAXN 10000#define MAXK 5 typedef struct {    int start, end;} Shift; static Shift shifts[MAXN];static int heap[MAXN]; /* 최대 힙: heap[0]이 가장 크고, i의 자식은 2i+1, 2i+2 */static int heapSize = 0; static int byStart(const void *a, const void *b) {    int x = ((const Shift *)a)->start, y = ((const Shift *)b)->start;    return (x > y) - (x < y);} static void heapPush(int x) {    int i = heapSize++;    heap[i] = x;    while (i > 0) {        int parent = (i - 1) / 2;        if (heap[parent] >= heap[i]) break;        int t = heap[parent];        heap[parent] = heap[i];        heap[i] = t;        i = parent;    }} static int heapPop(void) {    int top = heap[0];    heap[0] = heap[--heapSize];    int i = 0;    for (;;) {        int l = 2 * i + 1, r = l + 1, big = i;        if (l < heapSize && heap[l] > heap[big]) big = l;        if (r < heapSize && heap[r] > heap[big]) big = r;        if (big == i) break;        int t = heap[big];        heap[big] = heap[i];        heap[i] = t;        i = big;    }    return top;} int main(void) {    int n, length, k;    if (scanf("%d %d %d", &n, &length, &k) != 3) return 1;    for (int i = 0; i < n; i++) scanf("%d %d", &shifts[i].start, &shifts[i].end);    qsort(shifts, (size_t)n, sizeof(Shift), byStart);     int active[MAXK]; /* 뽑은 사람 가운데 x칸을 지키는 사람들의 끝 칸(많아야 k개) */    int activeCount = 0;    int next = 0, chosen = 0, fail = 0;    int x = 1; /* 아직 k명이 채워지지 않은 가장 앞 칸 */    while (x <= length && fail == 0) {        while (next < n && shifts[next].start <= x) heapPush(shifts[next++].end);        /* x칸보다 먼저 끝난 사람을 뺀다 */        int kept = 0;        for (int i = 0; i < activeCount; i++)            if (active[i] >= x) active[kept++] = active[i];        activeCount = kept;        /* 모자란 만큼, 가장 늦게까지 서는 사람부터 뽑는다 */        while (activeCount < k) {            if (heapSize == 0 || heap[0] < x) {                fail = x;                break;            }            active[activeCount++] = heapPop();            chosen++;        }        if (fail == 0) {            /* 가장 먼저 끝나는 사람이 빠지는 칸까지는 k명이 그대로 있다 */            int first = active[0];            for (int i = 1; i < activeCount; i++)                if (active[i] < first) first = active[i];            x = first + 1;        }    }    if (fail > 0) printf("-1 %d\n", fail);    else printf("%d\n", chosen);    return 0;}
Craykonmain.cray · 127 行
// 큰 수가 맨 위에 오는 최대 힙(목록 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;} 첫줄 = 控制台.读数字();n = 첫줄[0];length = 첫줄[1];k = 첫줄[2];starts = [];ends = [];order = [];循环 (i = 0; i < n; i++){    줄 = 控制台.读数字();    starts.添加(줄[0]);    ends.添加(줄[1]);    order.添加(i);} // 시작 칸이 이른 사람부터 보도록 번호를 정렬한다byStart(a, b){    返回 starts[a] - starts[b];} order.排序(byStart); cand = []; // 아직 뽑지 않았고 x칸 이전에 시작하는 사람들의 끝 칸(최대 힙)active = []; // 뽑은 사람 가운데 x칸을 지키는 사람들의 끝 칸(많아야 k개)next = 0; // 아직 후보에 넣지 않은 첫 사람chosen = 0;x = 1; // 아직 k명이 채워지지 않은 가장 앞 칸fail = 0;当 (x <= length && fail == 0){    当 (next < n && starts[order[next]] <= x)    {        힙넣기(cand, ends[order[next]]);        next += 1;    }    // x칸보다 먼저 끝난 사람을 뺀다    still = [];    遍历 (e 在 active)    {        如果 (e >= x)        {            still.添加(e);        }    }    active = still;    // 모자란 만큼, 가장 늦게까지 서는 사람부터 뽑는다    当 (active.数量 < k)    {        如果 (cand.数量 == 0 || cand[0] < x)        {            fail = x;            跳出;        }        active.添加(힙빼기(cand));        chosen += 1;    }    如果 (fail == 0)    {        // 가장 먼저 끝나는 사람이 빠지는 칸까지는 k명이 그대로 있다        first = active[0];        遍历 (e 在 active)        {            first = 数学.较小值(first, e);        }        x = first + 1;    }} 如果 (fail > 0){    控制台.输出("-1 " + fail);}否则{    控制台.输出(chosen);}