E - 直線の傾き / Slope of a Line 解説 by admin
Claude Opus 5 (High)概要
\(N\) 個の点から \(2\) 点を選んだとき、その \(2\) 点を結ぶ直線の傾きが \([A, B]\) に入るような組の個数を数える問題です。座標変換によって「2 次元の順序に関する組の数え上げ(転倒数の数え上げ)」に帰着させ、BIT(Fenwick Tree)で高速に処理します。
考察
素朴な方法
すべての組 \((i,j)\) を試すと \(O(N^2)\) で、\(N = 10^5\) では約 \(5 \times 10^9\) 回の計算になり間に合いません。また、傾きを浮動小数点で計算すると誤差で WA になる危険があります(比較は必ず整数の掛け算で行うべきです)。
不等式を「点ごとの値」に分解する
\(X_i \ne X_j\) なので、\(X_i < X_j\) となるように \(i, j\) を決めておきます(傾きは \(i,j\) を入れ替えても変わらないので一般性を失いません)。\(X_j - X_i > 0\) なので、分母を払っても不等号の向きは変わりません。
\[A(X_j - X_i) \le Y_j - Y_i \le B(X_j - X_i)\]
左側を整理すると $\(Y_i - A X_i \le Y_j - A X_j\)\( 右側を整理すると \)\(Y_i - B X_i \ge Y_j - B X_j\)$
ここで各点について $\(u_i = Y_i - A X_i,\qquad v_i = Y_i - B X_i\)$ と定義すると、条件は
\[u_i \le u_j \quad \text{かつ} \quad v_i \ge v_j \qquad (X_i < X_j)\]
というシンプルな形になります。各点ごとに計算できる 2 つの値の大小比較に変わったのがポイントです。
「\(X\) の順序」を気にしなくてよい理由
まだ「\(X_i < X_j\) という前提」が残っていますが、実は不要です。\(u_i - v_i = (B-A)X_i\) であり \(B \ge A\) なので、
- \(u_i > u_j\) ならば \(u_i - v_i \le u_j - v_j\)(\(X_i<X_j\) より)から \(v_i > v_j\) が従う
- \(v_i < v_j\) ならば同様に \(u_i < u_j\) が従う
つまり \(X_i < X_j\) のもとで条件の否定は $\((u_i < u_j \text{ かつ } v_i < v_j) \quad\text{または}\quad (u_i > u_j \text{ かつ } v_i > v_j)\)\( に限られます。これは「\)u\( と \)v\( が同じ向きに**狭義単調**で並んでいる組」であり、\)i,j\( の入れ替えについて対称なので、\)X$ の大小を気にせず判定できます。
よって
\[\text{答え} = \binom{N}{2} - \#\{ \text{組 } (i,j) : u,v \text{ がともに同じ向きに狭義増加している} \}\]
(\(A = B\) の場合も、\(u_i = v_i\) となり「答え \(=\) \(u_i = u_j\) となる組の個数」を正しく与えます。)
アルゴリズム
数えるべきは「\(u_i < u_j\) かつ \(v_i < v_j\) となる組の数」(いわゆる concordant pair の数)です。これは転倒数の数え方と同じ要領で求められます。
- 各点について \(u_i = Y_i - A X_i\), \(v_i = Y_i - B X_i\) を計算する。
- \(u\) の昇順にソートする。ただし \(u\) が等しいときは \(v\) の降順にする。
- こうすると、ソート後の順で「前にある要素の \(v\) より現在の \(v\) が大きい」ことが、そのまま「\(u\) も狭義に小さい」ことを意味します(\(u\) が同じ組は \(v\) が降順なので数え上げられません)。
- \(v\) の値を座標圧縮し、BIT(Fenwick Tree、要素数 \(m \le N\))を用意する。
- ソート順に走査し、各要素について「すでに挿入済みで \(v\) のランクが自分より真に小さい個数」(= ランク \(r-1\) までの prefix sum)を足し込み、その後で自分のランクに \(+1\) する。この総和が concordant pair の数。
- \(\binom{N}{2}\) から引いて出力する。
小さな例
\(A=0, B=1\)、点 \((0,0), (1,2), (2,1)\) の場合。 \(u = (0, 2, 1)\)、\(v = (0, 1, -1)\)。 - 組 \((1,2)\): \(u:0<2\), \(v:0<1\) → 同じ向き → 条件を満たさない(実際傾き \(2\))。 - 組 \((1,3)\): \(u:0<1\), \(v:0>-1\) → 満たす(傾き \(0.5\))。 - 組 \((2,3)\): \(u:2>1\), \(v:1>-1\) → 同じ向き → 満たさない(傾き \(-1\))。
答えは \(3 - 2 = 1\) で正しいです。
計算量
- 時間計算量: \(O(N \log N)\)(ソートと BIT の操作)
- 空間計算量: \(O(N)\)
実装のポイント
オーバーフロー: \(|Y_i - A X_i| \le 10^9 + 10^9 \cdot 10^9 \approx 10^{18}\) なので、C++ 等では必ず
long longを使います(Python は多倍長なので安心)。ソートのタイブレーク: コードでは
(u, -v)をキーにソートしています。これで \(u\) が同じときに \(v\) が降順に並び、\(u\) が等しい組を誤って数えるのを防げます。ここを間違えると、傾きがちょうど \(A\)(または \(B\))になる組の扱いを誤ります。座標圧縮 + BIT: \(v\) の値は最大 \(10^{18}\) 規模なのでそのままでは添字にできません。
sorted(set(...))で圧縮し、\(1\) 始まりのランクにします(BIT は添字 \(0\) を扱えないため)。prefix sum の範囲: 「真に小さい」を数えるので、ランク \(r\) の要素に対して \(r-1\) までの和を取ります(\(r\) までにすると同値を含んでしまいます)。
入出力: \(N = 10^5\) なので Python では
sys.stdin.buffer.read()による一括読み込みが安全です。傾きの割り算は一切行わないため、精度の問題は発生しません。
ソースコード
import sys
def main():
data = sys.stdin.buffer.read().split()
n = int(data[0]); A = int(data[1]); B = int(data[2])
X = list(map(int, data[3::2]))
Y = list(map(int, data[4::2]))
# u_i = Y_i - A*X_i, v_i = Y_i - B*X_i
# condition for pair: u_i <= u_j and v_i >= v_j (orientation implied)
# answer = C(n,2) - #{pairs with u_i < u_j and v_i < v_j}
arr = [0] * n
for i in range(n):
x = X[i]; y = Y[i]
arr[i] = (y - A * x, -(y - B * x))
arr.sort() # u asc, v desc for ties in u
vlist = [-t[1] for t in arr]
sv = sorted(set(vlist))
idx = {}
for i, v in enumerate(sv):
idx[v] = i + 1
m = len(sv)
ranks = [idx[v] for v in vlist]
tree = [0] * (m + 1)
conc = 0
for r in ranks:
s = 0
p = r - 1
while p > 0:
s += tree[p]
p -= p & (-p)
conc += s
p = r
while p <= m:
tree[p] += 1
p += p & (-p)
sys.stdout.write(str(n * (n - 1) // 2 - conc) + "\n")
main()
この解説は claude5opus-high によって生成されました。
投稿日時:
最終更新: