D - 果物狩りフェスティバル / Fruit Picking Festival 解説 by admin
gemini-3.5-flash-thinking概要
おいしさと高さが異なる \(N\) 個の果物と、使える高さが制限された \(M\) 回の収穫チャンス(脚立)があります。各脚立は1回しか使えず、その高さ以下の位置にある果物を1つ収穫できます。このとき、収穫できる果物のおいしさの合計を最大化する問題です。
考察
この問題は、「どのおいしい果物を、どの脚立を使って回収するか」を最適に決める問題です。価値を最大化したいので、貪欲法(Greedy)の適用を考えます。
1. どの果物から優先して決めるべきか?
得られるおいしさの合計を最大化したいので、当然「おいしさ(\(V_i\))が大きい果物」から優先的に確保したいです。したがって、果物をおいしさの降順(大きい順)に並べ替えて、上から順に収穫できるか判定していくのが自然です。
2. 条件を満たす脚立が複数あるとき、どれを使うべきか?
ある果物(高さ \(D_i\))を収穫することを考えます。この果物を収穫するために使える脚立は、高さが \(D_i\) 以上(\(L_j \geq D_i\))のものすべてです。 もし候補となる脚立が複数ある場合、「条件を満たす脚立のうち、最も高さが低い(\(D_i\) に最も近い)もの」を使うのが最適になります。
なぜなら、必要以上に高い脚立をここで使ってしまうと、後から出てくる「もっと高い位置にある(しかしおいしい)果物」を収穫するための貴重な高い脚立が失われてしまうからです。低い脚立を優先して消費することで、高い脚立を未来のために温存することができます。
3. 高速に処理するためのデータ構造
「ある値 \(D_i\) 以上の最小の要素を検索し、それを削除する」という操作を高速に行う必要があります。 配列やリストを使うと、検索や削除に \(O(M)\) の時間がかかってしまい、全体で \(O(N \times M)\) となり実行時間制限に間に合いません(TLE)。
そこで、要素の追加・二分探索・削除をすべて \(O(\log M)\) で行える平衡二分探索木(C++の std::multiset)を使用します。
アルゴリズム
- 果物の情報を、おいしさ \(V_i\) の降順にソートします。
- 脚立の高さ \(L_j\) をすべて
std::multisetに追加します。 - おいしい果物から順に、以下の処理を行います。
multisetの中から、果物の高さ \(D_i\) 以上となる最小の脚立を二分探索(lower_bound)で探します。- 条件を満たす脚立が見つかった場合:
- その果物を収穫することにし、おいしさ \(V_i\) を合計に加算します。
- 使用した脚立を
multisetから削除します。
- 見つからなかった場合:
- その果物は諦めて次の果物に進みます。
- 最終的なおいしさの合計を出力します。
計算量
時間計算量: \(O(N \log N + M \log M)\)
- 果物のソートに \(O(N \log N)\) かかります。
- \(M\) 個の脚立を
multisetに挿入するのに \(O(M \log M)\) かかります。 - 各果物について、
multisetでの二分探索と削除(最大 \(M\) 回)を行うのに、1回あたり \(O(\log M)\) かかるため、全体で \(O(N \log M)\) です。 - 制約 \(N, M \leq 2 \times 10^5\) において、全体の計算量は約 \(10^7\) 回の演算となり、実行時間制限に余裕で間に合います。
空間計算量: \(O(N + M)\)
- 果物の情報を格納する配列に \(O(N)\)、脚立の情報を管理する
multisetに \(O(M)\) のメモリを使用します。
- 果物の情報を格納する配列に \(O(N)\)、脚立の情報を管理する
実装のポイント
std::multisetの検索方法:multisetで二分探索を行う際は、必ずメンバー関数であるL.lower_bound(val)を使用してください。汎用関数であるstd::lower_bound(L.begin(), L.end(), val)を使用すると、イテレータの性質上 \(O(M)\) の時間がかかってしまい TLE になります。重複要素の削除:
multisetから要素を削除する際、L.erase(val)のように値を直接指定すると、その値を持つすべての要素が削除されてしまいます。今回は使った脚立を「1つだけ」削除したいため、必ずL.erase(it)のように検索で見つかったイテレータを渡して削除するようにします。ソースコード
#include <iostream>
#include <vector>
#include <algorithm>
#include <set>
using namespace std;
struct Fruit {
long long d;
long long v;
// おいしさの降順でソートするための比較演算子
bool operator<(const Fruit& other) const {
return v > other.v;
}
};
int main() {
// 入出力の高速化
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
if (!(cin >> n >> m)) return 0;
vector<Fruit> fruits(n);
for (int i = 0; i < n; ++i) {
cin >> fruits[i].d >> fruits[i].v;
}
multiset<long long> L;
for (int j = 0; j < m; ++j) {
long long l;
cin >> l;
L.insert(l);
}
// おいしさの降順にソート
sort(fruits.begin(), fruits.end());
long long total_v = 0;
for (const auto& fruit : fruits) {
// 高さ fruit.d 以上の脚立のうち、最も高さが低いものを選択する
auto it = L.lower_bound(fruit.d);
if (it != L.end()) {
total_v += fruit.v;
L.erase(it); // 使用した脚立を削除
}
}
cout << total_v << "\n";
return 0;
}
この解説は gemini-3.5-flash-thinking によって生成されました。
投稿日時:
最終更新: