Official

E - 宇宙ステーションへの移動 / Traveling to the Space Station Editorial by harurun4635


「距離が \(D\) 以下のデブリ」同士を辺で結んだグラフを構築すれば、結局は 01-BFS をすれば良いです。

このグラフは簡単に \(O(N^2)\) で構築できますが TLE となるでしょう。


以下、単に「距離」というと座標平面上かグラフ上かわかりにくいため、(濫用ですが)グラフ上の距離に関しては \(\text{dist}\) と書くことにします。

\(O(N \log N)\) で解く方法がいくつかあります。

改善のための簡単な気持ちとしては、近くの点は必ず結ぶのでわざわざ「距離は \(D\) 以下ですか?」という計算したくはないですし、遠くの点はどうせ結ばないのでこれも計算したくはないです。


解法 1 : グリッドに分割する

小さい定数 \(C\) を決め、座標平面全体を \(\dfrac{D}{C}\times\dfrac{D}{C}\) の大きさのグリッドに分割します。(各領域を以下「マス」と呼びます)本解説では、以下 \(C = 2\) とします。(他の小さい定数でも同様の議論が可能です)

同じマスに含まれる \(2\) 点間の距離は最大でも\(\dfrac{D}{\sqrt2}\) ですから、同じマスに存在する頂点は必ず辺で結ばれます。また、あるマスの内部からは、高々周囲の \(25\) マスしか辺が張られないことも計算からわかります。(それ以上は、必ず \(D\) より遠くなります)

そして、マスの内部のある頂点の \(\text{dist}\) が \(k\) と決まれば、その瞬間、内部の他の頂点の \(\text{dist}\) が \(k, k+1\) のどちらかと決まることに注意してください。


解法 1 - 1 : KD-Tree を使う

ライブラリに押し付けることでシンプルに書けますが、最悪計算量の保証がない解法です。

\(\text{dist}\) がちょうど \(k\) である頂点の集合 \(V_k\) を考えます。\(V_{k+1}\) を定めるには以下のようにします。

  • KD-tree を \(V_k\) を頂点集合として構築する
  • \(V_k\) に含まれる頂点の周囲 \(25\) マスを列挙する
  • 周囲マスに含まれるすべての頂点 \(p\) について \(q \in V_k\) の最近点距離を調べる
  • もし \(p, q\) の距離が \(D\) 以下なら \(p \in V_{k+1}\) である

\(1\) つ目の構築は \(O(|V_k| \log |V_k|)\) で、 \(3\) マス目のクエリ回数も各頂点について高々 \(50\) 回です。

ただし、「KD-tree の最近点距離クエリ」は最悪 \(O(N)\) となる配置が存在します。回転・平行移動などの乱数を加えることで、実用上は高速に動くことが多く、もっとも通されやすい解法だとも考えています。


解法 1 - 2 円弧包絡線をもちいる

理論的な計算量保証があり、かつ定数倍も良好な方針です。ただ、ライブラリ等に押し付けられる部分が少ないです。

ある \(V_k\) の点を含むマス \(A\) と、周囲マス \(B\) を考えます。適切に反転・軸の入れ替えをして「 \(A\) より \(B\) が右にある」ようにしておきます。

\(V_k\) の各点と辺をむすべる範囲は、半径 \(D\) の円の内側です。そこで「各高さ \(v\) についてどの円が一番右まで届くか」という包絡線を求めることを考えます。中心を高さの順にならべると、その包絡線上に現れる円もおなじ順に現れます。(微分を比べれば簡単に示せます)

よって、 CHT と同様のアルゴリズムをもちいることで、包絡線を「 \(A\) に含まれる頂点の線形時間」で構築することができます。包絡線の左側にあるかどうかも「 \(B\) に含まれる頂点の線形時間」で判定することができるため、ソートがボトルネックとなり、 \(O(N \log N)\) です。


解法 1 - 3 : Voronoi 図を使う

最近点距離クエリは Voronoi 図をもちいることで、頂点集合のサイズを \(S\) として、構築 \(O(S \log S)\) クエリ \(O(\log S)\) で処理できることが知られています。よって、これにより \(O(N \log N)\) も保証されます。

しかし、この実装は KD-Tree に比べると、とても難解かつ重実装であることも同時に知られています。また、理論的な計算量保証はありますが、実際には定数倍がかなり重く TLE となる可能性も否めません。


以下の解法は(解説作成者の体力が限界であるため)紹介するに留めることにします。

  • 解法 2 : Delaunay 三角形分割をする方法

    グリッドで分けて探索領域を制限する代わりに、Delaunay 三角形分割によって、探索領域を制限します。

    三角形分割に使われた辺のみが候補辺になるわけではないですが、この辺により \(V_k\) が連結であることが示されるため、制限することが可能です。同様に最近点クエリも必要になります。

  • 解法 3 : 動的最近点探索をする方法 \(O(N \ \text{poly} (\log N))\)

    もっともふつうの BFS に近い解法です。各頂点について「無駄無く \(\text{dist}\) が \(1\) 大きくなる辺だけを見る」ということができれば、 \(O(N)\) で BFS ができます。

    そのためには、

    • はじめにすべての頂点を候補とした「最近点クエリを処理するデータ構造」をつくる
    • すでに訪問した頂点について、その候補から消す

    といった、動的削除が可能なデータ構造があればよいです。これを実現するデータ構造が存在するようです。(これが正しいことは確認したつもりですが、間違いがあれば削除します)


