aoc/year2018/
day06.rs

1//! # Chronal Coordinates
2//!
3//! Both parts can be solved with a [BFS](https://en.wikipedia.org/wiki/Breadth-first_search)
4//! approach. The bounding box of the coordinates is roughly 300 wide by 300 high so the total
5//! complexity would be approximately `O(90,000)`.
6//!
7//! A much faster approach for both parts is a
8//! [sweep line algorithm](https://en.wikipedia.org/wiki/Sweep_line_algorithm). We sweep from
9//! top to bottom (minimum y coordinate to maximum y coordinate) computing the area a slice at a
10//! time. There are 50 coordinates so the complexity of this approach is much lower at
11//! approximately `O(300 * 50) = O(15000)`.
12use crate::util::iter::*;
13use crate::util::parse::*;
14use crate::util::point::*;
15
16pub struct Input {
17    min_y: i32,
18    max_y: i32,
19    points: Vec<Point>,
20}
21
22/// Parse points while keeping track of the min and max y coordinates.
23pub fn parse(input: &str) -> Input {
24    let mut min_y = i32::MAX;
25    let mut max_y = i32::MIN;
26    let points: Vec<_> = input
27        .iter_signed()
28        .chunk::<2>()
29        .map(|[x, y]| {
30            min_y = min_y.min(y);
31            max_y = max_y.max(y);
32            Point::new(x, y)
33        })
34        .collect();
35
36    Input { min_y, max_y, points }
37}
38
39/// Sweep line approach computing the area of each *finite* coordinate. A coordinate has infinite
40/// area if any point on the edge of the bounding box formed by the minimum and maximum x and y
41/// coordinates is closest to that coordinate.
42///
43/// We sort the coordinates in ascending x value then for each row, compare the next coordinate
44/// against the head of a stack. This quickly eliminates coordinates that are further away at all
45/// points. Interestingly this approach is very similar to the previous [`Day 5`].
46///
47/// [`Day 5`]: crate::year2018::day05
48pub fn part1(input: &Input) -> i32 {
49    let mut points = input.points.clone();
50    let mut area = vec![0; points.len()];
51    let mut finite = vec![true; points.len()];
52    let mut candidates: Vec<(usize, i32, i32)> = Vec::new();
53
54    // Special value for coordinates that are equidistant from nearest neighbour.
55    let marker = usize::MAX;
56
57    // Sorts points left to right so that ranges can be merged.
58    points.sort_unstable_by_key(|p| p.x);
59
60    // Sweep top to bottom.
61    for row in input.min_y..=input.max_y {
62        // Left to right.
63        for (j, &p) in points.iter().enumerate() {
64            // Manhattan distance is the absolute difference in y coordinates since the x
65            // coordinate is already identical.
66            let m1 = (p.y - row).abs();
67            let x1 = p.x;
68
69            loop {
70                if let Some((i, m0, x0)) = candidates.pop() {
71                    // Compare against the head of the stack.
72                    let delta = m1 - m0;
73                    let width = x1 - x0;
74
75                    if delta < -width {
76                        // Left coordinate is further away at every points.
77                        // Discard and pop next left coordinate from the stack.
78                        //
79                        //    rrrrrrrrrrrrrrrr     <-- Considering only this row
80                        //    ....R...........
81                        //    ................
82                        //    ................
83                        //    ..L.............
84                        continue;
85                    } else if delta == -width {
86                        // Left coordinate is equal from its center leftwards
87                        // Replace with special marker value.
88                        //
89                        //    ...rrrrrrrrrrrrr
90                        //    ....R...........
91                        //    ................
92                        //    ..L.............
93                        candidates.push((marker, m0, x0));
94                        candidates.push((j, m1, x1));
95                    } else if delta == width {
96                        // Right coordinate is equal from its center rightwards.
97                        // Replace with special marker value.
98                        //
99                        //    llll............
100                        //    ..L.............
101                        //    ................
102                        //    ....R...........
103                        candidates.push((i, m0, x0));
104                        candidates.push((marker, m1, x1));
105                    } else if delta > width {
106                        // Right coordinate is further away at every point.
107                        // Discard then check next right coordinate from points.
108                        //
109                        //    llllllllllllllll
110                        //    ..L.............
111                        //    ................
112                        //    ................
113                        //    ....R...........
114                        candidates.push((i, m0, x0));
115                    } else {
116                        // Coordinates split the distance, some points closer to left and others
117                        // closer to right. Add both to candidates.
118                        //
119                        //    lllll.rrrrrrrrrr
120                        //    .........R......
121                        //    ..L.............
122                        //    ................
123                        //    ................
124                        candidates.push((i, m0, x0));
125                        candidates.push((j, m1, x1));
126                    }
127                } else {
128                    // Nothing on stack to compare with, push coordinate.
129                    candidates.push((j, m1, x1));
130                }
131
132                break;
133            }
134        }
135
136        // Any coordinates that are closest to the bounding box edges are infinite.
137        let left = candidates[0].0;
138        if left != marker {
139            finite[left] = false;
140        }
141
142        let right = candidates[candidates.len() - 1].0;
143        if right != marker {
144            finite[right] = false;
145        }
146
147        // Only consider finite coordinates.
148        for window in candidates.windows(3) {
149            let (_, m0, x0) = window[0];
150            let (i, m1, x1) = window[1];
151            let (_, m2, x2) = window[2];
152
153            // Skip coordinates where all points are equally distant from their neighbor.
154            if i != marker {
155                if row == input.min_y || row == input.max_y {
156                    // All coordinates the are closest to the top or bottom row are infinite.
157                    finite[i] = false;
158                } else {
159                    // Count points closest to the left, to the right and the coordinate itself.
160                    let left = (x1 - x0 + m0 - m1 - 1) / 2;
161                    let right = (x2 - x1 + m2 - m1 - 1) / 2;
162                    area[i] += left + 1 + right;
163                }
164            }
165        }
166
167        candidates.clear();
168    }
169
170    // Find largest area closest to finite coordinate.
171    (0..points.len()).filter_map(|i| finite[i].then_some(area[i])).max().unwrap()
172}
173
174pub fn part2(input: &Input) -> i32 {
175    part2_testable(input, 10_000)
176}
177
178/// Sweep from top to bottom to find the size of the roughly circular area that is less than
179/// a specified maximum distance from all other points.
180///
181/// Finding the center of this circle to act as a starting point is an interesting sub-problem.
182/// The two dimensional [geometric median](https://en.wikipedia.org/wiki/Geometric_median) that
183/// minimizes the Euclidean distance to all other points has no general closed form formula.
184/// The [centroid](https://en.wikipedia.org/wiki/Centroid) is close but not exact as it minimizes
185/// the distance *squared*.
186///
187/// However the Manhattan distance is independent for each axis, so we can instead solve for the
188/// one dimensional case. This is the [median](https://en.wikipedia.org/wiki/Median) of each axis.
189/// Intuitively this makes sense, as the median has the same number of points on either side,
190/// so moving either direction, the increase from half the points is cancelled out by the decrease
191/// of the other half of the points.
192///
193/// The algorithm is:
194/// * Find center
195/// * Go upwards from center until top edge of circle reached.
196/// * For each row of circle, find left and right extents
197/// * Add area of row to total, then advance to row below.
198pub fn part2_testable(input: &Input, max_distance: i32) -> i32 {
199    // Sort points in ascending order in order to find median.
200    let mut xs: Vec<_> = input.points.iter().map(|p| p.x).collect();
201    xs.sort_unstable();
202
203    let mut ys: Vec<_> = input.points.iter().map(|p| p.y).collect();
204    ys.sort_unstable();
205
206    // Find coordinate closest to median point.
207    let x = xs[xs.len() / 2];
208    let mut y = ys[ys.len() / 2];
209
210    // Calculate minimum distance.
211    let median = Point::new(x, y);
212    let mut y_distance: i32 = input.points.iter().map(|o| o.manhattan(median)).sum();
213
214    // Find top of region
215    while y_distance + prev(&ys, y) < max_distance {
216        y_distance += prev(&ys, y);
217        y -= 1;
218    }
219
220    let mut left = x;
221    let mut left_dist = y_distance;
222    let mut right = x;
223    let mut right_dist = y_distance;
224    let mut area = 0;
225
226    // Sweep top to bottom.
227    while y_distance < max_distance {
228        // Expand moving left edge to the left
229        while left_dist < max_distance {
230            left_dist += prev(&xs, left);
231            left -= 1;
232        }
233        // Contract moving left edge to the right
234        while left_dist >= max_distance {
235            left_dist += next(&xs, left);
236            left += 1;
237        }
238        // Expand moving right edge to the right
239        while right_dist < max_distance {
240            right_dist += next(&xs, right);
241            right += 1;
242        }
243        // Contract moving right edge to the left
244        while right_dist >= max_distance {
245            right_dist += prev(&xs, right);
246            right -= 1;
247        }
248
249        // Move downwards one row.
250        let next = next(&ys, y);
251        y_distance += next;
252        left_dist += next;
253        right_dist += next;
254
255        y += 1;
256        area += right - left + 1;
257    }
258
259    area
260}
261
262/// Calculate the change in distance moving left or up.
263fn prev(slice: &[i32], n: i32) -> i32 {
264    let mut total = 0;
265
266    for &s in slice {
267        if s >= n {
268            total += 1;
269        } else {
270            total -= 1;
271        }
272    }
273
274    total
275}
276
277/// Calculate the change in distance moving down or right.
278fn next(slice: &[i32], n: i32) -> i32 {
279    let mut total = 0;
280
281    for &s in slice {
282        if s <= n {
283            total += 1;
284        } else {
285            total -= 1;
286        }
287    }
288
289    total
290}