atcoder/lib/geometry.rs
1//! 整数座標の2次元幾何を扱います。
2//!
3//! x 軸は右向き、y 軸は上向きの座標系を想定します。画面やグリッドのように
4//! y が下向きに増える座標系では、時計回り・反時計回りの見え方が逆になります。
5//! 座標だけでなく、差・積・和を含む途中計算も `isize` に収まる必要があります。
6
7use std::cmp::Ordering;
8use std::ops::{Add, Mul, Sub};
9
10/// 整数座標上の点またはベクトルです。
11///
12/// 演算結果が [`isize`] の範囲を超えないことは呼び出し側で保証してください。
13///
14/// # Examples
15///
16/// ```
17/// use atcoder::geometry::Point;
18///
19/// let a = Point::new(1, 2);
20/// let b = Point::new(4, 6);
21/// assert_eq!(a.distance_squared(b), 25);
22/// ```
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub struct Point {
25 /// x 座標です。
26 pub x: isize,
27 /// y 座標です。
28 pub y: isize,
29}
30
31impl Point {
32 /// 座標 `(x, y)` の点を作ります。
33 pub const fn new(x: isize, y: isize) -> Self {
34 Self { x, y }
35 }
36
37 /// 2つのベクトルの内積を返します。
38 ///
39 /// `self.x * other.x + self.y * other.y` です。両方が零ベクトルでない場合、
40 /// 正ならなす角は鋭角、`0` なら直角、負なら鈍角または反対向きです。
41 /// 点を引き算して作った方向ベクトルの角度判定に使えます。計算量は `O(1)` です。
42 pub const fn dot(self, other: Self) -> isize {
43 self.x * other.x + self.y * other.y
44 }
45
46 /// 2つのベクトルの外積を返します。
47 ///
48 /// `self.x * other.y - self.y * other.x` です。正なら `other` は `self` の
49 /// 反時計回り側、負なら時計回り側です。`0` は平行で、逆向きや零ベクトルも含みます。
50 /// 絶対値は2つのベクトルが作る平行四辺形の面積です。計算量は `O(1)` です。
51 pub const fn cross(self, other: Self) -> isize {
52 self.x * other.y - self.y * other.x
53 }
54
55 /// 2つのベクトルが平行なら `true` を返します。
56 ///
57 /// 外積が `0` かを判定します。逆向きも平行とし、零ベクトルを含む場合も `true` です。
58 pub const fn is_parallel(self, other: Self) -> bool {
59 self.cross(other) == 0
60 }
61
62 /// 2つのベクトルが直交するなら `true` を返します。
63 ///
64 /// 内積が `0` かを判定します。零ベクトルを含む場合も `true` です。
65 pub const fn is_orthogonal(self, other: Self) -> bool {
66 self.dot(other) == 0
67 }
68
69 /// 原点からの距離の二乗を返します。
70 ///
71 /// `x * x + y * y` です。平方根を取らずに距離の大小を比較できます。
72 pub const fn norm_squared(self) -> isize {
73 self.dot(self)
74 }
75
76 /// 原点からの距離の二乗を返します。
77 ///
78 /// [`norm_squared`](Self::norm_squared) と同じです。
79 pub const fn norm2(self) -> isize {
80 self.norm_squared()
81 }
82
83 /// `other` までの距離の二乗を返します。
84 ///
85 /// `(self.x - other.x)^2 + (self.y - other.y)^2` です。実際の距離ではなく
86 /// その二乗を返すため、距離の比較にも閾値の二乗を使います。計算量は `O(1)` です。
87 pub fn distance_squared(self, other: Self) -> isize {
88 (self - other).norm_squared()
89 }
90
91 /// `other` までの距離の二乗を返します。
92 ///
93 /// [`distance_squared`](Self::distance_squared) と同じです。
94 pub fn dist2(self, other: Self) -> isize {
95 self.distance_squared(other)
96 }
97}
98
99impl Add for Point {
100 type Output = Self;
101
102 fn add(self, rhs: Self) -> Self::Output {
103 Self::new(self.x + rhs.x, self.y + rhs.y)
104 }
105}
106
107impl Sub for Point {
108 type Output = Self;
109
110 fn sub(self, rhs: Self) -> Self::Output {
111 Self::new(self.x - rhs.x, self.y - rhs.y)
112 }
113}
114
115impl Mul<isize> for Point {
116 type Output = Self;
117
118 fn mul(self, rhs: isize) -> Self::Output {
119 Self::new(self.x * rhs, self.y * rhs)
120 }
121}
122
123/// 原点から見た2点の偏角を比較します。
124///
125/// 正のx軸を始点に反時計回りで比較し、同じ向きなら [`Ordering::Equal`] を返します。
126/// 原点は含めず、座標同士の積が `isize` に収まる範囲で使用してください。
127/// 計算量は `O(1)` です。
128/// 同じ向きの点同士は距離では並べ替えません。別の点を中心にする場合は、
129/// その中心を両方の点から引いたベクトルを比較してください。
130///
131/// # Examples
132///
133/// ```
134/// use atcoder::geometry::{argcmp, Point};
135///
136/// let mut points = [Point::new(0, -1), Point::new(0, 1), Point::new(1, 0)];
137/// points.sort_by(|&a, &b| argcmp(a, b));
138/// assert_eq!(points, [Point::new(1, 0), Point::new(0, 1), Point::new(0, -1)]);
139/// ```
140///
141/// # 参考
142///
143/// ngtkanaさんの[整数演算による偏角ソートの記事](https://blog.ngtkana.com/entry/2021/11/13/202103)を参考にしています。
144pub fn argcmp(a: Point, b: Point) -> Ordering {
145 // 辞書順で (y, x) < (0, 0) は「y < 0、または y == 0 かつ x < 0」。
146 // 下半平面と負のx軸を後半に分け、角度が一周する境界を正のx軸に置く。
147 ((a.y, a.x) < (0, 0))
148 .cmp(&((b.y, b.x) < (0, 0)))
149 // 同じ半分の中では外積の符号で順序を決める。a×b > 0 なら a が先。
150 .then((b.x * a.y).cmp(&(a.x * b.y)))
151}
152
153/// 3点 `a`, `b`, `c` の向きを外積で返します。
154///
155/// 正なら反時計回り、負なら時計回り、`0` なら一直線上です。計算量は `O(1)` です。
156///
157/// 有向直線 `a → b` から見て `c` が左側なら正、右側なら負です。
158/// 戻り値は `(b - a) × (c - a)` そのもので、`-1 / 0 / 1` に正規化しません。
159/// 絶対値は三角形 `abc` の面積の2倍です。`0` だけでは `c` が線分上かは
160/// 分からないため、線分上の判定には [`on_segment`] を使ってください。
161///
162/// ```
163/// use atcoder::geometry::{ccw, Point};
164/// let a = Point::new(0, 0);
165/// let b = Point::new(2, 0);
166/// assert_eq!(ccw(a, b, Point::new(1, 3)), 6); // 左側(上側)
167/// assert_eq!(ccw(a, b, Point::new(1, -3)), -6); // 右側(下側)
168/// assert_eq!(ccw(a, b, Point::new(3, 0)), 0); // 直線上だが線分外
169/// ```
170pub fn ccw(a: Point, b: Point, c: Point) -> isize {
171 (b - a).cross(c - a)
172}
173
174/// 点 `point` が閉線分 `ab` 上にあるか判定します。
175///
176/// 端点を含みます。計算量は `O(1)` です。
177/// `a == b` のときは、`point == a` の場合だけ `true` です。
178pub fn on_segment(a: Point, b: Point, point: Point) -> bool {
179 // 同一直線上で、point から両端へのベクトルが反対向き(端点なら一方が0)か調べる。
180 ccw(a, b, point) == 0 && (a - point).dot(b - point) <= 0
181}
182
183/// 2つの閉線分 `ab` と `cd` が交差するか判定します。
184///
185/// 端点での接触や同一直線上の重なりも交差に含めます。計算量は `O(1)` です。
186/// 長さ0の線分も扱えます。交点の座標や交差の種類は返しません。
187///
188/// # Examples
189///
190/// ```
191/// use atcoder::geometry::{segments_intersect, Point};
192///
193/// assert!(segments_intersect(
194/// Point::new(0, 0),
195/// Point::new(2, 2),
196/// Point::new(0, 2),
197/// Point::new(2, 0),
198/// ));
199/// ```
200pub fn segments_intersect(a: Point, b: Point, c: Point, d: Point) -> bool {
201 let ab_c = ccw(a, b, c);
202 let ab_d = ccw(a, b, d);
203 let cd_a = ccw(c, d, a);
204 let cd_b = ccw(c, d, b);
205
206 if ab_c == 0 && on_segment(a, b, c) {
207 return true;
208 }
209 if ab_d == 0 && on_segment(a, b, d) {
210 return true;
211 }
212 if cd_a == 0 && on_segment(c, d, a) {
213 return true;
214 }
215 if cd_b == 0 && on_segment(c, d, b) {
216 return true;
217 }
218
219 (ab_c > 0) != (ab_d > 0) && (cd_a > 0) != (cd_b > 0)
220}
221
222/// 多角形の符号付き面積の2倍を返します。
223///
224/// `points` は周上の順に並べます。反時計回りなら正、時計回りなら負です。
225/// 空または頂点が2個以下なら `0` を返します。頂点数を `N` とすると `O(N)` です。
226/// 最後の頂点と最初の頂点は自動的につなぐため、先頭頂点を末尾に再追加する必要はありません。
227/// 自己交差しない多角形の通常の面積は、戻り値の絶対値を2で割った値です。
228/// 戻り値が奇数なら面積は半整数なので、整数除算による切り捨てに注意してください。
229pub fn polygon_area2(points: &[Point]) -> isize {
230 if points.is_empty() {
231 return 0;
232 }
233 (0..points.len())
234 .map(|index| points[index].cross(points[(index + 1) % points.len()]))
235 .sum()
236}
237
238#[cfg(test)]
239mod tests {
240 use std::cmp::Ordering;
241
242 use super::{argcmp, polygon_area2, segments_intersect, Point};
243
244 #[test]
245 fn compares_polar_angles_counterclockwise_from_positive_x_axis() {
246 let points = [
247 Point::new(1, 0),
248 Point::new(1, 1),
249 Point::new(0, 1),
250 Point::new(-1, 1),
251 Point::new(-1, 0),
252 Point::new(-1, -1),
253 Point::new(0, -1),
254 Point::new(1, -1),
255 ];
256 for (i, &a) in points.iter().enumerate() {
257 for (j, &b) in points.iter().enumerate() {
258 assert_eq!(argcmp(a, b), i.cmp(&j));
259 }
260 }
261 }
262
263 #[test]
264 fn compares_same_direction_vectors_as_equal() {
265 assert_eq!(argcmp(Point::new(1, 2), Point::new(2, 4)), Ordering::Equal);
266 assert_eq!(
267 argcmp(Point::new(-1, -2), Point::new(-2, -4)),
268 Ordering::Equal
269 );
270 assert_ne!(
271 argcmp(Point::new(1, 2), Point::new(-1, -2)),
272 Ordering::Equal
273 );
274 }
275
276 #[test]
277 fn detects_touching_and_disjoint_segments() {
278 assert!(segments_intersect(
279 Point::new(0, 0),
280 Point::new(2, 0),
281 Point::new(2, 0),
282 Point::new(3, 1)
283 ));
284 assert!(!segments_intersect(
285 Point::new(0, 0),
286 Point::new(1, 0),
287 Point::new(2, 0),
288 Point::new(3, 0)
289 ));
290 }
291
292 #[test]
293 fn computes_signed_double_area() {
294 let triangle = [Point::new(0, 0), Point::new(3, 0), Point::new(0, 2)];
295 assert_eq!(polygon_area2(&triangle), 6);
296 }
297}