Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 125 additions & 60 deletions src/year2025/day09.rs
Original file line number Diff line number Diff line change
@@ -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];

Expand All @@ -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<Candidate> = 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<u64> = 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<u64, i32> {
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<u64>, 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<Interval>) {
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));
}
}
Loading