{ }codelper
優先度付きキュー(ヒープ)シミュレーション64ビット整数P0211

期限の早いパンから売るパン屋

パン屋で一日に起きた出来事 q 個が、起きた順に与えられる。
出来事は二種類ある。
"bake t e c" は、時刻 t にパン c 個を一まとまりとして焼き、これらのパンは時刻 e まで(e を含む)売れることを表す。
"buy t k" は、時刻 t に客が一人、パンを k 個買いに来たことを表す。
客が来ると、パン屋はその時刻に売れるパン、つまり e ≥ t のパンだけを売る。
売れるパンのうち、期限 e が最も早いパンから順に k 個を渡す。
一まとまりのうち一部だけを売ることもあり、残りのパンは同じ期限のまま残る。
売れるパンが k 個より少なければ、あるパンをすべて渡す。
期限が同じパンどうしは、どれを先に渡しても答えは同じである。
同じ時刻の出来事は、入力に書かれた順に起きる。
buy の出来事ごとに一行に、客が受け取ったパンの数と、受け取ったパンごとの (e − t) の和(残り期限の合計)を空白で区切って出力せよ。
受け取ったパンがなければ "0 0" と出力する。

入力
一行目に q、続く q 行に出来事が一つずつ与えられる。
1 ≤ q ≤ 10,000、0 ≤ t ≤ e ≤ 1,000,000,000、1 ≤ c, k ≤ 1,000,000。
出来事の t は減らず、buy の出来事が一つ以上ある。
出力
buy の出来事ごとに一行 "受け取った数 残り期限の合計"
例 1 · stdin
6
bake 1 5 3
bake 2 3 2
buy 3 4
buy 6 2
bake 6 6 1
buy 6 5
stdout
4 4
0 0
1 0
main.py
入力(stdin)

実行すると、これらの行が順に input()・標準入力に渡されます(実行中に入力を求めません)。

$ 準備完了

コードを入力して解くにはパソコンで開いてください。モバイルでは問題と解答を見られます。

ほかの言語では?言語別の解答を見る · 8 言語の解答

パンのまとまりを期限 e をキーとする最小ヒープに入れれば、いつも先頭(最も早い期限)から売れます。
期限切れのまとまりを前もって探して消す必要はなく、客が来たときに先頭が e < t なら取り出して捨てればよいです(遅延削除)— 時刻は減らないので、一度期限が切れたまとまりはもう売れません。
まとまりはパン一個ずつではなく [期限, 残り個数] の一つの要素にします(c は 100 万まであり、一個ずつ入れると多すぎます)。
一部だけ売ったら個数を減らすだけで、期限(キー)は変わらないのでヒープの順序は崩れません。
落とし穴その一:期限ちょうど(e == t)でもまだ売れます — e <= t で捨てると間違えます。
落とし穴その二:最大ヒープだと期限の遅いパンから出ていきます。
例の最初の客は最小ヒープなら "4 4"、最大ヒープなら "4 6" です。
C++ の std::priority_queue は既定が最大ヒープなので std::greater<std::pair<long long, long long>> を渡す必要があり、top() は書き換えられないので取り出してから残りの個数で入れ直します。
落とし穴その三:一部だけ売ったまとまりを丸ごと取り出すと、残りのパンが消えます。
落とし穴その四:残り期限の合計は 100 万 × 10 億 = 10^15 まで行き、32 ビットの int(約 21 億)を超えます。
C・C++ は long long、Java・C# は long が必要で、Python の整数には上限がなく、JavaScript・TypeScript・Craykon の数は倍精度浮動小数点数で 2^53(約 9×10^15)まで正確なので、そのままで大丈夫です。
ヒープは言語ごとに違います:Python heapq はリスト [期限, 個数] を先頭から比べるのでそのまま使え、heap[0][1] を書き換えても構いません。
Java PriorityQueue<long[]> には比較関数 (x, y) -> Long.compare(x[0], y[0]) を渡し、peek() で見た配列を書き換えます。
C# PriorityQueue<long[], long> は値と優先度(期限)を別々に入れ、TryPeek で両方を見ます。
JavaScript・TypeScript・C・Craykon はヒープがないのでリスト(配列)で自作します:i の親は (i - 1) / 2(整数の割り算 — Craykon 数学.商(i - 1, 2))、子は 2i + 1・2i + 2。
Craykon は行を コンソール.行読み().空白除去().分ける(" ") で分け、単語を .数に() で数にします。
ヒープの要素数は bake の出来事の数(10,000 未満)を超えないので、リストの上限内です。

