Skip to content

Commit 415b5b5

Browse files
author
Ben Rampling
committed
Solve day 9 part 2 using a sweep line algorithm
1 parent 4ac7e43 commit 415b5b5

File tree

1 file changed

+118
-61
lines changed

1 file changed

+118
-61
lines changed

src/year2025/day09.rs

Lines changed: 118 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
1-
use crate::util::grid::*;
2-
use crate::util::hash::*;
31
use crate::util::iter::*;
42
use crate::util::parse::*;
5-
use crate::util::point::*;
6-
use std::collections::VecDeque;
7-
8-
const OUTSIDE: i64 = 0;
9-
const INSIDE: i64 = 1;
10-
const UNKNOWN: i64 = 2;
113

124
type Tile = [u64; 2];
135

@@ -30,75 +22,140 @@ pub fn part1(tiles: &[Tile]) -> u64 {
3022
}
3123

3224
pub fn part2(tiles: &[Tile]) -> u64 {
33-
let size = tiles.len();
34-
let shrink_x = shrink(tiles, 0);
35-
let shrink_y = shrink(tiles, 1);
36-
let shrunk: Vec<_> = tiles.iter().map(|&[x, y]| (shrink_x[&x], shrink_y[&y])).collect();
25+
let mut tiles = tiles.to_vec();
3726

38-
let mut area = 0;
39-
let mut todo = VecDeque::from([ORIGIN]);
40-
let mut grid = Grid::new(shrink_x.len() as i32, shrink_y.len() as i32, UNKNOWN);
27+
tiles.sort_unstable_by_key(|&[x, y]| (y, x));
4128

42-
for i in 0..size {
43-
let (x1, y1, x2, y2) = minmax(shrunk[i], shrunk[(i + 1) % size]);
29+
let tiles = tiles;
4430

45-
for x in x1..x2 + 1 {
46-
for y in y1..y2 + 1 {
47-
grid[Point::new(x, y)] = INSIDE;
48-
}
49-
}
31+
// Track the largest area so far during scanning:
32+
let mut largest_area: u64 = 0;
33+
34+
// Each red tile (`x`, `y`) becomes a candidate for being a top corner of the largest area, and during the
35+
// scan the `interval` containing the maximum possible width is updated:
36+
struct Candidate {
37+
x: u64,
38+
y: u64,
39+
interval: Interval,
5040
}
5141

52-
while let Some(point) = todo.pop_front() {
53-
for next in ORTHOGONAL.map(|o| point + o) {
54-
if grid.contains(next) && grid[next] == UNKNOWN {
55-
grid[next] = OUTSIDE;
56-
todo.push_back(next);
42+
let mut candidates: Vec<Candidate> = Vec::with_capacity(512);
43+
44+
// Maintain an ordered list of descending edges, i.e. [begin_interval_0, end_interval_0, begin_interval_1, end_interval_1, ...]:
45+
let mut descending_edges: Vec<u64> = vec![];
46+
let mut intervals_from_descending_edges = vec![];
47+
48+
// Invariants on the input data (defined by the puzzle) result in points arriving in pairs on the same y line:
49+
let mut it = tiles.into_iter();
50+
51+
while let (Some([x0, y]), Some([x1, y1])) = (it.next(), it.next()) {
52+
debug_assert_eq!(y, y1);
53+
54+
// Update the descending edges; since we are scanning from top to bottom, and within each line left to right,
55+
// when we, starting from outside of the region, hit a corner tile it is either:
56+
//
57+
// - The corner of two edges, one going right and one going down. In this case, the `descending_edges` won't contain
58+
// the `x` coordinate, and we should "toggle" it on to denote that there is a new descending edge.
59+
// - The corner of two edges, one going right and one going up. The `descending_edges` will contain an `x` coordinate,
60+
// that should be "toggled" off.
61+
//
62+
// Simular arguments work for when we are scanning inside the edge and we hit the corner that ends the edge; this is also
63+
// why corners always arrive in pairs.
64+
//
65+
// Do the update:
66+
for x in [x0, x1] {
67+
toggle_value_membership_in_ordered_list(&mut descending_edges, x);
68+
}
69+
70+
// Every pair of descending edges in the ordered list defines a region; find the resulting intervals on this line:
71+
update_intervals_from_descending_edges(&descending_edges, &mut intervals_from_descending_edges);
72+
73+
// Check the rectangles this red tile could be a bottom tile for, with the current candidates:
74+
for candidate in candidates.iter() {
75+
for x in [x0, x1] {
76+
if candidate.interval.contains(x) {
77+
largest_area = largest_area.max((candidate.x.abs_diff(x) + 1) * (candidate.y.abs_diff(y) + 1));
78+
}
79+
}
80+
}
81+
82+
// Update candidates when their interval shrinks due to descending edge changes, and drop them when their interval becomes empty:
83+
candidates.retain_mut(|candidate| {
84+
if let Some(intersection_containing_x) = intervals_from_descending_edges.iter().find(|i| i.contains(candidate.x)) {
85+
candidate.interval = intersection_containing_x.intersection(candidate.interval);
86+
87+
true
88+
} else {
89+
false
90+
}
91+
});
92+
93+
// Add any new candidates:
94+
for x in [x0, x1] {
95+
if let Some(&containing) = intervals_from_descending_edges.iter().find(|i| i.contains(x)) {
96+
candidates.push(Candidate { x, y, interval: containing });
5797
}
5898
}
5999
}
60100

61-
for y in 1..grid.height {
62-
for x in 1..grid.width {
63-
let point = Point::new(x, y);
64-
let value = i64::from(grid[point] != OUTSIDE);
65-
grid[point] = value + grid[point + UP] + grid[point + LEFT] - grid[point + UP + LEFT];
66-
}
101+
largest_area
102+
}
103+
104+
/// The set { x in u64 | l <= x <= r }.
105+
#[derive(Clone, Copy)]
106+
struct Interval {
107+
l: u64,
108+
r: u64,
109+
}
110+
111+
impl Interval {
112+
fn new(l: u64, r: u64) -> Self {
113+
debug_assert!(l <= r);
114+
115+
Interval { l, r }
67116
}
68117

69-
for i in 0..size {
70-
for j in i + 1..size {
71-
let (x1, y1, x2, y2) = minmax(shrunk[i], shrunk[j]);
72-
73-
let expected = (x2 - x1 + 1) as i64 * (y2 - y1 + 1) as i64;
74-
let actual = grid[Point::new(x2, y2)]
75-
- grid[Point::new(x1 - 1, y2)]
76-
- grid[Point::new(x2, y1 - 1)]
77-
+ grid[Point::new(x1 - 1, y1 - 1)];
78-
79-
if expected == actual {
80-
let [x1, y1] = tiles[i];
81-
let [x2, y2] = tiles[j];
82-
let dx = x1.abs_diff(x2) + 1;
83-
let dy = y1.abs_diff(y2) + 1;
84-
area = area.max(dx * dy);
85-
}
86-
}
118+
fn intersects(self, other: Self) -> bool {
119+
other.l <= self.r && self.l <= other.r
87120
}
88121

89-
area
122+
fn intersection(self, other: Self) -> Self {
123+
debug_assert!(self.intersects(other));
124+
125+
Interval::new(self.l.max(other.l), self.r.min(other.r))
126+
}
127+
128+
fn contains(self, x: u64) -> bool {
129+
self.l <= x && x <= self.r
130+
}
90131
}
91132

92-
fn shrink(tiles: &[Tile], index: usize) -> FastMap<u64, i32> {
93-
let mut axis: Vec<_> = tiles.iter().map(|tile| tile[index]).collect();
94-
axis.push(u64::MIN);
95-
axis.push(u64::MAX);
96-
axis.sort_unstable();
97-
axis.dedup();
98-
axis.iter().enumerate().map(|(i, &n)| (n, i as i32)).collect()
133+
// Adds `value` if it isn't in `ordered_list`, removes it if it is, maintaining the order.
134+
fn toggle_value_membership_in_ordered_list(ordered_list: &mut Vec<u64>, value: u64) {
135+
let mut i = 0;
136+
137+
while i < ordered_list.len() && ordered_list[i] < value {
138+
i += 1;
139+
}
140+
141+
if i == ordered_list.len() || ordered_list[i] != value {
142+
ordered_list.insert(i, value);
143+
} else {
144+
ordered_list.remove(i);
145+
}
99146
}
100147

148+
// Changes the list of descending edges, [begin_interval_0, end_interval_0, begin_interval_1, end_interval_1, ...],
149+
// into a vector containing the intervals.
101150
#[inline]
102-
fn minmax((x1, y1): (i32, i32), (x2, y2): (i32, i32)) -> (i32, i32, i32, i32) {
103-
(x1.min(x2), y1.min(y2), x1.max(x2), y1.max(y2))
151+
fn update_intervals_from_descending_edges(descending_edges: &[u64], to_update: &mut Vec<Interval>) {
152+
debug_assert!(descending_edges.len().is_multiple_of(2));
153+
154+
to_update.clear();
155+
156+
let mut it = descending_edges.iter();
157+
158+
while let (Some(&l), Some(&r)) = (it.next(), it.next()) {
159+
to_update.push(Interval::new(l, r));
160+
}
104161
}

0 commit comments

Comments
 (0)