From 37476b9111d3af0757baa9574fd82adc0ec22d64 Mon Sep 17 00:00:00 2001 From: Ben Rampling Date: Mon, 15 Dec 2025 07:30:45 +0100 Subject: [PATCH] Solve day 9 part 2 using a sweep line algorithm --- src/year2025/day09.rs | 185 ++++++++++++++++++++++++++++-------------- 1 file changed, 125 insertions(+), 60 deletions(-) diff --git a/src/year2025/day09.rs b/src/year2025/day09.rs index 48ce975..c272225 100644 --- a/src/year2025/day09.rs +++ b/src/year2025/day09.rs @@ -1,13 +1,5 @@ -use crate::util::grid::*; -use crate::util::hash::*; use crate::util::iter::*; use crate::util::parse::*; -use crate::util::point::*; -use std::collections::VecDeque; - -const OUTSIDE: i64 = 0; -const INSIDE: i64 = 1; -const UNKNOWN: i64 = 2; type Tile = [u64; 2]; @@ -30,75 +22,148 @@ pub fn part1(tiles: &[Tile]) -> u64 { } pub fn part2(tiles: &[Tile]) -> u64 { - let size = tiles.len(); - let shrink_x = shrink(tiles, 0); - let shrink_y = shrink(tiles, 1); - let shrunk: Vec<_> = tiles.iter().map(|&[x, y]| (shrink_x[&x], shrink_y[&y])).collect(); + let mut tiles = tiles.to_vec(); - let mut area = 0; - let mut todo = VecDeque::from([ORIGIN]); - let mut grid = Grid::new(shrink_x.len() as i32, shrink_y.len() as i32, UNKNOWN); + tiles.sort_unstable_by_key(|&[x, y]| (y, x)); + + let tiles = tiles; + + // Track the largest area so far during scanning: + let mut largest_area: u64 = 0; - for i in 0..size { - let (x1, y1, x2, y2) = minmax(shrunk[i], shrunk[(i + 1) % size]); + // Each red tile (`x`, `y`) becomes a candidate for being a top corner of the largest area, and during the + // scan the `interval` containing the maximum possible width is updated: + struct Candidate { + x: u64, + y: u64, + interval: Interval, + } + + let mut candidates: Vec = Vec::with_capacity(512); + + // Maintain an ordered list of descending edges, i.e. [begin_interval_0, end_interval_0, begin_interval_1, end_interval_1, ...]: + let mut descending_edges: Vec = vec![]; + let mut intervals_from_descending_edges = vec![]; + + // Invariants on the input data (defined by the puzzle) result in points arriving in pairs on the same y line: + let mut it = tiles.into_iter(); + + while let (Some([x0, y]), Some([x1, y1])) = (it.next(), it.next()) { + debug_assert_eq!(y, y1); + + // Update the descending edges; since we are scanning from top to bottom, and within each line left to right, + // when we, starting from outside of the region, hit a corner tile it is either: + // + // - The corner of two edges, one going right and one going down. In this case, the `descending_edges` won't contain + // the `x` coordinate, and we should "toggle" it on to denote that there is a new descending edge. + // - The corner of two edges, one going right and one going up. The `descending_edges` will contain an `x` coordinate, + // that should be "toggled" off. + // + // Simular arguments work for when we are scanning inside the edge and we hit the corner that ends the edge; this is also + // why corners always arrive in pairs. + // + // Do the update: + for x in [x0, x1] { + toggle_value_membership_in_ordered_list(&mut descending_edges, x); + } - for x in x1..x2 + 1 { - for y in y1..y2 + 1 { - grid[Point::new(x, y)] = INSIDE; + // Every pair of descending edges in the ordered list defines a region; find the resulting intervals on this line: + update_intervals_from_descending_edges( + &descending_edges, + &mut intervals_from_descending_edges, + ); + + // Check the rectangles this red tile could be a bottom tile for, with the current candidates: + for candidate in candidates.iter() { + for x in [x0, x1] { + if candidate.interval.contains(x) { + largest_area = largest_area + .max((candidate.x.abs_diff(x) + 1) * (candidate.y.abs_diff(y) + 1)); + } } } - } - while let Some(point) = todo.pop_front() { - for next in ORTHOGONAL.map(|o| point + o) { - if grid.contains(next) && grid[next] == UNKNOWN { - grid[next] = OUTSIDE; - todo.push_back(next); + // Update candidates when their interval shrinks due to descending edge changes, and drop them when their interval becomes empty: + candidates.retain_mut(|candidate| { + if let Some(intersection_containing_x) = + intervals_from_descending_edges.iter().find(|i| i.contains(candidate.x)) + { + candidate.interval = intersection_containing_x.intersection(candidate.interval); + + true + } else { + false + } + }); + + // Add any new candidates: + for x in [x0, x1] { + if let Some(&containing) = + intervals_from_descending_edges.iter().find(|i| i.contains(x)) + { + candidates.push(Candidate { x, y, interval: containing }); } } } - for y in 1..grid.height { - for x in 1..grid.width { - let point = Point::new(x, y); - let value = i64::from(grid[point] != OUTSIDE); - grid[point] = value + grid[point + UP] + grid[point + LEFT] - grid[point + UP + LEFT]; - } + largest_area +} + +/// The set { x in u64 | l <= x <= r }. +#[derive(Clone, Copy)] +struct Interval { + l: u64, + r: u64, +} + +impl Interval { + fn new(l: u64, r: u64) -> Self { + debug_assert!(l <= r); + + Interval { l, r } } - for i in 0..size { - for j in i + 1..size { - let (x1, y1, x2, y2) = minmax(shrunk[i], shrunk[j]); - - let expected = (x2 - x1 + 1) as i64 * (y2 - y1 + 1) as i64; - let actual = grid[Point::new(x2, y2)] - - grid[Point::new(x1 - 1, y2)] - - grid[Point::new(x2, y1 - 1)] - + grid[Point::new(x1 - 1, y1 - 1)]; - - if expected == actual { - let [x1, y1] = tiles[i]; - let [x2, y2] = tiles[j]; - let dx = x1.abs_diff(x2) + 1; - let dy = y1.abs_diff(y2) + 1; - area = area.max(dx * dy); - } - } + fn intersects(self, other: Self) -> bool { + other.l <= self.r && self.l <= other.r } - area + fn intersection(self, other: Self) -> Self { + debug_assert!(self.intersects(other)); + + Interval::new(self.l.max(other.l), self.r.min(other.r)) + } + + fn contains(self, x: u64) -> bool { + self.l <= x && x <= self.r + } } -fn shrink(tiles: &[Tile], index: usize) -> FastMap { - let mut axis: Vec<_> = tiles.iter().map(|tile| tile[index]).collect(); - axis.push(u64::MIN); - axis.push(u64::MAX); - axis.sort_unstable(); - axis.dedup(); - axis.iter().enumerate().map(|(i, &n)| (n, i as i32)).collect() +// Adds `value` if it isn't in `ordered_list`, removes it if it is, maintaining the order. +fn toggle_value_membership_in_ordered_list(ordered_list: &mut Vec, value: u64) { + let mut i = 0; + + while i < ordered_list.len() && ordered_list[i] < value { + i += 1; + } + + if i == ordered_list.len() || ordered_list[i] != value { + ordered_list.insert(i, value); + } else { + ordered_list.remove(i); + } } +// Changes the list of descending edges, [begin_interval_0, end_interval_0, begin_interval_1, end_interval_1, ...], +// into a vector containing the intervals. #[inline] -fn minmax((x1, y1): (i32, i32), (x2, y2): (i32, i32)) -> (i32, i32, i32, i32) { - (x1.min(x2), y1.min(y2), x1.max(x2), y1.max(y2)) +fn update_intervals_from_descending_edges(descending_edges: &[u64], to_update: &mut Vec) { + debug_assert!(descending_edges.len().is_multiple_of(2)); + + to_update.clear(); + + let mut it = descending_edges.iter(); + + while let (Some(&l), Some(&r)) = (it.next(), it.next()) { + to_update.push(Interval::new(l, r)); + } }