とても教育的かつ典型的な貪欲法の問題ですね。
問題概要
二次元平面上に、赤い点と青い点が 個ずつあります。 個目の赤い点の座標は であり、 個目の青い点の座標は です。
赤い点と青い点は、 座標と 座標がともに赤い点よりも青い点の方が大きいとき、仲良しペアになれます。ただし,1 つの点が複数のペアに所属することはできません。
あなたは最大で何個の仲良しペアを作ることができますか?
制約
- はそれぞれ互いに相異なる
- はそれぞれ互いに相異なる
順序を定めて考える
このような問題を考察するときには、 座標の小さい順に考えるなど、なんらかの順序を定めて考えることが重要だと思います。
さて、ここでは、青い点たちを 座標が小さい順に並べてあげて、それぞれの青い点に対して順番に、どの赤い点をペアにしていくかを考えることにしましょう1。このときポイントとなるのは、 座標を大きくしていくたびに、青い点とマッチングされうる赤い点の選択肢が徐々に広がっていくことです。
直感的な議論
さて、基本的には「赤い点のうち、 座標が大きいものは売れ残りやすい」という構造になっていることに注意しましょう。 座標が大きい赤い点は、それよりも 座標が大きい青い点しか手に負えないのです。
このことを踏まえた上で、 座標が小さい順に青い点を見ていきましょう。結論から述べると、
- まだ残っている赤い点のうち、
- その青い点の左側 ( 座標が小さい側) にあって、
- 座標が最大の点
を選ぶようにしておくとよいです。その理由を考えてみましょう。とりあえず、今考えている青い点を として、その 座標を としておきます。
さて、その青い点 の左側にあるまだ残っている赤い点たちの中に、 座標が よりも小さいものが複数あったとします。これらの赤い点たちの集合を としましょう。 に含まれる赤い点たちのうち、将来的に最も売れ残るリスクが高いものは「 座標が最大である赤い点」です。よって、最も売れ残るリスクの高い哀れな赤い点を拾ってあげるのが良いということになります2。
まとめると、次のように解けます。
- 青い点たちを 座標が小さい順にソートして、その順に処理していく
- 各青い点に対して、以下の条件を満たす赤い点が存在しないときはスキップして、存在するときは 座標が最大のものとペアにしていく
- まだどの青い点ともペアにされずに残っている
- その青い点よりも 座標も 座標も小さい
計算量は、「各青い点に対して条件を満たす赤い点を探索する」という単純な実装で となります。今回の制約は であるため、十分間に合います。
「交換しても悪化しない」による証明
上記の直感的な説明は、貪欲法の証明でよくある「交換しても悪化しないことを示す」という論法で示せます。
まず青い点を 座標が小さい順に並べたとします。そのうちの最初の点 ( 座標が最小の点) について考えます。
点 よりも 座標がともに小さい赤い点が存在しないならば がペアを組むことはできませんし、1 個しかないならばそれと がペアを組んで損することは絶対にありません (その 1 個の点が他の青い点と組んだとしても、点 B に組み直して解が悪化することはありません)。
そのような赤い点が複数あったとして、そのうちの 座標が最大の点を とします。このとき、他の赤い点 と点 とがペアを組むような解があったとしましょう。
このとき、その解を悪化させることなく、 がペアになる解へと変形できることを示します。まずその解において点 が他のどの青い点ともペアを組んでいない場合は、 の相方を から へと組み替えれば良いです。
点 が他の点 とペアを組んでいるとします。このとき実はペア を解消してペア に組み替えることができます。なぜなら、まず
,
が成り立っていますし、
, ( の最大性より)
が成り立つからです。
以上から、点 に対して 座標が最大な点 をペアにするような最適解が存在することが言えました。よって二次元平面上から 2 点 を除去してよく、残った点に対して同様の手続きを進めていくことができます。
コード
#include <iostream> #include <vector> #include <algorithm> using namespace std; // 二次元座標を定義 using Point = pair<int,int>; int main() { // 入力 int N; cin >> N; vector<Point> red(N), blue(N); for (int i = 0; i < N; ++i) cin >> red[i].first >> red[i].second; for (int i = 0; i < N; ++i) cin >> blue[i].first >> blue[i].second; // 青い点を x 座標が小さい順にソートする (デフォルトで第一引数の辞書順比較) sort(blue.begin(), blue.end()); // 各赤い点が使用済みかどうか vector<bool> used(N, false); // 青い点を順番に見ていく int res = 0; for (int i = 0; i < N; ++i) { // 使用済みでなく、y 座標最大の赤い点を探す int maxy = -1, maxid = -1; for (int j = 0; j < N; ++j) { // 使用済みの赤い点は不可 if (used[j]) continue; // x 座標, y 座標がより大きい赤い点は不可 if (red[j].first >= blue[i].first) continue; if (red[j].second >= blue[i].second) continue; // 最大値を更新 if (maxy < red[j].second) { maxy = red[j].second; maxid = j; } } // 存在しない場合はスキップ if (maxid == -1) continue; // ペアリングする ++res; used[maxid] = true; } cout << res << endl; }