D - コンテストの準備 / Contest Preparation 解説 by admin
Claude Opus 5 (High)概要
\(N\) 個の問題から \(K\) 問を選び、配点の合計を \(P\) 以上にしつつ、難易度の最大値を最小化する問題です。「難易度でソートして、先頭から順に候補を増やしながら配点上位 \(K\) 個の和を管理する」ことで解けます。
考察
気づき1:答えを固定して考える
「難易度の最大値の最小値」を求める問題は、答えを \(X\) に固定して判定問題に変換するのが定石です。
「難易度の最大値が \(X\) 以下になるように \(K\) 問選んで、配点合計 \(P\) 以上にできるか?」
この判定は簡単です。使える問題は \(B_i \le X\) を満たすものだけなので、その中から配点 \(A_i\) が大きい順に \(K\) 個取ればよく、その合計が \(P\) 以上かどうかを見れば十分です(合計を最大化したいので貪欲に大きい方から取るのが最適)。もちろん、\(B_i \le X\) を満たす問題が \(K\) 個未満なら不可能です。
気づき2:単調性がある
\(X\) を大きくすると使える問題は増えるだけなので、「\(X\) で達成可能」なら「\(X' > X\) でも達成可能」です。つまり判定結果は \(X\) に関して単調で、二分探索が使えます。
ただし、\(X\) の候補は実質 \(B_i\) の値のみなので、\(B\) でソートしてから「先頭 \(i\) 個までを使える」という形で \(i\) を動かすと考えれば、二分探索を書かずに一度の走査で済みます。こちらの方が実装が楽です。
素朴な方法の問題点
各 \(i\) について「先頭 \(i\) 個の中の \(A\) の上位 \(K\) 個の和」を毎回ソートして求めると、\(O(N K \log N)\) や \(O(N^2 \log N)\) になって \(N = 10^5\) では間に合いません。
そこで、\(i\) を \(1\) ずつ増やすとき、上位 \(K\) 個の集合はほとんど変化しないことを使います。新しい要素 \(A_i\) が入るかどうかは「今持っている \(K\) 個の中の最小値」と比べればわかります。これは 最小値を取り出せる優先度付きキュー(min-heap) でちょうど管理できます。
アルゴリズム
- 各問題を \((B_i, A_i)\) のペアにして、\(B\) の昇順にソートする。
- min-heap
pq(現在選んでいる \(A\) たち)と、その総和sumを用意する。 - \(i = 0, 1, \dots, N-1\) の順に見て、
pqのサイズが \(K\) 未満なら、\(A_i\) を追加してsum += A_i。- サイズが \(K\) でかつ \(A_i > \)
pqの最小値なら、最小値を取り除いて \(A_i\) を入れる(sumも更新)。これで「先頭 \(i+1\) 個の中の \(A\) の上位 \(K\) 個」が常に保たれる。 - その後、
pqのサイズが \(K\) でかつsum\(\ge P\) なら、答えは \(B_i\)(ソート後の \(i\) 番目の \(B\))なので出力して終了。
- 最後まで達成できなければ \(-1\) を出力。
なぜ最初に条件を満たした時点の \(B_i\) が答えになるのか? \(i-1\) 番目までで達成できなかったということは、先頭 \(i\) 個(\(0\) ~ \(i-1\))からどう \(K\) 問選んでも \(P\) 未満ということです(上位 \(K\) 個の和が最大値なので)。よって最適解は必ず \(i\) 番目の問題を含み、選ばれた問題の難易度の最大値は \(B_i\) になります。\(B\) が同じ値の問題が複数あっても、出力される値は同じなので問題ありません。
具体例
\(N=4, K=2, P=10\)、\((A,B) = (3,5), (8,2), (4,9), (6,3)\) の場合。
\(B\) 昇順に並べると \((8,2), (6,3), (3,5), (4,9)\)。
- \(i=0\): pq \(=\{8\}\), sum \(=8\)(サイズ \(1 < K\))
- \(i=1\): pq \(=\{8,6\}\), sum \(=14 \ge 10\) かつサイズ \(=2=K\) → 答え \(B=3\)
実際に配点 \(8\) と \(6\) の問題を選ぶと合計 \(14 \ge 10\) で、難易度の最大値は \(3\) です。
計算量
- 時間計算量: \(O(N \log N)\)(ソートとヒープ操作がそれぞれ \(O(N\log N)\))
- 空間計算量: \(O(N)\)
実装のポイント
オーバーフロー:\(P \le 10^{14}\)、配点の合計は最大 \(10^5 \times 10^9 = 10^{14}\) なので、
intでは溢れます。合計や \(P\) は 64bit 整数(long long)で扱いましょう。ペアのソート:
pair<long long,long long>に \((B, A)\) の順で入れておけば、sortで \(B\) 昇順に並びます(\(A\) を第一要素にしない点に注意)。min-heap の書き方:C++ の
priority_queueはデフォルトが最大値取り出しなので、priority_queue<long long, vector<long long>, greater<long long>>と書いて最小値取り出しにします。\(K\) 個未満の状態での判定禁止:ちょうど \(K\) 問選ぶ必要があるので、
pq.size() == Kの条件を必ず入れます。二分探索で書く場合は、判定の中で「\(B_i \le X\) の問題の \(A\) を降順ソートして先頭 \(K\) 個の和」を求めればよく、\(O(N \log^2 N)\) 程度で通ります。ただし上記の走査の方がシンプルです。
ソースコード
#include <bits/stdc++.h>
using namespace std;
int main(){
int N; long long K, P;
scanf("%d %lld %lld", &N, &K, &P);
vector<pair<long long,long long>> v(N); // (B, A)
for(int i=0;i<N;i++){
long long a,b;
scanf("%lld %lld",&a,&b);
v[i] = {b,a};
}
sort(v.begin(), v.end());
priority_queue<long long, vector<long long>, greater<long long>> pq;
long long sum = 0;
for(int i=0;i<N;i++){
long long a = v[i].second;
if((long long)pq.size() < K){
pq.push(a);
sum += a;
} else if(!pq.empty() && a > pq.top()){
sum -= pq.top();
pq.pop();
pq.push(a);
sum += a;
}
if((long long)pq.size() == K && sum >= P){
printf("%lld\n", v[i].first);
return 0;
}
}
printf("-1\n");
return 0;
}
この解説は claude5opus-high によって生成されました。