公式

G - Push Simultaneously 解説 by MMNMM


高橋くんたちがそれぞれ最終的に向かうボタンを決めたとき、それぞれの高橋くんの最適な動きは「スタートしたらまっすぐ目的のボタンへ向かい、着いたらそこにとどまる」ことです。

よって、答えはいずれかの高橋くんといずれかのボタンとの距離と等しいです。

ここから、次の判定問題を十分高速に解くことができれば、たかだか \(N ^ 2\) 通りの答えの候補に対して二分探索を行うことなどでこの問題を解くことができます。

正実数 \(t\) が与えられる。 高橋くんからボタンへの全単射をひとつ取ることで、どの高橋くんに対しても、その高橋くんと対応するボタンとの距離を \(t\) 以下にすることができるか?

これは、距離が \(t\) 以下の高橋くんとボタンの間に辺を張ることで得られる二部グラフの、最大マッチングのサイズが \(N\) と等しいことと同値です。

二部グラフの最大マッチングは Hopcroft-Karp 法や Dinic 法で \(O(E\sqrt V)\) 時間で得ることができるので、それらを利用することでこの問題を \(O(N ^ {2.5}\log N)\) 時間で解くことができました。

実装例は以下のようになります。

誤差については、hypot 関数の引数に座標の差を与えるところでたかだか \(0.5\operatorname{ulp}\) 、hypot 関数の計算でたかだか \(1\operatorname{ulp}\) の誤差(参考 : Known Maximum Errors in Math Functions)が生じて、他の部分では誤差が生じないため、十分な精度の解が得られます。

#include <iostream>
#include <vector>
#include <cmath>
#include <algorithm>
#include <ranges>
#include <iomanip>
#include <atcoder/maxflow>

int main() {
    using namespace std;
    
    unsigned N;
    cin >> N;

    vector<pair<unsigned long, unsigned long>> start(N), goal(N);
    for (auto&& [x, y] : start) cin >> x >> y;
    for (auto&& [x, y] : goal) cin >> x >> y;

    vector<tuple<double, unsigned, unsigned>> edges;
    for (unsigned i = 0; const auto& [sx, sy] : start) {
        for (unsigned j = 0; const auto& [gx, gy] : goal) {
            // ((sx, sy) と (gx, gy) との距離, 高橋くんの番号, ボタンの番号)
            edges.emplace_back(hypot(max(sx, gx) - min(sx, gx), max(sy, gy) - min(sy, gy)), i, j);
            ++j;
        }
        ++i;
    }

    // 距離 X 以下の辺を用いてマッチングができるか判定
    const auto judge = [N, &edges](const auto& X) {
        atcoder::mf_graph<long long> graph(1 + N + N + 1);
        for (unsigned i = 0; i < N; ++i) graph.add_edge(0, 1 + i, 1);
        for (unsigned i = 0; i < N; ++i) graph.add_edge(1 + N + i, 1 + N + N + 0, 1);

        // 距離 X 以下の辺だけをグラフに追加する
        for (const auto& [w, u, v] : edges | views::filter([X](const auto& e) { return get<0>(e) <= X; }))
            graph.add_edge(1 + u, 1 + N + v, 1);

        // マッチングの大きさが N かどうか判定
        return graph.flow(0, 1 + N + N + 0) < N;
    };

    // 長さでソートして二分探索
    ranges::sort(edges);
    cout << setprecision(7) << *ranges::partition_point(edges | views::keys, judge) << endl;
    return 0;
}

投稿日時:
最終更新: