Add solution for "Day 9: Movie Theater", part 2
This commit is contained in:
Generated
+7
@@ -6,6 +6,7 @@ version = 4
|
||||
name = "advent_of_code_2025"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"grapherity",
|
||||
"grid",
|
||||
"memchr",
|
||||
@@ -29,6 +30,12 @@ version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "0.1.10"
|
||||
|
||||
@@ -4,6 +4,7 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
bitflags = "2.13.0"
|
||||
grapherity = "0.2.1"
|
||||
grid = "1.0.0"
|
||||
memchr = "2.7.6"
|
||||
|
||||
@@ -70,6 +70,12 @@ When adding any new junction box, we either take connect it via the shortest edg
|
||||
|
||||
:mag_right: Puzzle: <https://adventofcode.com/2025/day/9>, :white_check_mark: Solver: [`MovieTheater`](src/solvers/movie_theater.rs)
|
||||
|
||||
The solver first collects all red tiles with info on which quadrants of each are inside the green-tiled shape, and collects the green-tiles lines that define the edge of the shape. The length of the longest of these edges is tracked as a lower bound for the solution.
|
||||
|
||||
For part 1, it then calculates areas of the rectangles spanned by each pair of red tiles that are not neighbors, and finds the largest.
|
||||
|
||||
For part 2, additional checks are included for a spanned rectangle to be considered. An interior quadrant of both red tiles has to face the other red tile, and no green-tiled edge crosses the interior of the spanned rectangle.
|
||||
|
||||
## Tests
|
||||
|
||||
The package contains integration tests for each solver, and unit tests for some, to help troubleshoot issues and prevent regressions. These tests cover the solutions for provided examples and full data inputs. The solutions used within the full data tests are user-specific.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use bitflags::bitflags;
|
||||
use std::ops;
|
||||
use std::str::FromStr;
|
||||
|
||||
@@ -19,11 +20,34 @@ impl Point2 {
|
||||
pub const FORWARD_DOWN_DIRECTIONS: &'static [Point2] =
|
||||
&[Point2(-1, 1), Point2(0, 1), Point2(1, 1), Point2(1, 0)];
|
||||
|
||||
pub const ZERO: Point2 = Point2(0, 0);
|
||||
|
||||
const POINT_SPLIT_CHAR: char = ',';
|
||||
|
||||
pub fn rect_area(&self, other: &Self) -> u64 {
|
||||
(self.0.abs_diff(other.0) + 1) * (self.1.abs_diff(other.1) + 1)
|
||||
}
|
||||
|
||||
// Determines the quadrants of self, in which the other point is in. Possible results are either a single quadrant,
|
||||
// two adjacent quadrants if self and other have either identical x or y coordinate, or all four quadrants if self
|
||||
// and other are identical.
|
||||
pub fn quadrants(&self, other: &Self) -> Quadrant {
|
||||
if self == other {
|
||||
return Quadrant::all();
|
||||
}
|
||||
let mut q = Quadrant::empty();
|
||||
if self.0 <= other.0 && self.1 <= other.1 {
|
||||
q |= Quadrant::BottomRight;
|
||||
} else if self.0 >= other.0 && self.1 >= other.1 {
|
||||
q |= Quadrant::TopLeft;
|
||||
}
|
||||
if self.0 <= other.0 && self.1 >= other.1 {
|
||||
q |= Quadrant::TopRight;
|
||||
} else if self.0 >= other.0 && self.1 <= other.1 {
|
||||
q |= Quadrant::BottomLeft;
|
||||
}
|
||||
q
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Point2 {
|
||||
@@ -47,6 +71,14 @@ impl ops::Add<&Self> for Point2 {
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Sub<&Self> for Point2 {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, rhs: &Self) -> Self {
|
||||
Self(self.0 - rhs.0, self.1 - rhs.1)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Hash, PartialEq, Eq, Debug)]
|
||||
pub struct Point3(pub i64, pub i64, pub i64);
|
||||
|
||||
@@ -75,5 +107,27 @@ impl FromStr for Point3 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct Rect {
|
||||
pub min: Point2,
|
||||
pub max: Point2,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Quadrant: u8 {
|
||||
const TopRight = 1;
|
||||
const BottomRight = 1 << 1;
|
||||
const BottomLeft = 1 << 2;
|
||||
const TopLeft = 1 << 3;
|
||||
}
|
||||
}
|
||||
|
||||
impl Quadrant {
|
||||
pub fn mirror(&self) -> Quadrant {
|
||||
Quadrant::from_bits_truncate((self.bits() << 2) | (self.bits() >> 2))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct ParsePointError;
|
||||
|
||||
+181
-18
@@ -1,33 +1,182 @@
|
||||
use crate::common::geometry::Point2;
|
||||
use crate::common::geometry::{Point2, Quadrant, Rect};
|
||||
use crate::solvers::Solver;
|
||||
use std::cmp;
|
||||
use std::io::BufRead;
|
||||
|
||||
pub struct MovieTheater {}
|
||||
|
||||
impl MovieTheater {
|
||||
// Collects all red tile coordinates.
|
||||
fn red_tiles<R: BufRead>(reader: R) -> Vec<Point2> {
|
||||
const DEFAULT_INTERIOR: TurnDirection = TurnDirection::Right;
|
||||
|
||||
// Collects all red tiles and green tile lines. Returns a tuple of red tiles, green tile lines, the interior
|
||||
// direction, and a lower bound for the largest rectangle area.
|
||||
fn tiles_and_lines<R: BufRead>(reader: R) -> (Vec<RedTile>, Vec<Rect>, u64) {
|
||||
let mut red_tiles = Vec::new();
|
||||
let mut tile_lines = Vec::new();
|
||||
let mut direction = Point2::ZERO;
|
||||
let mut turns = 0;
|
||||
let mut len = 0;
|
||||
let mut area = 0;
|
||||
for line in reader.lines().map_while(Result::ok) {
|
||||
red_tiles.push(line.parse::<Point2>().unwrap());
|
||||
red_tiles.push(RedTile {
|
||||
position: line.parse::<Point2>().unwrap(),
|
||||
turn: TurnDirection::Unknown,
|
||||
interior: Quadrant::empty(),
|
||||
});
|
||||
len += 1;
|
||||
if len > 1 {
|
||||
let (left, right) = red_tiles.split_at_mut(len - 1);
|
||||
let r1 = left.last_mut().unwrap();
|
||||
let r2 = &right[0];
|
||||
Self::update_red_tile_turn(r1, r2, &mut direction, &mut turns);
|
||||
Self::add_tile_line(&mut tile_lines, r1, r2, &direction, &mut area);
|
||||
}
|
||||
red_tiles
|
||||
}
|
||||
|
||||
// Maps the closest pairs of junction boxes to their distances, up to the maximum number of
|
||||
// connections.
|
||||
fn largest_rectangle(red_tiles: &Vec<Point2>) -> u64 {
|
||||
let mut max = 0;
|
||||
for i in 0..red_tiles.len() {
|
||||
let (_, right) = red_tiles.split_at(i + 1);
|
||||
for b in right {
|
||||
let a = red_tiles[i].rect_area(b);
|
||||
if max < a {
|
||||
max = a;
|
||||
if len > 1 {
|
||||
// Handles the last green tile line and the turn direction of the last red tile.
|
||||
let (left, right) = red_tiles.split_at_mut(1);
|
||||
let r1 = right.last_mut().unwrap();
|
||||
let r2 = &left[0];
|
||||
Self::update_red_tile_turn(r1, r2, &mut direction, &mut turns);
|
||||
Self::add_tile_line(&mut tile_lines, r1, r2, &direction, &mut area);
|
||||
|
||||
// Handles the turn direction of the first red tile.
|
||||
let (left, right) = red_tiles.split_at_mut(1);
|
||||
let r1 = left.last_mut().unwrap();
|
||||
let r2 = &right[0];
|
||||
Self::update_red_tile_turn(r1, r2, &mut direction, &mut turns);
|
||||
}
|
||||
|
||||
let interior = if turns > 0 {
|
||||
TurnDirection::Left
|
||||
} else {
|
||||
TurnDirection::Right
|
||||
};
|
||||
Self::correct_red_tiles_interior(&mut red_tiles, interior);
|
||||
(red_tiles, tile_lines, area)
|
||||
}
|
||||
|
||||
// Updates turn and interior information for the previous red tile, and updates the direction.
|
||||
fn update_red_tile_turn(
|
||||
r1: &mut RedTile,
|
||||
r2: &RedTile,
|
||||
direction: &mut Point2,
|
||||
turns: &mut i32,
|
||||
) {
|
||||
let previous_direction = direction.clone();
|
||||
*direction = r2.position - &r1.position;
|
||||
// Calculate the turn direction of the previous red tile. This works without normalization because
|
||||
// consecutive directions are always perpendicular with exactly one non-zero coordinate.
|
||||
r1.turn = if previous_direction.0 * direction.1 < previous_direction.1 * direction.0 {
|
||||
*turns += 1;
|
||||
TurnDirection::Left
|
||||
} else {
|
||||
*turns -= 1;
|
||||
TurnDirection::Right
|
||||
};
|
||||
|
||||
// Sets the interior quadrants, assuming that the interior is on the right (see DEFAULT_INTERIOR). If the
|
||||
// interior is later determined to be on the left, the quadrants will be inverted.
|
||||
r1.interior = Quadrant::empty();
|
||||
if 0 < previous_direction.0 {
|
||||
r1.interior |= Quadrant::BottomLeft;
|
||||
if r1.turn == TurnDirection::Left {
|
||||
r1.interior |= Quadrant::BottomRight | Quadrant::TopRight;
|
||||
}
|
||||
} else if previous_direction.0 < 0 {
|
||||
r1.interior |= Quadrant::TopRight;
|
||||
if r1.turn == TurnDirection::Left {
|
||||
r1.interior |= Quadrant::TopLeft | Quadrant::BottomLeft;
|
||||
}
|
||||
} else if 0 < previous_direction.1 {
|
||||
r1.interior |= Quadrant::TopLeft;
|
||||
if r1.turn == TurnDirection::Left {
|
||||
r1.interior |= Quadrant::BottomLeft | Quadrant::BottomRight;
|
||||
}
|
||||
} else if previous_direction.1 < 0 {
|
||||
r1.interior |= Quadrant::BottomRight;
|
||||
if r1.turn == TurnDirection::Left {
|
||||
r1.interior |= Quadrant::TopRight | Quadrant::TopLeft;
|
||||
}
|
||||
}
|
||||
}
|
||||
max
|
||||
|
||||
fn add_tile_line(
|
||||
tile_lines: &mut Vec<Rect>,
|
||||
r1: &RedTile,
|
||||
r2: &RedTile,
|
||||
direction: &Point2,
|
||||
area: &mut u64,
|
||||
) {
|
||||
let (min, max) = if (r1.position.0 < r2.position.0) || (r1.position.1 < r2.position.1) {
|
||||
(r1, r2)
|
||||
} else {
|
||||
(r2, r1)
|
||||
};
|
||||
let tile_line = Rect {
|
||||
min: min.position,
|
||||
max: max.position,
|
||||
};
|
||||
*area = cmp::max(
|
||||
*area,
|
||||
u64::try_from((direction.0 + direction.1).abs() + 1).expect("area must be positive"),
|
||||
);
|
||||
tile_lines.push(tile_line);
|
||||
}
|
||||
|
||||
fn correct_red_tiles_interior(red_tiles: &mut [RedTile], interior: TurnDirection) {
|
||||
if interior != Self::DEFAULT_INTERIOR {
|
||||
for r in red_tiles.iter_mut() {
|
||||
r.interior = !r.interior;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn largest_rectangles(
|
||||
red_tiles: &[RedTile],
|
||||
tile_lines: &[Rect],
|
||||
init_area: u64,
|
||||
) -> (u64, u64) {
|
||||
let mut part1 = init_area;
|
||||
let mut part2 = init_area;
|
||||
// Loops over all pairs of red tiles red_tiles[i] and b, except for neighbors. A rectangle of two neighbors
|
||||
// would be the line of green tiles between them, which was already considered in init_area.
|
||||
for i in 0..red_tiles.len() - 1 {
|
||||
let (left, right) = red_tiles.split_at(i + 2);
|
||||
let a = &left[i];
|
||||
'b_loop: for b in right {
|
||||
let area = a.position.rect_area(&b.position);
|
||||
part1 = cmp::max(part1, area);
|
||||
|
||||
// Part 2. Checks whether the area would actually be an improvement over the current max.
|
||||
if part2 >= area {
|
||||
continue 'b_loop;
|
||||
}
|
||||
// Checks whether the local neighborhoods of two corners of the rect are within the green-tiled shape.
|
||||
let q = a.position.quadrants(&b.position);
|
||||
if (a.interior & q).is_empty() || (b.interior & q.mirror()).is_empty() {
|
||||
continue 'b_loop;
|
||||
}
|
||||
// Checks whether any edge intersects the rectangle.
|
||||
for tile_line in tile_lines {
|
||||
if Self::intersects(tile_line, &a.position, &b.position) {
|
||||
continue 'b_loop;
|
||||
}
|
||||
}
|
||||
part2 = area;
|
||||
}
|
||||
}
|
||||
(part1, part2)
|
||||
}
|
||||
|
||||
fn intersects(tile_line: &Rect, a: &Point2, b: &Point2) -> bool {
|
||||
let (min0, max0) = if a.0 <= b.0 { (a.0, b.0) } else { (b.0, a.0) };
|
||||
let (min1, max1) = if a.1 <= b.1 { (a.1, b.1) } else { (b.1, a.1) };
|
||||
min0 < tile_line.max.0
|
||||
&& tile_line.min.0 < max0
|
||||
&& min1 < tile_line.max.1
|
||||
&& tile_line.min.1 < max1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +185,21 @@ impl Solver for MovieTheater {
|
||||
const PUZZLE_NAME: &'static str = "Movie Theater";
|
||||
|
||||
fn process_data<R: BufRead>(&self, reader: R) -> (u64, u64) {
|
||||
let red_tiles = Self::red_tiles(reader);
|
||||
(Self::largest_rectangle(&red_tiles), 0)
|
||||
let (red_tiles, tile_lines, area) = Self::tiles_and_lines(reader);
|
||||
Self::largest_rectangles(&red_tiles, &tile_lines, area)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum TurnDirection {
|
||||
Unknown,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RedTile {
|
||||
position: Point2,
|
||||
turn: TurnDirection,
|
||||
interior: Quadrant,
|
||||
}
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ fn playground() {
|
||||
#[test]
|
||||
fn movie_theater() {
|
||||
assert_eq!(
|
||||
Ok((50, 0)),
|
||||
Ok((50, 24)),
|
||||
solvers::run_solver(solvers::movie_theater::MovieTheater {}, &EXAMPLE_PATHS)
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ fn playground() {
|
||||
#[test]
|
||||
fn movie_theater() {
|
||||
assert_eq!(
|
||||
Ok((4781235324, 0)),
|
||||
Ok((4781235324, 1566935900)),
|
||||
solvers::run_solver(
|
||||
solvers::movie_theater::MovieTheater {},
|
||||
&solvers::DATA_PATHS
|
||||
|
||||
Reference in New Issue
Block a user