Skip to main content

aoc/year2019/
day10.rs

1//! # Monitoring Station
2//!
3//! Integer only solution, avoiding floating point or trigonometric lookups such as [`atan2`].
4//!
5//! ## Part One
6//!
7//! We compare each pair of points, first subtracting the current asteroid to get a relative vector.
8//! Since all coordinates are integers we can check for multiple points on the same line by
9//! reducing the vector by its [greatest common divisor](https://en.wikipedia.org/wiki/Greatest_common_divisor).
10//! For example, looking from the origin `(0, 0)`, the points `(3, 5)`, `(6, 10)` and `(21, 35)`
11//! are all on the same line, with gcds of 1, 2 and 7 respectively.
12//!
13//! For each point we build a set of previously seen values. Since we can see at most one asteroid
14//! in a given direction, if a vector is already in the set then we ignore it. The final size of
15//! the set is the number of visible asteroids.
16//!
17//! To speed things up a little, we rely on the fact that if asteroid `a` can see `b`, then `b`
18//! can see `a`. The solution is still `O(n²)` complexity but we reduce the work by half. This
19//! works by implicitly processing asteroids in the same order as the input, from left to right and
20//! top to bottom, so that nearest asteroids are always encountered first.
21//!
22//! ## Part Two
23//!
24//! The key insight is that we only need the *relative* angle between two vectors to sort them
25//! in clockwise order. The [vector cross product](https://en.wikipedia.org/wiki/Cross_product)
26//! of `a` and `b` can be defined as `|a||b|sinθ` where `θ` is the angle between the vectors.
27//! `θ` will be negative if anti-clockwise, zero if on the same line or positive if clockwise.
28//!
29//! This works for angles up to 90°. To handle the complete 360° rotation, we first order points
30//! by [quadrant](https://en.wikipedia.org/wiki/Quadrant_(plane_geometry)) then by relative angle.
31//!
32//! Finally, we also order points from nearest to furthest, so that the total ordering is:
33//! 1. Quadrant
34//! 2. Clockwise angle
35//! 3. Distance
36//!
37//! This results in something like (where letters indicate angle and numbers indicate distance):
38//!
39//! `a1 a2 a3 b1 c1 c2 c3 c4 d1 d2`
40//!
41//! We want to process asteroids in the order:
42//!
43//! `a1 b1 c1 d1 a2 c2 d2 a3 c3 c4`
44//!
45//! We do this by first numbering the position within each group, then numbering the group and
46//! sorting a second time in this order.
47//!
48//! [`atan2`]: f64::atan2
49use std::cmp::Ordering;
50
51use crate::util::grid::*;
52use crate::util::math::*;
53use crate::util::point::*;
54
55type Input = (i32, i32);
56
57pub fn parse(input: &str) -> Input {
58    let grid = Grid::parse(input);
59    let width = grid.width;
60    let height = grid.height;
61
62    // Convert asteroids to `Point` objects.
63    let mut points = Vec::new();
64
65    for y in 0..height {
66        for x in 0..width {
67            let point = Point::new(x, y);
68            if grid[point] == b'#' {
69                points.push(point);
70            }
71        }
72    }
73
74    // Find asteroid with the highest visibility.
75    let mut seen = Grid::new(2 * width, height, 0);
76    let mut cache = seen.same_size_with(ORIGIN);
77    let mut visible = vec![0; points.len()];
78    let mut max_visible = 0;
79    let mut max_index = 0;
80
81    for i in 0..(points.len() - 1) {
82        for j in (i + 1)..points.len() {
83            let delta = points[j] - points[i];
84            // Because points was populated in order from left to right and top to bottom, delta.y
85            // will be non-negative. Although delta.x might be negative, shifting it by
86            // width gives us a positive reference point that can then cache the mapping
87            // to a representative index.
88            let key = Point::new(width + delta.x, delta.y);
89
90            if cache[key] == ORIGIN {
91                // Key insight is that points on the same line are integer multiples of each other.
92                let gcd = delta.x.abs().gcd(delta.y);
93                let adjusted = Point::new(delta.x / gcd + width, delta.y / gcd);
94
95                cache[key] = adjusted;
96            }
97            let index = cache[key];
98
99            if seen[index] <= i {
100                seen[index] = i + 1;
101                visible[i] += 1;
102                visible[j] += 1;
103            }
104        }
105
106        if visible[i] > max_visible {
107            max_visible = visible[i];
108            max_index = i;
109        }
110    }
111
112    // Remove our new base of operations, then sort remaining asteroids in clockwise order to
113    // group by angle.
114    let station = points.swap_remove(max_index);
115    points.iter_mut().for_each(|p| *p -= station);
116    points.sort_unstable_by(|&a, &b| clockwise(a, b));
117
118    // Sort asteroids a second time, first by order within the group, then the group's order.
119    let mut groups = Vec::with_capacity(points.len());
120    let mut first = 0;
121    let mut second = 0;
122
123    groups.push((first, second, 0));
124
125    for i in 1..points.len() {
126        if angle(points[i], points[i - 1]) == Ordering::Greater {
127            first = 0;
128            second += 1;
129        } else {
130            first += 1;
131        }
132        groups.push((first, second, i));
133    }
134
135    // The 200th asteroid is at index 199. Quickselect is faster than a full sort.
136    let index = groups.select_nth_unstable(199).1.2;
137    let target = station + points[index];
138    let result = 100 * target.x + target.y;
139
140    (max_visible, result)
141}
142
143pub fn part1(input: &Input) -> i32 {
144    input.0
145}
146
147pub fn part2(input: &Input) -> i32 {
148    input.1
149}
150
151/// The [`then`] method chains [`Ordering`] results.
152///
153/// [`then`]: Ordering::then
154fn clockwise(point: Point, other: Point) -> Ordering {
155    quadrant(point)
156        .cmp(&quadrant(other))
157        .then(angle(point, other))
158        .then(distance(point).cmp(&distance(other)))
159}
160
161/// Divide points into one of four quadrants. For points exactly on an axis, for example (1, 0)
162/// or (-5, 0) we can choose either adjacent quadrant as long as we're consistent.
163fn quadrant(point: Point) -> i32 {
164    match (point.x >= 0, point.y >= 0) {
165        (true, false) => 0,
166        (true, true) => 1,
167        (false, true) => 2,
168        (false, false) => 3,
169    }
170}
171
172/// Positive if clockwise, zero if on the same line, negative if anti-clockwise.
173fn angle(point: Point, other: Point) -> Ordering {
174    (other.x * point.y).cmp(&(other.y * point.x))
175}
176
177/// Euclidean distance squared. No need to take square root since we're only interested
178/// in the *relative* distance.
179fn distance(point: Point) -> i32 {
180    point.x * point.x + point.y * point.y
181}