Official

E - レーザーポインターの実験 / Laser Pointer Experiment Editorial by admin

gpt-5.5-high

概要

レーザー光線から距離 \(D\) 以下にある点を数える問題は、幅 \(2D\) の帯状領域(ストリップ)に含まれる点の最大個数を求める問題と言い換えられます。

考察

レーザー光線を中心線とすると、反応するセンサーはその中心線から距離 \(D\) 以下にある点です。
これは、中心線の両側に距離 \(D\) の境界線を引いた、幅

\(W = 2D\)

のストリップに含まれる点を数えることと同じです。

つまり問題は、

平面上に幅 \(W\) の平行な 2 直線でできる帯を置き、その中に入る点の重み付き個数を最大化する

問題になります。

同じ座標に複数センサーがある場合、それらは常に同時に反応するため、座標ごとにまとめて「重み」として扱います。


素朴に「2 点を通る直線」をすべて試すだけでは不十分です。

例えば \(D=1\) で点が \((0,0)\), \((0,2)\) にある場合、最適なレーザー光線は \(y=1\) ですが、この直線はどちらの点も通りません。

そこで、レーザー光線そのものではなく、幅 \(W\) のストリップの境界線に注目します。

最適なストリップは、少し平行移動・回転しても含まれる点数が減らないように調整できるため、次のどちらかの形にできます。

  1. 片方の境界線上に 2 点以上ある
  2. 両方の境界線上にそれぞれ 1 点ずつある

したがって、この 2 種類の候補だけを列挙すれば十分です。

アルゴリズム

まず、同じ座標にあるセンサーをまとめます。
以降、各点 \(P_i=(X_i,Y_i)\) は重み \(w_i\) を持つものとして扱います。

1. \(D=0\) の場合

\(D=0\) のとき、ストリップの幅は \(0\) なので、単に同一直線上にある点の重みの最大値を求めればよいです。

各点 \(P_i\) を基準にして、他の点への方向ベクトルを正規化します。

方向ベクトル \((dx,dy)\) に対して、

\(g=\gcd(|dx|,|dy|)\)

で割り、

\((dx/g,dy/g)\)

を使います。

ただし、反対向きは同じ直線なので、符号を統一します。

各方向ごとに重みを合計し、最大値を更新します。


2. \(D>0\) の場合

幅を

\(W=2D\)

とします。

ケース A: 片方の境界線上に 2 点ある場合

\(P_i\), \(P_j\) が同じ境界線上にあるとします。

この境界線の方向ベクトルを

\((dx,dy)=P_j-P_i\)

とすると、その法線ベクトルは例えば

\((dy,-dx)\)

です。

法線ベクトルを \(n=(n_x,n_y)\) とします。

\(P_k\) について、

\(s=n_x(X_k-X_i)+n_y(Y_k-Y_i)\)

を考えます。

これは、点 \(P_k\) が境界線から法線方向にどれだけ離れているかを表す量です。

\(P_k\) がストリップ内に入る条件は、境界線の片側について

\(0 \leq s \leq W|n|\)

または反対側について

\(-W|n| \leq s \leq 0\)

です。

平方根を避けるため、実装では

\(s^2 \leq W^2(n_x^2+n_y^2)\)

で判定しています。

境界線の両側のどちらにストリップを置くかで個数が変わるので、両方数えて大きい方を使います。


ケース B: 両方の境界線上にそれぞれ 1 点ある場合

\(P_i\) が一方の境界線上、点 \(P_j\) がもう一方の境界線上にあるとします。

このとき、\(P_i\) から \(P_j\) へのベクトルを

\(v=(dx,dy)\)

とし、

\(r^2=dx^2+dy^2\)

とします。

2 点が幅 \(W\) のストリップの反対側の境界に乗るためには、少なくとも

\(r > W\)

である必要があります。

法線方向は、\(v\) への射影がちょうど \(W\) になる方向です。
これは、\(r>W\) のとき 2 通りあります。

\(v\) に垂直なベクトルを

\(q=(-dy,dx)\)

とすると、法線方向に比例するベクトルは

\(h_\pm = Wv \pm \sqrt{r^2-W^2}q\)

と表せます。

このベクトルは長さが \(r^2\) になるようにスケールされており、

\(h_\pm \cdot v = Wr^2\)

を満たします。

\(P_k\) について、\(u=P_k-P_i\) とすると、ストリップ内にある条件は

\(0 \leq h_\pm \cdot u \leq Wr^2\)

です。

実装では

\(h_\pm \cdot u = W(v\cdot u) \pm \sqrt{r^2-W^2}(q\cdot u)\)

を使って判定しています。

平方根が整数でない場合もあるため、浮動小数点数は使わず、平方して大小比較することで厳密に判定しています。


以上の 2 種類の候補をすべて試し、最大値を答えます。

計算量

重複をまとめた後の異なる座標数を \(M\) とします。
\(M \leq N\) です。

  • 時間計算量: \(O(M^3)\)
  • 空間計算量: \(O(M^2)\)

\(N \leq 200\) なので、\(O(N^3)\) で十分間に合います。

実装のポイント

  • 同じ座標のセンサーは重みとしてまとめます。

  • \(D=0\) の場合は、最大共線点数を求める問題として別処理します。

  • 距離判定では平方根や浮動小数点数を避け、整数演算で比較します。

  • ストリップの境界上の点も反応するため、不等号はすべて \(\leq\) を使います。

  • 答えが \(N\) になった時点で、それ以上改善できないので早期終了できます。

    ソースコード

import sys
import math
from collections import defaultdict