Pythonmain.py · 31 行
import heapqimport sys lines = sys.stdin.read().split("\n")q = int(lines[0])heap = []  # [기한, 남은 개수]: 기한이 가장 이른 묶음이 heap[0]out = []for line in lines[1 : 1 + q]:    w = line.split()    t = int(w[1])    if w[0] == "bake":        heapq.heappush(heap, [int(w[2]), int(w[3])])        continue    need = int(w[2])    got = 0    total = 0  # 받은 빵마다 (기한 - t)를 더한 값    while need > 0 and heap:        e, c = heap[0]        if e < t:  # 기한이 지난 묶음은 버린다(e == t이면 아직 팔 수 있다)            heapq.heappop(heap)            continue        take = min(c, need)        got += take        total += take * (e - t)        need -= take        if take == c:            heapq.heappop(heap)        else:            heap[0][1] = c - take  # 개수만 줄인다: 기한은 그대로라 힙 차례가 깨지지 않는다    out.append(f"{got} {total}")print("\n".join(out))
JavaScriptmain.js · 75 行
const lines = require("fs").readFileSync(0, "utf8").split("\n");const q = Number(lines[0]); // 최소 힙: 칸마다 [기한, 남은 개수]. heap[0]이 기한이 가장 이른 묶음이고, i의 자식은 2i+1, 2i+2class MinHeap {  constructor() {    this.items = [];  }  get size() {    return this.items.length;  }  top() {    return this.items[0];  }  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][0] <= a[i][0]) 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 small = i;        if (l < a.length && a[l][0] < a[small][0]) small = l;        if (r < a.length && a[r][0] < a[small][0]) small = r;        if (small === i) break;        [a[small], a[i]] = [a[i], a[small]];        i = small;      }    }    return top;  }} const heap = new MinHeap();const out = [];for (let i = 1; i <= q; i++) {  const w = lines[i].trim().split(" ");  const t = Number(w[1]);  if (w[0] === "bake") {    heap.push([Number(w[2]), Number(w[3])]);    continue;  }  let need = Number(w[2]);  let got = 0;  let total = 0; // 받은 빵마다 (기한 - t)를 더한 값: 10^15까지라 배정밀도 수로도 정확하다  while (need > 0 && heap.size > 0) {    const batch = heap.top();    const [e, c] = batch;    if (e < t) { // 기한이 지난 묶음은 버린다(e === t이면 아직 팔 수 있다)      heap.pop();      continue;    }    const take = Math.min(c, need);    got += take;    total += take * (e - t);    need -= take;    if (take === c) heap.pop();    else batch[1] = c - take; // 개수만 줄인다: 기한은 그대로라 힙 차례가 깨지지 않는다  }  out.push(`${got} ${total}`);}console.log(out.join("\n"));
TypeScriptmain.ts · 81 行
import { readFileSync } from "node:fs"; const lines: string[] = readFileSync(0, "utf8").split("\n");const q: number = Number(lines[0]); type Batch = [expiry: number, count: number]; // 최소 힙: heap[0]이 기한이 가장 이른 묶음이고, i의 자식은 2i+1, 2i+2class MinHeap {  private items: Batch[] = [];   get size(): number {    return this.items.length;  }   top(): Batch {    return this.items[0];  }   push(x: Batch): void {    const a = this.items;    a.push(x);    let i = a.length - 1;    while (i > 0) {      const parent = (i - 1) >> 1;      if (a[parent][0] <= a[i][0]) break;      [a[parent], a[i]] = [a[i], a[parent]];      i = parent;    }  }   pop(): Batch {    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 small = i;        if (l < a.length && a[l][0] < a[small][0]) small = l;        if (r < a.length && a[r][0] < a[small][0]) small = r;        if (small === i) break;        [a[small], a[i]] = [a[i], a[small]];        i = small;      }    }    return top;  }} const heap = new MinHeap();const out: string[] = [];for (let i = 1; i <= q; i++) {  const w: string[] = lines[i].trim().split(" ");  const t: number = Number(w[1]);  if (w[0] === "bake") {    heap.push([Number(w[2]), Number(w[3])]);    continue;  }  let need: number = Number(w[2]);  let got = 0;  let total = 0; // 받은 빵마다 (기한 - t)를 더한 값: 10^15까지라 number로도 정확하다  while (need > 0 && heap.size > 0) {    const batch: Batch = heap.top();    const [e, c] = batch;    if (e < t) { // 기한이 지난 묶음은 버린다(e === t이면 아직 팔 수 있다)      heap.pop();      continue;    }    const take: number = Math.min(c, need);    got += take;    total += take * (e - t);    need -= take;    if (take === c) heap.pop();    else batch[1] = c - take; // 개수만 줄인다: 기한은 그대로라 힙 차례가 깨지지 않는다  }  out.push(`${got} ${total}`);}console.log(out.join("\n"));
JavaMain.java · 44 行
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;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 q = Integer.parseInt(in.readLine().trim());        // {기한, 남은 개수}를 기한이 이른 것부터 꺼내는 최소 힙        PriorityQueue<long[]> heap = new PriorityQueue<>((x, y) -> Long.compare(x[0], y[0]));        StringBuilder out = new StringBuilder();        for (int i = 0; i < q; i++) {            StringTokenizer st = new StringTokenizer(in.readLine());            String kind = st.nextToken();            long t = Long.parseLong(st.nextToken());            if (kind.equals("bake")) {                long e = Long.parseLong(st.nextToken());                long c = Long.parseLong(st.nextToken());                heap.add(new long[] {e, c});                continue;            }            long need = Long.parseLong(st.nextToken());            long got = 0;            long total = 0; // 받은 빵마다 (기한 - t)를 더한 값: 10^15까지라 long이 필요하다            while (need > 0 && !heap.isEmpty()) {                long[] batch = heap.peek();                if (batch[0] < t) { // 기한이 지난 묶음은 버린다(같은 시각이면 아직 팔 수 있다)                    heap.poll();                    continue;                }                long take = Math.min(batch[1], need);                got += take;                total += take * (batch[0] - t);                need -= take;                if (take == batch[1]) heap.poll();                else batch[1] -= take; // 개수만 줄인다: 기한은 그대로라 힙 차례가 깨지지 않는다            }            out.append(got).append(' ').append(total).append('\n');        }        System.out.print(out);    }}
C#Main.cs · 44 行
using System;using System.Collections.Generic;using System.Text; class Program{    static void Main()    {        int q = int.Parse(Console.ReadLine().Trim());        // PriorityQueue<값, 우선순위>: 우선순위(기한)가 이른 묶음부터 나온다.        // 값은 남은 개수를 고칠 수 있게 길이 1짜리 배열로 둔다        var heap = new PriorityQueue<long[], long>();        var sb = new StringBuilder();        for (int i = 0; i < q; i++)        {            string[] w = Console.ReadLine().Split(' ', StringSplitOptions.RemoveEmptyEntries);            long t = long.Parse(w[1]);            if (w[0] == "bake")            {                heap.Enqueue(new long[] { long.Parse(w[3]) }, long.Parse(w[2]));                continue;            }            long need = long.Parse(w[2]);            long got = 0;            long total = 0; // 받은 빵마다 (기한 - t)를 더한 값: 10^15까지라 long이 필요하다            while (need > 0 && heap.TryPeek(out long[] left, out long e))            {                if (e < t) // 기한이 지난 묶음은 버린다(같은 시각이면 아직 팔 수 있다)                {                    heap.Dequeue();                    continue;                }                long take = Math.Min(left[0], need);                got += take;                total += take * (e - t);                need -= take;                if (take == left[0]) heap.Dequeue();                else left[0] -= take; // 개수만 줄인다: 우선순위(기한)는 그대로다            }            sb.Append(got).Append(' ').Append(total).Append('\n');        }        Console.Write(sb.ToString());    }}
C++main.cpp · 45 行
#include <algorithm>#include <iostream>#include <queue>#include <string>#include <utility>#include <vector> int main() {    std::ios::sync_with_stdio(false);    std::cin.tie(nullptr);    int q;    std::cin >> q;    // (기한, 남은 개수): std::greater를 줘야 기한이 이른 것부터 나온다(기본은 최대 힙)    using Batch = std::pair<long long, long long>;    std::priority_queue<Batch, std::vector<Batch>, std::greater<Batch>> heap;    std::string out;    for (int i = 0; i < q; i++) {        std::string kind;        long long t;        std::cin >> kind >> t;        if (kind == "bake") {            long long e, c;            std::cin >> e >> c;            heap.push({e, c});            continue;        }        long long need;        std::cin >> need;        long long got = 0, total = 0;  // total: 받은 빵마다 (기한 - t)를 더한 값, 10^15까지        while (need > 0 && !heap.empty()) {            auto [e, c] = heap.top();            heap.pop();            if (e < t) continue;  // 기한이 지난 묶음은 버린다(같은 시각이면 아직 팔 수 있다)            long long take = std::min(c, need);            got += take;            total += take * (e - t);            need -= take;            // top()은 고칠 수 없으니 꺼낸 뒤 남은 것을 다시 넣는다(기한이 같아 다시 맨 앞에 온다)            if (take < c) heap.push({e, c - take});        }        out += std::to_string(got) + " " + std::to_string(total) + "\n";    }    std::cout << out;    return 0;}
Cmain.c · 74 行
#include <stdio.h>#include <string.h> #define MAXQ 10000 /* 최소 힙: 칸마다 (기한, 남은 개수). heap_e[0]이 기한이 가장 이르다 */static long long heap_e[MAXQ], heap_c[MAXQ];static int heap_size = 0; static void heap_swap(int i, int j) {    long long t = heap_e[i];    heap_e[i] = heap_e[j];    heap_e[j] = t;    t = heap_c[i];    heap_c[i] = heap_c[j];    heap_c[j] = t;} static void heap_push(long long e, long long c) {    int i = heap_size++;    heap_e[i] = e;    heap_c[i] = c;    while (i > 0 && heap_e[(i - 1) / 2] > heap_e[i]) {        heap_swap((i - 1) / 2, i);        i = (i - 1) / 2;    }} static void heap_pop(void) {    heap_size--;    heap_e[0] = heap_e[heap_size];    heap_c[0] = heap_c[heap_size];    int i = 0;    for (;;) {        int l = 2 * i + 1, r = l + 1, small = i;        if (l < heap_size && heap_e[l] < heap_e[small]) small = l;        if (r < heap_size && heap_e[r] < heap_e[small]) small = r;        if (small == i) break;        heap_swap(small, i);        i = small;    }} int main(void) {    int q;    if (scanf("%d", &q) != 1) return 0;    for (int i = 0; i < q; i++) {        char kind[8];        long long t;        scanf("%7s %lld", kind, &t);        if (strcmp(kind, "bake") == 0) {            long long e, c;            scanf("%lld %lld", &e, &c);            heap_push(e, c);            continue;        }        long long need, got = 0, total = 0; /* total: 받은 빵마다 (기한 - t)를 더한 값, 10^15까지 */        scanf("%lld", &need);        while (need > 0 && heap_size > 0) {            if (heap_e[0] < t) { /* 기한이 지난 묶음은 버린다(같은 시각이면 아직 팔 수 있다) */                heap_pop();                continue;            }            long long take = heap_c[0] < need ? heap_c[0] : need;            got += take;            total += take * (heap_e[0] - t);            need -= take;            if (take == heap_c[0]) heap_pop();            else heap_c[0] -= take; /* 개수만 줄인다: 기한은 그대로라 힙 차례가 깨지지 않는다 */        }        printf("%lld %lld\n", got, total);    }    return 0;}
Craykonmain.cray · 91 行
// 최소 힙(목록 heap): 칸마다 [기한, 남은 개수]. heap[0]이 기한이 가장 이른 묶음이고, i의 자식은 2i+1, 2i+2힙넣기(heap, x){    heap.追加(x);    i = heap.個数 - 1;    間 (i > 0)    {        부모 = 数学.商(i - 1, 2);        もし (heap[부모][0] <= heap[i][0])        {            抜ける;        }        t = heap[부모];        heap[부모] = heap[i];        heap[i] = t;        i = 부모;    }} 힙빼기(heap){    heap[0] = heap[heap.個数 - 1];    heap.位置で取り除く(heap.個数 - 1);    i = 0;    間 (真)    {        l = 2 * i + 1;        r = l + 1;        small = i;        もし (l < heap.個数 && heap[l][0] < heap[small][0])        {            small = l;        }        もし (r < heap.個数 && heap[r][0] < heap[small][0])        {            small = r;        }        もし (small == i)        {            抜ける;        }        t = heap[small];        heap[small] = heap[i];        heap[i] = t;        i = small;    }} q = コンソール.行読み().空白除去().数に();heap = [];繰り返す (n = 0; n < q; n++){    w = コンソール.行読み().空白除去().分ける(" ");    t = w[1].数に();    もし (w[0] == "bake")    {        힙넣기(heap, [w[2].数に(), w[3].数に()]);    }    でなければ    {        need = w[2].数に();        got = 0;        total = 0; // 받은 빵마다 (기한 - t)를 더한 값        間 (need > 0 && heap.個数 > 0)        {            batch = heap[0];            e = batch[0];            c = batch[1];            もし (e < t)            {                힙빼기(heap);            }            でなければ            {                take = 数学.小さい方(c, need);                got += take;                total += take * (e - t);                need -= take;                もし (take == c)                {                    힙빼기(heap);                }                でなければ                {                    batch[1] = c - take; // 개수만 줄인다: 기한은 그대로라 힙 차례가 깨지지 않는다                }            }        }        コンソール.書く(got + " " + total);    }}