D - コンテストの準備 / Contest Preparation Editorial by admin
Claude Opus 5 (High)概要
\(N\) 問の候補問題から \(K\) 問を選び、配点の合計を \(P\) 以上にしつつ、選んだ問題の難易度の最大値を最小化する問題です。難易度でソートしてから「配点上位 \(K\) 問の和」を優先度付きキュー(最小ヒープ)で管理することで解けます。
考察
気づき 1: 「最大値の最小化」は閾値を決めると簡単になる
「難易度の最大値が \(X\) 以下」という条件で \(K\) 問選ぶことを考えます。このとき使える問題は \(B_i \le X\) を満たすものだけです。その中から \(K\) 問選んで配点の合計を最大にしたいなら、配点 \(A_i\) が大きいものを上から \(K\) 個取るのが最適です(貪欲)。
つまり、
\(B_i \le X\) である問題の中で、\(A_i\) の上位 \(K\) 個の和が \(P\) 以上かどうか
を判定できれば、その \(X\) が答えの候補になるかが分かります。
気づき 2: 答えは必ずいずれかの \(B_i\) に一致する
最大値の最小値なので、答えは実際に選んだ問題のうち難易度が最大のもの、すなわち入力に現れる \(B_i\) のどれかです。よって、\(X\) として \(B_1, \dots, B_N\) だけを試せば十分です。
気づき 3: 単調性があるので二分探索も可能
\(X\) を大きくすると使える問題が増えるだけなので、「上位 \(K\) 個の和」は単調非減少です。よって答えに関して単調性があり、\(X\) について二分探索できます。ただし各判定でソートし直すと \(O(N \log^2 N)\) 程度になり、少し無駄です。
素朴なアプローチの問題点
- 全ての \(K\) 問の組み合わせを試す: \(\binom{N}{K}\) 通りで指数的、\(N \le 10^5\) では絶望的。
- 各 \(B_i\) を閾値として毎回ソート・上位 \(K\) 個を数え直す: 判定 1 回に \(O(N \log N)\) かかり、全体 \(O(N^2 \log N)\) で TLE。
解決策: 難易度昇順に見ていき、ヒープで上位 \(K\) 個を維持
問題を \(B\) の昇順に並べて 1 つずつ「使える集合」に追加していきます。閾値を \(B\) の小さい方から順に広げていくイメージです。このとき、
- 現在までに見た問題の中の \(A\) の上位 \(K\) 個の和 \(s\)
を、最小ヒープ(サイズ \(K\)、中身は選ばれている \(A\) の値)で \(O(\log K)\) ずつ更新できます。\(s \ge P\) になった瞬間の \(B\) が答えです(それ以降に \(B\) を大きくしても最大値は増えるだけなので、最初に条件を満たした時点が最小)。
アルゴリズム
- \((B_i, A_i)\) のペアを作り、\(B\) の昇順にソートする。
- 空の最小ヒープ
heapと合計 \(s = 0\) を用意する。 - ソート順に \((b, a)\) を見ていく:
- ヒープのサイズが \(K\) 未満なら、\(a\) を push し \(s \mathrel{+}= a\)。
- サイズが \(K\) になり、かつ \(s \ge P\) なら答えは \(b\) で終了。
- サイズが \(K\) のとき、\(a\) がヒープの最小値
heap[0]より大きければ、最小値を捨てて \(a\) を入れる(heappushpop)。\(s\) をその差分だけ更新する。- \(s \ge P\) なら答えは \(b\) で終了。
- \(a \le\)
heap[0]のときは、\(a\) を入れると合計が減るだけなので何もしない(ただし、この問題を「使える」ようにしたこと自体は最大値に影響しないので無視して良い)。
- ヒープのサイズが \(K\) 未満なら、\(a\) を push し \(s \mathrel{+}= a\)。
- 最後まで \(s \ge P\) にならなければ \(-1\) を出力。
具体例
\(N = 4, K = 2, P = 10\)、\((A, B) = (3, 1), (8, 5), (6, 3), (2, 2)\) とします。
\(B\) 昇順に並べると \((B, A) = (1, 3), (2, 2), (3, 6), (5, 8)\)。
| 処理 | ヒープ(\(A\) の上位 2 個) | \(s\) | 判定 |
|---|---|---|---|
| \((1,3)\) | \(\{3\}\) | 3 | サイズ \(<K\) |
| \((2,2)\) | \(\{2,3\}\) | 5 | \(5 < 10\) |
| \((3,6)\) | \(\{3,6\}\)(2 を捨てる) | 9 | \(9 < 10\) |
| \((5,8)\) | \(\{6,8\}\)(3 を捨てる) | 14 | \(14 \ge 10\) → 答え \(5\) |
答えは \(5\) です。
計算量
- 時間計算量: \(O(N \log N)\)(ソートが \(O(N\log N)\)、ヒープ操作が全体で \(O(N \log K)\))
- 空間計算量: \(O(N)\)(入力の保持。ヒープ自体は \(O(K)\))
実装のポイント
ソートキーの順序: タプルを \((B_i, A_i)\) の順で持つと、そのままソートするだけで \(B\) 昇順になります(コードでは
itemsを(B, A)の順に作っています)。答えを返すタイミング: サイズが \(K\) に達した後、\(s\) を更新するたびに \(s \ge P\) を判定します。「サイズ \(K\) 未満」の段階では \(K\) 問選べていないので判定してはいけません(コードでは
len(heap) == k and s >= pとしてサイズ到達時のみ判定)。同じ \(B\) が複数ある場合: \(B\) 昇順に処理しているので、同じ \(B\) の問題群を途中まで入れた時点で条件を満たしても、答えはその \(B\) で正しく、問題ありません。
合計の更新は差分で:
heappushpopで押し出された値oldを使いs += a - oldとすると \(O(1)\) で合計を保てます。毎回ヒープ全体を足し直すと \(O(K)\) かかり TLE の恐れがあります。\(a \le\) heap[0] のスキップ: これを忘れて無条件に
heappushpopすると合計が減ってしまい WA になります。オーバーフロー: \(P \le 10^{14}\)、\(A_i \le 10^9\) で合計は最大 \(10^{14}\) 程度。Python では多倍長整数なので問題ありませんが、C++ では
long longを使う必要があります。入力の高速化: \(N \le 10^5\) なので
sys.stdin.buffer.read()でまとめて読むと安全です。ソースコード
import sys, heapq
def main():
data = sys.stdin.buffer.read().split()
n = int(data[0]); k = int(data[1]); p = int(data[2])
items = [(int(data[3 + 2*i + 1]), int(data[3 + 2*i])) for i in range(n)]
items.sort()
heap = []
push = heapq.heappush
pushpop = heapq.heappushpop
s = 0
for i in range(n):
b, a = items[i]
if len(heap) < k:
push(heap, a)
s += a
if len(heap) == k and s >= p:
print(b)
return
else:
if a > heap[0]:
old = pushpop(heap, a)
s += a - old
if s >= p:
print(b)
return
print(-1)
main()
この解説は claude5opus-high によって生成されました。