ntk log ntk

競プロや非競技プロのやったことを書いています.

ABC181 F - Silver Woods

昨日のABCはEまでの5完!やったぜ!

コンテスト本番中はわからなかったFを復習したのでまとめる.

F - Silver Woods

問題概要

下の図のように,y = 100 からy = -100の直線に挟まれた通路にいくつか釘(赤い点で表す)が打たれている. 円を(-10^9,0)から(10^9,0)まで動かすとき,円の内部に釘や通路の境界が入らないようにしたい. 円の半径の最大値を求めよ. f:id:ntk_ta01:20201102142635j:plain

観察

ある2点,もしくは点と直線の距離より円の直径が大きいと,円が通れない.

f:id:ntk_ta01:20201102143252j:plain

解法

全点対間距離と,それぞれの点とそれぞれの直線の距離をすべて求めておく.2点,もしくは点と直線のペアを,その距離でソートしておく.そして,距離が小さい2点,もしくは点と直線から順に,線を結ぶとする.線を結んだ間は,円が通れないとする.y = 100の直線とy = -100の直線が繋がってしまうときの2点,もしくは点と直線の距離が,求める円の直径の最大値であり,これの半分が求めたい半径である.

上の直線と下の直線が,繋がっていなければ円が通れる.

f:id:ntk_ta01:20201102142645j:plain

繋がってしまうと通れなくなる.このときの下図,緑の線で表される距離が,通れる円の直径の最大値.

f:id:ntk_ta01:20201102142653j:plain

線を結ぶ,というのは直線と釘を無向グラフの頂点とみなし,union-findで管理する.上と下の直線が連結になったときの距離が,求める直径の最大値.下のコードでは,ソートを整数で行うほうがいいかも?と思って,距離を二乗で管理している.

コード

class UnionFind:
    def __init__(self, N):
        self.par = [i for i in range(N)]
        self.size = [1 for i in range(N)]

    def find(self, x):
        if self.par[x] == x:
            return x
        else:
            self.par[x] = self.find(self.par[x])
            self.size[x] = self.size[self.par[x]]
            return self.par[x]

    def same(self, x, y):
        return self.find(x) == self.find(y)

    def merge(self, x, y):
        x = self.find(x)
        y = self.find(y)
        if x == y:
            return
        if self.size[x] < self.size[y]:
            x, y = y, x
        self.size[x] += self.size[y]
        self.par[y] = x

    def get_size(self, x):
        return self.size[self.find(x)]


def main():
    from math import sqrt
    import sys
    readline = sys.stdin.buffer.readline
    N = int(readline())
    X = []
    Y = []
    for _ in range(N):
        x, y = (int(i) for i in readline().split())
        X.append(x)
        Y.append(y)

    # 頂点数 N + 2 釘と直線2本
    dist = []
    for i in range(N):
        for j in range(i+1, N):
            d = (X[i] - X[j])**2 + (Y[i] - Y[j])**2
            dist.append((d, i*(N+2) + j))

        # y = 100 との距離 頂点番号N
        d = abs(Y[i] - 100)
        dist.append((d**2, i*(N+2) + N))
        # y = -100 との距離 頂点番号N+1
        d = abs(Y[i] + 100)
        dist.append((d**2, i*(N+2) + N + 1))

    V = N + 2

    dist.sort()
    uf = UnionFind(V)
    ans = -1
    for d, pair in dist:
        u, v = pair // V, pair % V
        uf.merge(u, v)
        if uf.same(N, N + 1):
            ans = sqrt(d) / 2
            break
    print(ans)


if __name__ == '__main__':
    main()

提出コードへのリンク

tatyamさんの公式解説がきれいにまとまっていたので,整理してみたら新しいこと何も言ってないかも.直径や直径の二乗を二分探索してもいいみたい!

コード

class UnionFind:
    def __init__(self, N):
        self.par = [i for i in range(N)]
        self.size = [1 for i in range(N)]

    def find(self, x):
        if self.par[x] == x:
            return x
        else:
            self.par[x] = self.find(self.par[x])
            self.size[x] = self.size[self.par[x]]
            return self.par[x]

    def same(self, x, y):
        return self.find(x) == self.find(y)

    def merge(self, x, y):
        x = self.find(x)
        y = self.find(y)
        if x == y:
            return
        if self.size[x] < self.size[y]:
            x, y = y, x
        self.size[x] += self.size[y]
        self.par[y] = x

    def get_size(self, x):
        return self.size[self.find(x)]


def is_ok(N, V, dist, L):
    uf = UnionFind(V)
    for d, pair in dist:
        u, v = pair // V, pair % V
        if L > d:
            uf.merge(u, v)
    return not uf.same(N, N + 1)


def binary_search(N, V, dist):
    ng = 40000
    ok = 1
    while abs(ok - ng) > 1:
        mid = ng + (ok - ng) // 2
        if is_ok(N, V, dist, mid):
            ok = mid
        else:
            ng = mid
    return ok


def main():
    from math import sqrt
    import sys
    readline = sys.stdin.buffer.readline
    N = int(readline())
    X = []
    Y = []
    for _ in range(N):
        x, y = (int(i) for i in readline().split())
        X.append(x)
        Y.append(y)

    # 頂点数 N + 2 釘と直線2本
    dist = []
    for i in range(N):
        for j in range(i+1, N):
            d = (X[i] - X[j])**2 + (Y[i] - Y[j])**2
            dist.append((d, i*(N+2) + j))

        # y = 100 との距離 頂点番号N
        d = abs(Y[i] - 100)
        dist.append((d**2, i*(N+2) + N))
        # y = -100 との距離 頂点番号N+1
        d = abs(Y[i] + 100)
        dist.append((d**2, i*(N+2) + N + 1))

    V = N + 2

    dist.sort()
    # 直径の二乗を二分探索
    ans = sqrt(binary_search(N, V, dist)) / 2
    print(ans)


if __name__ == '__main__':
    main()
    

提出コードへのリンク


その他,コンテスト終了後のTLで見かけた類題や発想が近い?らしい問題.グラフにいい感じに帰着させられるようになりたい~.

codeforces.com atcoder.jp atcoder.jp