Skip to main content

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