def count_lower_both(nx, ny, dxs, dys, ws, W2):
    lim = W2 * (nx * nx + ny * ny)
    cp = 0
    cn = 0
    m = len(ws)
    for i in range(m):
        dot = nx * dxs[i] + ny * dys[i]
        sq = dot * dot
        if sq <= lim:
            w = ws[i]
            if dot >= 0:
                cp += w
            if dot <= 0:
                cn += w
    return cp if cp >= cn else cn

def count_upper_both(dx, dy, r2, dxs, dys, ws, W, W2):
    mm = r2 - W2
    c_lim = W * r2
    qx = -dy
    qy = dx
    m = len(ws)

    root = math.isqrt(mm)
    if root * root == mm:
        cp = 0
        cn = 0
        for i in range(m):
            ux = dxs[i]
            uy = dys[i]
            a = W * (dx * ux + dy * uy)
            b = qx * ux + qy * uy
            w = ws[i]
            v = a + b * root
            if 0 <= v <= c_lim:
                cp += w
            v = a - b * root
            if 0 <= v <= c_lim:
                cn += w
        return cp if cp >= cn else cn

    cp = 0
    cn = 0
    for i in range(m):
        ux = dxs[i]
        uy = dys[i]
        a = W * (dx * ux + dy * uy)
        b = qx * ux + qy * uy
        ac = a - c_lim
        wt = ws[i]

        b2m = -1
        a2 = -1
        ac2 = -1

        ok = True
        if a < 0:
            if b < 0:
                ok = False
            else:
                b2m = b * b * mm
                a2 = a * a
                if b2m < a2:
                    ok = False
        else:
            if b < 0:
                b2m = b * b * mm
                a2 = a * a
                if a2 < b2m:
                    ok = False

        if ok:
            if ac > 0:
                if b > 0:
                    ok = False
                else:
                    if b2m < 0:
                        b2m = b * b * mm
                    ac2 = ac * ac
                    if ac2 > b2m:
                        ok = False
            else:
                if b > 0:
                    if b2m < 0:
                        b2m = b * b * mm
                    ac2 = ac * ac
                    if b2m > ac2:
                        ok = False
            if ok:
                cp += wt

        ok = True
        if a < 0:
            if b > 0:
                ok = False
            else:
                if b2m < 0:
                    b2m = b * b * mm
                if a2 < 0:
                    a2 = a * a
                if b2m < a2:
                    ok = False
        else:
            if b > 0:
                if b2m < 0:
                    b2m = b * b * mm
                if a2 < 0:
                    a2 = a * a
                if a2 < b2m:
                    ok = False

        if ok:
            if ac > 0:
                if b < 0:
                    ok = False
                else:
                    if b2m < 0:
                        b2m = b * b * mm
                    if ac2 < 0:
                        ac2 = ac * ac
                    if ac2 > b2m:
                        ok = False
            else:
                if b < 0:
                    if b2m < 0:
                        b2m = b * b * mm
                    if ac2 < 0:
                        ac2 = ac * ac
                    if b2m > ac2:
                        ok = False
            if ok:
                cn += wt

    return cp if cp >= cn else cn

def main():
    input = sys.stdin.readline
    N, D = map(int, input().split())

    cnt = defaultdict(int)
    for _ in range(N):
        x, y = map(int, input().split())
        cnt[(x, y)] += 1

    pts = list(cnt.keys())
    ws = [cnt[p] for p in pts]
    xs = [p[0] for p in pts]
    ys = [p[1] for p in pts]
    M = len(pts)

    if M == 1:
        print(N)
        return

    ans = max(ws)

    dxmat = [[xs[j] - xs[i] for j in range(M)] for i in range(M)]
    dymat = [[ys[j] - ys[i] for j in range(M)] for i in range(M)]

    if D == 0:
        for i in range(M):
            mp = defaultdict(int)
            dxs = dxmat[i]
            dys = dymat[i]
            for j in range(M):
                if i == j:
                    continue
                dx = dxs[j]
                dy = dys[j]
                g = math.gcd(abs(dx), abs(dy))
                dx //= g
                dy //= g
                if dx < 0 or (dx == 0 and dy < 0):
                    dx = -dx
                    dy = -dy
                mp[(dx, dy)] += ws[j]
            best = ws[i]
            for v in mp.values():
                cur = ws[i] + v
                if cur > best:
                    best = cur
            if best > ans:
                ans = best
        print(ans)
        return

    W = 2 * D
    W2 = W * W

    for i in range(M):
        dxs = dxmat[i]
        dys = dymat[i]

        normals = set()
        for j in range(M):
            if i == j:
                continue
            dx = dxs[j]
            dy = dys[j]
            g = math.gcd(abs(dx), abs(dy))
            nx = dy // g
            ny = -dx // g
            if nx < 0 or (nx == 0 and ny < 0):
                nx = -nx
                ny = -ny
            normals.add((nx, ny))

        for nx, ny in normals:
            v = count_lower_both(nx, ny, dxs, dys, ws, W2)
            if v > ans:
                ans = v
                if ans == N:
                    print(ans)
                    return

        for j in range(M):
            if i == j:
                continue
            dx = dxs[j]
            dy = dys[j]
            r2 = dx * dx + dy * dy
            if r2 > W2:
                v = count_upper_both(dx, dy, r2, dxs, dys, ws, W, W2)
                if v > ans:
                    ans = v
                    if ans == N:
                        print(ans)
                        return

    print(ans)

if __name__ == "__main__":
    main()

この解説は gpt-5.5-high によって生成されました。

posted:
last update: