aoc/year2025/day09.rs
1//! # Movie Theater
2use crate::util::iter::*;
3use crate::util::parse::*;
4
5type Tile = [u32; 2];
6
7struct Candidate {
8 x: u32,
9 y: u32,
10 interval: Interval,
11}
12
13/// The set { x in u32 | l <= x <= r }.
14#[derive(Clone, Copy)]
15struct Interval {
16 l: u32,
17 r: u32,
18}
19
20impl Interval {
21 fn new(l: u32, r: u32) -> Self {
22 debug_assert!(l <= r);
23
24 Interval { l, r }
25 }
26
27 fn intersects(self, other: Self) -> bool {
28 other.l <= self.r && self.l <= other.r
29 }
30
31 fn intersection(self, other: Self) -> Self {
32 debug_assert!(self.intersects(other));
33
34 Interval::new(self.l.max(other.l), self.r.min(other.r))
35 }
36
37 fn contains(self, x: u32) -> bool {
38 self.l <= x && x <= self.r
39 }
40}
41
42pub fn parse(input: &str) -> Vec<Tile> {
43 let mut tiles: Vec<_> = input.iter_unsigned::<u32>().chunk::<2>().collect();
44 tiles.sort_unstable_by_key(|&[x, y]| (y, x));
45 tiles
46}
47
48pub fn part1(tiles: &[Tile]) -> u64 {
49 let (top_left_tiles, top_right_tiles) = get_potential_left_corner_tiles(tiles.iter().copied());
50 let (bottom_left_tiles, bottom_right_tiles) =
51 get_potential_left_corner_tiles(tiles.iter().copied().rev());
52
53 find_largest_from_all_corners(&top_left_tiles, &bottom_right_tiles, true)
54 .max(find_largest_from_all_corners(&bottom_left_tiles, &top_right_tiles, false))
55}
56
57/// This function filters `sorted_tiles` into two lists, one containing all tiles that could be the top left
58/// corner of the largest rectangle (assuming the largest rectangle has a top left corner), and the second
59/// containing all tiles that could be the top right corner.
60///
61/// It assumes `sorted_tiles` is sorted in ascending "y" values, or, to get the top right and bottom right corners,
62/// that `sorted_tiles` is sorted in descending "y" order.
63///
64/// It works (for the top left corners, for illustration) by only returning tiles (from the set of all tiles, "T") within
65/// the region:
66///
67/// R = { (x, y) ∈ ℝ² : ∀ (tx, ty) ∈ T, tx ≤ x ⇒ ty ≥ y }
68///
69/// Tiles outside of this region cannot possibly be a corner of the largest rectangle. Assume, for proof by contradiction,
70/// that the top left corner of the largest rectangle is in the complement of the set "R":
71///
72/// R' = { (x, y) ∈ ℝ² : ¬ (∀ (tx, ty) ∈ T, tx ≤ x ⇒ ty ≥ y) }
73/// = { (x, y) ∈ ℝ² : ∃ (tx, ty) ∈ T, tx ≤ x ∧ ty < y }
74///
75/// That is, for the corner (x, y), there exists another tile (tx, ty) that is to the left and above the corner tile, which
76/// means the tile isn't the corner of the largest possible rectangle, completing the proof by contradiction.
77///
78/// The `top_tiles` and `bottom_tiles` are the corner points of this region `R`, built up by scanning through tiles
79/// in either left to right or right to left order.
80///
81/// With just this selection of candidate edge points, the number of points that have to be
82/// compared is already reduced compared to a naive quadratic pairing of all original points.
83/// But exploiting the relationships we just proved above, we can further reduce the comparisons
84/// to O(n log n) by repeatedly picking the mid-point of `top_tiles`, finding which corresponding
85/// point in `bottom_tiles` forms the best rectangle, and then recursively checking just two of the
86/// four combinations of the sublists remaining on either side of the pivots.
87/// [This post](https://codeforces.com/blog/entry/128350) goes more into the theory.
88fn get_potential_left_corner_tiles(
89 sorted_tiles: impl Iterator<Item = [u32; 2]>,
90) -> (Vec<[u32; 2]>, Vec<[u32; 2]>) {
91 let mut left_tiles = Vec::new();
92 let mut left_tiles_last_x = u32::MAX;
93
94 let mut right_tiles = Vec::new();
95 let mut right_tiles_last_x = u32::MIN;
96
97 let mut it = sorted_tiles.peekable();
98
99 while let Some(first_in_row) = it.next() {
100 let mut last_in_row = first_in_row;
101
102 while let Some(p) = it.next_if(|p| p[1] == first_in_row[1]) {
103 last_in_row = p;
104 }
105
106 let (y, left_x, right_x) = (
107 first_in_row[1],
108 first_in_row[0].min(last_in_row[0]),
109 first_in_row[0].max(last_in_row[0]),
110 );
111
112 if left_x < left_tiles_last_x {
113 left_tiles.push([left_x, y]);
114 left_tiles_last_x = left_x;
115 }
116
117 if right_x > right_tiles_last_x {
118 right_tiles.push([right_x, y]);
119 right_tiles_last_x = right_x;
120 }
121 }
122
123 right_tiles.reverse();
124 (left_tiles, right_tiles)
125}
126
127#[inline]
128fn find_largest_from_all_corners(
129 corner: &[[u32; 2]],
130 opposite_corner: &[[u32; 2]],
131 top_left: bool,
132) -> u64 {
133 // Helper struct for a work queue of remaining pairings that need to be checked.
134 struct Work {
135 p_lo: usize,
136 p_hi: usize,
137 q_lo: usize,
138 q_hi: usize,
139 }
140
141 fn addrange(work: &mut Vec<Work>, p_lo: usize, p_hi: usize, q_lo: usize, q_hi: usize) {
142 if p_lo <= p_hi && q_lo <= q_hi {
143 let job = Work { p_lo, p_hi, q_lo, q_hi };
144 work.push(job);
145 }
146 }
147
148 // Instead of performing an O(n^2) pairing of every point between the two sets, we can
149 // divide and conquer for O(n log n) work by repeatedly dividing the set corner against
150 // the partitions of opposite_corner that correspond to the best result from the halfway
151 // point of corner.
152 let mut largest = 0_u64;
153 let start = Work { p_lo: 0, p_hi: corner.len() - 1, q_lo: 0, q_hi: opposite_corner.len() - 1 };
154 let mut work = vec![start];
155
156 while let Some(job) = work.pop() {
157 // For a given point in corner, sweep the points in opposite_corner to find the
158 // partition point for the best rectangle on the sweep.
159 let p_mid = usize::midpoint(job.p_lo, job.p_hi);
160 let p = corner[p_mid];
161 let mut best_i = None;
162 let mut maxsize = 0_u64;
163 let mut q_lim = job.q_lo;
164
165 for (q_i, q) in opposite_corner.iter().enumerate().take(job.q_hi + 1).skip(job.q_lo) {
166 if p[0] > q[0] {
167 q_lim = q_i;
168 } else if (p[1] < q[1]) == top_left {
169 let size = (p[0].abs_diff(q[0]) + 1) as u64 * (p[1].abs_diff(q[1]) + 1) as u64;
170 if size > maxsize {
171 maxsize = size;
172 best_i = Some(q_i);
173 }
174 }
175 }
176
177 // The sweep determined how to partition smaller searches on the left and right halves.
178 if let Some(i) = best_i {
179 largest = largest.max(maxsize);
180 if p_mid > 0 {
181 addrange(&mut work, job.p_lo, p_mid - 1, job.q_lo, i);
182 }
183 addrange(&mut work, p_mid + 1, job.p_hi, i, job.q_hi);
184 } else {
185 if p_mid > 0 && q_lim > 0 {
186 addrange(&mut work, job.p_lo, p_mid - 1, job.q_lo, q_lim - 1);
187 }
188 addrange(&mut work, p_mid + 1, job.p_hi, q_lim, job.q_hi);
189 }
190 }
191
192 largest
193}
194
195pub fn part2(tiles: &[Tile]) -> u64 {
196 // Track the largest area so far during scanning.
197 let mut largest_area: u64 = 0;
198
199 // Each red tile (`x`, `y`) becomes a candidate for being a top corner of the largest area, and during the
200 // scan, the `interval` containing the maximum possible width is updated.
201 let mut candidates: Vec<Candidate> = Vec::with_capacity(512);
202
203 // Maintain an ordered list of descending edges, i.e. [begin_interval_0, end_interval_0, begin_interval_1, end_interval_1, ...].
204 let mut descending_edges: Vec<u32> = vec![];
205 let mut intervals_from_descending_edges = vec![];
206
207 // Invariants on the input data (defined by the puzzle) result in points arriving in pairs on the same y line.
208 for [&[x0, y], &[x1, y1]] in tiles.iter().chunk::<2>() {
209 debug_assert_eq!(y, y1);
210
211 // Update the descending edges. Since we are scanning from top to bottom, and within each line left to right,
212 // when we, starting from outside of the region, hit a corner tile it is either:
213 //
214 // - The corner of two edges, one going right and one going down. In this case, the `descending_edges` won't contain
215 // the `x` coordinate, and we should "toggle" it on to denote that there is a new descending edge.
216 // - The corner of two edges, one going right and one going up. The `descending_edges` will contain an `x` coordinate
217 // that should be "toggled" off.
218 //
219 // Similar arguments work for when we are scanning inside the edge and we hit the corner that ends the edge. This is also
220 // why corners always arrive in pairs.
221 //
222 // Do the update.
223 for x in [x0, x1] {
224 toggle_value_membership_in_ordered_list(&mut descending_edges, x);
225 }
226
227 // Every pair of descending edges in the ordered list defines a region. Find the resulting intervals on this line.
228 update_intervals_from_descending_edges(
229 &descending_edges,
230 &mut intervals_from_descending_edges,
231 );
232
233 // Check the rectangles this red tile could be a bottom tile for, with the current candidates.
234 for candidate in &candidates {
235 for x in [x0, x1] {
236 if candidate.interval.contains(x) {
237 largest_area = largest_area.max(
238 (candidate.x.abs_diff(x) + 1) as u64 * (candidate.y.abs_diff(y) + 1) as u64,
239 );
240 }
241 }
242 }
243
244 // Update candidates when their interval shrinks due to descending edge changes, and drop them when their interval becomes empty.
245 candidates.retain_mut(|candidate| {
246 if let Some(intersection_containing_x) =
247 intervals_from_descending_edges.iter().find(|i| i.contains(candidate.x))
248 {
249 candidate.interval = intersection_containing_x.intersection(candidate.interval);
250
251 true
252 } else {
253 false
254 }
255 });
256
257 // Add any new candidates.
258 for x in [x0, x1] {
259 if let Some(&containing) =
260 intervals_from_descending_edges.iter().find(|i| i.contains(x))
261 {
262 candidates.push(Candidate { x, y, interval: containing });
263 }
264 }
265 }
266
267 largest_area
268}
269
270// Adds `value` if it isn't in `ordered_list`, removes it if it is, maintaining the order.
271fn toggle_value_membership_in_ordered_list(ordered_list: &mut Vec<u32>, value: u32) {
272 match ordered_list.binary_search(&value) {
273 Ok(i) => {
274 ordered_list.remove(i);
275 }
276 Err(i) => {
277 ordered_list.insert(i, value);
278 }
279 }
280}
281
282// Changes the list of descending edges, [begin_interval_0, end_interval_0, begin_interval_1, end_interval_1, ...],
283// into a vector containing the intervals.
284#[inline]
285fn update_intervals_from_descending_edges(descending_edges: &[u32], to_update: &mut Vec<Interval>) {
286 to_update.clear();
287 to_update.extend(descending_edges.chunks_exact(2).map(|c| Interval::new(c[0], c[1])));
288}