📐

ABC207 D Congruence PointsをPythonと複素数で解く

2022/06/11に公開

問題

https://atcoder.jp/contests/abc207/tasks/abc207_d

方針

  • Sに含まれる全ての点(図の赤の点)の重心を求める。
  • Tに含まれる全ての点(図の緑の点)の重心を求める。
  • Sの重心からTの重心へ向かうベクトル\overrightarrow{v_{ST}}を考え、Sに含まれる全ての点にこのベクトル\overrightarrow{v_{ST}}を足してSを移動させる。
    • 移動後のSTの重心は一致し、以降これをGとする。

  • STからそれぞれ1点を選び、選んだ2点が一致するような回転角を定め、Sに含まれる全ての点をGを中心として回転させる。
    • これをすべての組み合わせで試す。回転後のSTと一致すればYesであり、すべての組で試して一致しなければNoである。
    • N \leq 100なので3重ループでも間に合う。

実装

import math
import cmath
import sys
from fractions import Fraction

# 標準入力
N = int(input())
S = [list(map(int, input().split())) for _ in range(N)]
T = [tuple(map(int, input().split())) for _ in range(N)]

# 重心
s_g = [Fraction(0), Fraction(0)]
t_g = [Fraction(0), Fraction(0)]

for i in range(N):
    s_g[0] += S[i][0]
    s_g[1] += S[i][1]

    t_g[0] += T[i][0]
    t_g[1] += T[i][1]

s_g[0] /= N
s_g[1] /= N

t_g[0] /= N
t_g[1] /= N

# 移動
vec_st = [t_g[0] - s_g[0], t_g[1] - s_g[1]]

for i in range(N):
    S[i][0] += vec_st[0]
    S[i][1] += vec_st[1]

# 回転中心の複素数
g_complex = complex(t_g[0], t_g[1])

# Tのin判定用
T_set = set(T)

# s, tを1組選ぶ
for s in S:
    for t in T:
        # 回転角thetaを求める
        s_complex = complex(s[0], s[1])
        t_complex = complex(t[0], t[1])

        if s_complex == g_complex:
            if s_complex == t_complex:
                theta = 0
            else:
                continue
        else:
            theta = cmath.phase((t_complex - g_complex) / (s_complex - g_complex))

        # Sを全てtheta回転する
        for i in range(N):
            s_rotated_complex = (complex(S[i][0], S[i][1]) - g_complex) * cmath.rect(1, theta) + g_complex
            s_rotated = [s_rotated_complex.real, s_rotated_complex.imag]

            # 整数に丸める
            if math.ceil(s_rotated[0]) - s_rotated[0] < 0.0001:
                s_rotated[0] = math.ceil(s_rotated[0])
            if s_rotated[0] - math.floor(s_rotated[0]) < 0.0001:
                s_rotated[0] = math.floor(s_rotated[0])
            if math.ceil(s_rotated[1]) - s_rotated[1] < 0.0001:
                s_rotated[1] = math.ceil(s_rotated[1])
            if s_rotated[1] - math.floor(s_rotated[1]) < 0.0001:
                s_rotated[1] = math.floor(s_rotated[1])

            if not tuple(s_rotated) in T_set:  # 回転後の点が元のTに含まれていなければ
                break
        else:  # forが最後まで回ったら
            print("Yes")
            sys.exit()

print("No")

標準入力

Sは後で移動させるのでlistに、Tは後でO(1)inを使うためにsetに変換するのでtupleにします。

N = int(input())
S = [list(map(int, input().split())) for _ in range(N)]
T = [tuple(map(int, input().split())) for _ in range(N)]

重心を求めて移動

重心は以下の式で求められます。

x_g=\frac{x_1+\cdots+x_N}{N}
y_g=\frac{y_1+\cdots+y_N}{N}

除算は怖いのでfractions.Fractionを使います。[1]

https://docs.python.org/ja/3/library/fractions.html

s_g = [Fraction(0), Fraction(0)]
t_g = [Fraction(0), Fraction(0)]

for i in range(N):
    s_g[0] += S[i][0]
    s_g[1] += S[i][1]

    t_g[0] += T[i][0]
    t_g[1] += T[i][1]

s_g[0] /= N
s_g[1] /= N

t_g[0] /= N
t_g[1] /= N

vec_st = [t_g[0]-s_g[0], t_g[1]-s_g[1]]

for i in range(N):
    S[i][0] += vec_st[0]
    S[i][1] += vec_st[1]

回転して一致するか試す

ここで座標を複素数に変換します。complex型では有理数や固定少数点数が使えないようなので、変換後の点が一致する場合に座標の値が整数になることを利用して、差が十分小さい時に値を丸めることにします。

複素数平面において、複素数sを複素数gを中心として\theta回転したものが複素数tであることは以下の式で表せます。ただし虚数単位をjとします。[2]

t-g=(s-g)e^{j \theta}=(s-g)(\cos{\theta} +j \sin{\theta})

よってs\neq gのとき、ある複素数sを回転させて複素数tに一致させる時の回転角\thetaは以下のようになります。

\theta=\arg{\left(\frac{t-g}{s-g}\right)}

s=gのときは回転角を定めることができないので、s=t=gの場合を考慮して\theta=0とします。[3]

# 回転中心の複素数
g_complex = complex(t_g[0], t_g[1])

# Tのin判定用
T_set = set(T)

# s, tを1組選ぶ
for s in S:
    for t in T:
        # 回転角thetaを求める
        s_complex = complex(s[0], s[1])
        t_complex = complex(t[0], t[1])

        if s_complex == g_complex:
            if s_complex == t_complex:
                theta = 0
            else:
                continue
        else:
            theta = cmath.phase((t_complex - g_complex) / (s_complex - g_complex))

        # Sを全てtheta回転する
        for i in range(N):
            s_rotated_complex = (complex(S[i][0], S[i][1]) - g_complex) * cmath.rect(1, theta) + g_complex
            s_rotated = [s_rotated_complex.real, s_rotated_complex.imag]

            # 整数に丸める
            if math.ceil(s_rotated[0]) - s_rotated[0] < 0.0001:
                s_rotated[0] = math.ceil(s_rotated[0])
            if s_rotated[0] - math.floor(s_rotated[0]) < 0.0001:
                s_rotated[0] = math.floor(s_rotated[0])
            if math.ceil(s_rotated[1]) - s_rotated[1] < 0.0001:
                s_rotated[1] = math.ceil(s_rotated[1])
            if s_rotated[1] - math.floor(s_rotated[1]) < 0.0001:
                s_rotated[1] = math.floor(s_rotated[1])

            if not tuple(s_rotated) in T_set:  # 回転後の点が元のTに含まれていなければ
                break
        else:  # forが最後まで回ったら
            print("Yes")
            sys.exit()

print("No")
脚注
  1. この問題ではfloat型のまま計算してもACできました。 ↩︎

  2. 数学では虚数単位として主にiを使いますが、工学などiが虚数単位以外の意味として使われる分野においては文字の重複を避けるために虚数単位として主にjを使うことがあります。Pythonでは虚数単位として1jを使うので式もこちらに合わせました。 ↩︎

  3. N=1, (a_1,b_1)=(c_1,d_1)=(0,0)の入力で困るため。 ↩︎

Discussion