実装例

解法 1-2 の実装例です。Chat GPT による実装です。

import sys
from collections import defaultdict
from math import sqrt

input = sys.stdin.readline


def solve():
    N, W, D = map(int, input().split())

    X = [0] * (N + 1)
    Y = [0] * (N + 1)

    for I in range(N):
        X[I], Y[I] = map(int, input().split())

    S = N
    D2 = D * D

    CELLS = {}
    BELONG = [None] * (N + 1)

    for I in range(N):
        C = (2 * X[I] // D, 2 * Y[I] // D)
        BELONG[I] = C
        CELLS.setdefault(C, []).append(I)

    BELONG[S] = (0, 0)

    BY_X = {
        C: sorted(A, key=lambda I: (X[I], Y[I]))
        for C, A in CELLS.items()
    }
    BY_Y = {
        C: sorted(A, key=lambda I: (Y[I], X[I]))
        for C, A in CELLS.items()
    }

    def UV(P, HORIZONTAL, SIGN):
        if HORIZONTAL:
            return SIGN * X[P], Y[P]
        return SIGN * Y[P], X[P]

    def BUILD(SRC, HORIZONTAL, SIGN):
        A = []

        for P in SRC:
            U, V = UV(P, HORIZONTAL, SIGN)

            if A and UV(A[-1], HORIZONTAL, SIGN)[1] == V:
                if UV(A[-1], HORIZONTAL, SIGN)[0] < U:
                    A[-1] = P
            else:
                A.append(P)

        if not A:
            return [], [], 0

        BASE = UV(A[0], HORIZONTAL, SIGN)[1]
        HULL = []
        START = []

        def SWITCH(P, Q):
            UP, VP = UV(P, HORIZONTAL, SIGN)
            UQ, VQ = UV(Q, HORIZONTAL, SIGN)

            DU = (UQ - UP) / D
            DV = (VQ - VP) / D

            VP = (VP - BASE) / D
            VQ = (VQ - BASE) / D

            Z = sqrt(max(0.0, DV * (2.0 - DV)))

            if DU >= Z:
                return VQ - 1.0

            if DU <= -Z:
                return VP + 1.0

            R2 = DU * DU + DV * DV
            T = sqrt((4.0 - R2) / R2)

            return (VP + VQ - DU * T) * 0.5

        for P in A:
            if not HULL:
                HULL.append(P)
                START.append(float("-inf"))
                continue

            C = SWITCH(HULL[-1], P)

            while len(HULL) >= 2 and C <= START[-1]:
                HULL.pop()
                START.pop()
                C = SWITCH(HULL[-1], P)

            HULL.append(P)
            START.append(max(C, START[-1]))

        return HULL, START, BASE

    DIST = [-1] * (N + 1)
    DIST[S] = 0

    FRONT = [S]
    LAYER = 0

    while FRONT:
        GROUP = defaultdict(list)

        for P in FRONT:
            GROUP[BELONG[P]].append(P)

        NEXT = []

        for C, SRC in GROUP.items():
            GX, GY = C

            if SRC == [S]:
                SRC_X = SRC_Y = SRC
            else:
                SRC_X = [P for P in BY_X[C] if DIST[P] == LAYER]
                SRC_Y = [P for P in BY_Y[C] if DIST[P] == LAYER]

            for Q in CELLS.get(C, ()):
                if DIST[Q] == -1:
                    DIST[Q] = LAYER + 1
                    NEXT.append(Q)

            CACHE = {}

            for DX in range(-2, 3):
                for DY in range(-2, 3):
                    if DX == 0 and DY == 0:
                        continue

                    TC = (GX + DX, GY + DY)

                    if TC not in CELLS:
                        continue

                    if DX != 0:
                        HORIZONTAL = True
                        SIGN = 1 if DX > 0 else -1
                        SRC_SORTED = SRC_Y
                        TGT = BY_Y[TC]
                    else:
                        HORIZONTAL = False
                        SIGN = 1 if DY > 0 else -1
                        SRC_SORTED = SRC_X
                        TGT = BY_X[TC]

                    KEY = (HORIZONTAL, SIGN)

                    if KEY not in CACHE:
                        CACHE[KEY] = BUILD(
                            SRC_SORTED,
                            HORIZONTAL,
                            SIGN
                        )

                    HULL, START, BASE = CACHE[KEY]

                    PTR = 0

                    for Q in TGT:
                        V = Y[Q] if HORIZONTAL else X[Q]
                        Z = (V - BASE) / D

                        while (
                            PTR + 1 < len(HULL)
                            and START[PTR + 1] <= Z
                        ):
                            PTR += 1

                        if DIST[Q] != -1:
                            continue

                        OK = False

                        for J in range(
                            max(0, PTR - 2),
                            min(len(HULL), PTR + 3)
                        ):
                            P = HULL[J]

                            DX2 = X[P] - X[Q]
                            DY2 = Y[P] - Y[Q]

                            if DX2 * DX2 + DY2 * DY2 <= D2:
                                OK = True
                                break

                        if OK:
                            DIST[Q] = LAYER + 1
                            NEXT.append(Q)

        for P in NEXT:
            DX = X[P]
            DY = Y[P] - W

            if DX * DX + DY * DY <= D2:
                print(LAYER + 2)
                return

        FRONT = NEXT
        LAYER += 1

    print(-1)


if __name__ == "__main__":
    solve()

posted:
last update: