mirror of
https://github.com/RussellChamp/advent-of-code-2025-cursor.git
synced 2026-10-27 15:45:03 +00:00
Add Day 12 solution (ft. an unused DLX library)
Turns out two simple heuristics solve the whole problem in 4ms: 1. Does the area fit? No → invalid 2. Are there enough 3x3 spaces for all presents? Yes → valid But hey, at least we learned how to use Dancing Links for a problem that didn't need it. The dlx crate thanks us for the download stats.
This commit is contained in:
1 parent
ac4208f893
commit
d8a67cf515
4 files changed
+378
No files matched your search
Generated
+17
@@ -23,6 +23,7 @@ version = "0.12.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dhat",
|
||||
"dlx",
|
||||
"itertools",
|
||||
"pico-args",
|
||||
"rayon",
|
||||
@@ -45,6 +46,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
@@ -154,6 +161,16 @@ dependencies = [
|
||||
"thousands",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dlx"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52b53f18c5d4079f2e73765df0516f171cc4d7291a3fdc3a5cbb815178255c1a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
|
||||
@@ -24,6 +24,7 @@ test_lib = []
|
||||
# Template dependencies
|
||||
chrono = { version = "0.4.38", optional = true }
|
||||
dhat = { version = "0.3.3", optional = true }
|
||||
dlx = "0.8.5"
|
||||
itertools = "0.14.0"
|
||||
pico-args = "0.5.0"
|
||||
rayon = "1.11.0"
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
0:
|
||||
###
|
||||
##.
|
||||
##.
|
||||
|
||||
1:
|
||||
###
|
||||
##.
|
||||
.##
|
||||
|
||||
2:
|
||||
.##
|
||||
###
|
||||
##.
|
||||
|
||||
3:
|
||||
##.
|
||||
###
|
||||
##.
|
||||
|
||||
4:
|
||||
###
|
||||
#..
|
||||
###
|
||||
|
||||
5:
|
||||
###
|
||||
.#.
|
||||
###
|
||||
|
||||
4x4: 0 0 0 0 2 0
|
||||
12x5: 1 0 1 0 2 2
|
||||
12x5: 1 0 1 0 3 2
|
||||
|
||||
+326
@@ -0,0 +1,326 @@
|
||||
advent_of_code::solution!(12);
|
||||
|
||||
use std::collections::HashSet;
|
||||
use dlx::choose::{mrv_chooser, no_tiebreak, prefer_any};
|
||||
use dlx::x::{INodes, ONodes, Problem};
|
||||
use dlx::{OptOrder, Solver, Uint};
|
||||
|
||||
/// Represents a shape as a set of (row, col) offsets from the top-left corner
|
||||
type Shape = Vec<(i32, i32)>;
|
||||
|
||||
/// Parse a shape from its visual representation
|
||||
fn parse_shape(lines: &[&str]) -> Shape {
|
||||
let mut cells = Vec::new();
|
||||
for (row, line) in lines.iter().enumerate() {
|
||||
for (col, ch) in line.chars().enumerate() {
|
||||
if ch == '#' {
|
||||
cells.push((row as i32, col as i32));
|
||||
}
|
||||
}
|
||||
}
|
||||
cells
|
||||
}
|
||||
|
||||
/// Normalize a shape so its minimum row and col are 0
|
||||
fn normalize(shape: &Shape) -> Shape {
|
||||
if shape.is_empty() {
|
||||
return shape.clone();
|
||||
}
|
||||
let min_row = shape.iter().map(|&(r, _)| r).min().unwrap();
|
||||
let min_col = shape.iter().map(|&(_, c)| c).min().unwrap();
|
||||
shape.iter().map(|&(r, c)| (r - min_row, c - min_col)).collect()
|
||||
}
|
||||
|
||||
/// Rotate a shape 90 degrees clockwise
|
||||
fn rotate(shape: &Shape) -> Shape {
|
||||
// (r, c) -> (c, -r)
|
||||
normalize(&shape.iter().map(|&(r, c)| (c, -r)).collect())
|
||||
}
|
||||
|
||||
/// Flip a shape horizontally
|
||||
fn flip(shape: &Shape) -> Shape {
|
||||
// (r, c) -> (r, -c)
|
||||
normalize(&shape.iter().map(|&(r, c)| (r, -c)).collect())
|
||||
}
|
||||
|
||||
/// Get all unique orientations (rotations and flips) of a shape
|
||||
fn all_orientations(shape: &Shape) -> Vec<Shape> {
|
||||
let mut orientations: Vec<Shape> = Vec::new();
|
||||
let mut seen: HashSet<Vec<(i32, i32)>> = HashSet::new();
|
||||
|
||||
let mut current = normalize(shape);
|
||||
|
||||
// Try all 4 rotations
|
||||
for _ in 0..4 {
|
||||
let mut sorted = current.clone();
|
||||
sorted.sort();
|
||||
if seen.insert(sorted) {
|
||||
orientations.push(current.clone());
|
||||
}
|
||||
current = rotate(¤t);
|
||||
}
|
||||
|
||||
// Flip and try all 4 rotations
|
||||
current = flip(shape);
|
||||
for _ in 0..4 {
|
||||
let mut sorted = current.clone();
|
||||
sorted.sort();
|
||||
if seen.insert(sorted) {
|
||||
orientations.push(current.clone());
|
||||
}
|
||||
current = rotate(¤t);
|
||||
}
|
||||
|
||||
orientations
|
||||
}
|
||||
|
||||
/// Parse the input into shapes and regions
|
||||
fn parse_input(input: &str) -> (Vec<Vec<Shape>>, Vec<(usize, usize, Vec<usize>)>) {
|
||||
let mut shapes: Vec<Vec<Shape>> = Vec::new();
|
||||
let mut regions: Vec<(usize, usize, Vec<usize>)> = Vec::new();
|
||||
|
||||
let mut current_shape_lines: Vec<&str> = Vec::new();
|
||||
let mut in_shape = false;
|
||||
|
||||
for line in input.lines() {
|
||||
let line = line.trim();
|
||||
|
||||
if line.is_empty() {
|
||||
// Finalize current shape if any
|
||||
if !current_shape_lines.is_empty() {
|
||||
let base_shape = parse_shape(¤t_shape_lines);
|
||||
shapes.push(all_orientations(&base_shape));
|
||||
current_shape_lines.clear();
|
||||
}
|
||||
in_shape = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is a shape header (e.g., "0:", "1:", etc.)
|
||||
if line.ends_with(':') && line.len() <= 3 && line.chars().next().unwrap().is_ascii_digit() {
|
||||
// Finalize previous shape if any
|
||||
if !current_shape_lines.is_empty() {
|
||||
let base_shape = parse_shape(¤t_shape_lines);
|
||||
shapes.push(all_orientations(&base_shape));
|
||||
current_shape_lines.clear();
|
||||
}
|
||||
in_shape = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is a region line (e.g., "38x36: 46 32 39 35 27 29")
|
||||
if line.contains('x') && line.contains(':') {
|
||||
let parts: Vec<&str> = line.split(':').collect();
|
||||
if parts.len() == 2 && parts[0].contains('x') {
|
||||
let dims: Vec<usize> = parts[0]
|
||||
.split('x')
|
||||
.filter_map(|s| s.trim().parse().ok())
|
||||
.collect();
|
||||
|
||||
if dims.len() == 2 {
|
||||
let quantities: Vec<usize> = parts[1]
|
||||
.split_whitespace()
|
||||
.filter_map(|s| s.parse().ok())
|
||||
.collect();
|
||||
|
||||
regions.push((dims[0], dims[1], quantities));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, it's part of a shape definition
|
||||
if in_shape {
|
||||
current_shape_lines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Finalize last shape if any
|
||||
if !current_shape_lines.is_empty() {
|
||||
let base_shape = parse_shape(¤t_shape_lines);
|
||||
shapes.push(all_orientations(&base_shape));
|
||||
}
|
||||
|
||||
(shapes, regions)
|
||||
}
|
||||
|
||||
/// Solve using Dancing Links (DLX) algorithm
|
||||
/// Returns true if all pieces can be placed in the grid
|
||||
fn can_fit_dlx(shapes: &[Vec<Shape>], width: usize, height: usize, quantities: &[usize]) -> bool {
|
||||
// Quick area check first
|
||||
let total_cells: usize = quantities
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &qty)| {
|
||||
if i < shapes.len() && !shapes[i].is_empty() {
|
||||
qty * shapes[i][0].len()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
})
|
||||
.sum();
|
||||
|
||||
let area = width * height;
|
||||
if total_cells > area {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Count total pieces to place
|
||||
let total_pieces: usize = quantities.iter().sum();
|
||||
if total_pieces == 0 {
|
||||
return true;
|
||||
}
|
||||
|
||||
// DLX formulation:
|
||||
// - Primary items (0..total_pieces): one for each piece copy we must place
|
||||
// - Secondary items (total_pieces..total_pieces+area): grid cells (can be covered 0 or 1 times)
|
||||
let num_primary = total_pieces;
|
||||
let num_secondary = area;
|
||||
let num_items = num_primary + num_secondary;
|
||||
|
||||
// Build piece copy indices for each shape type
|
||||
// piece_starts[i] = starting index in primary items for shape i
|
||||
let mut piece_starts: Vec<usize> = Vec::with_capacity(quantities.len() + 1);
|
||||
let mut idx = 0;
|
||||
for &qty in quantities {
|
||||
piece_starts.push(idx);
|
||||
idx += qty;
|
||||
}
|
||||
piece_starts.push(idx);
|
||||
|
||||
// Generate all options (placements)
|
||||
let mut options: Vec<Vec<Uint>> = Vec::new();
|
||||
|
||||
for (shape_idx, orientations) in shapes.iter().enumerate() {
|
||||
if shape_idx >= quantities.len() || quantities[shape_idx] == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let qty = quantities[shape_idx];
|
||||
let piece_start = piece_starts[shape_idx];
|
||||
|
||||
for orientation in orientations {
|
||||
// Try all positions
|
||||
for row in 0..height {
|
||||
for col in 0..width {
|
||||
// Check if this placement is valid (all cells within bounds)
|
||||
let mut cells: Vec<Uint> = Vec::new();
|
||||
let mut valid = true;
|
||||
|
||||
for &(dr, dc) in orientation {
|
||||
let r = row as i32 + dr;
|
||||
let c = col as i32 + dc;
|
||||
|
||||
if r < 0 || r >= height as i32 || c < 0 || c >= width as i32 {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Cell index in secondary items
|
||||
let cell_idx = num_primary + (r as usize) * width + (c as usize);
|
||||
cells.push(Uint(cell_idx as u64));
|
||||
}
|
||||
|
||||
if !valid {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create options for each copy of this piece type
|
||||
// Sort cells for consistent ordering
|
||||
cells.sort();
|
||||
|
||||
for copy in 0..qty {
|
||||
let piece_item = Uint((piece_start + copy) as u64);
|
||||
let mut option = vec![piece_item];
|
||||
option.extend(cells.iter().cloned());
|
||||
options.push(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if options.is_empty() {
|
||||
return total_pieces == 0;
|
||||
}
|
||||
|
||||
// Create DLX problem
|
||||
// INodes::new(np, ns) where np = primary items, ns = secondary items
|
||||
let items = INodes::new(Uint(num_primary as u64), Uint(num_secondary as u64));
|
||||
// ONodes::new(n, np, ...) where n = total items, np = number of options
|
||||
let opts = ONodes::new(Uint(num_items as u64), Uint(options.len() as u64), &options, OptOrder::Seq);
|
||||
let mut problem = Problem::new(items, opts);
|
||||
let mut solver = Solver::new(&mut problem);
|
||||
let mut chooser = mrv_chooser(prefer_any(), no_tiebreak());
|
||||
|
||||
// Find any solution
|
||||
solver.next_solution(&mut chooser)
|
||||
}
|
||||
|
||||
pub fn part_one(input: &str) -> Option<u64> {
|
||||
let (shapes, regions) = parse_input(input);
|
||||
|
||||
let mut count = 0;
|
||||
|
||||
for (_idx, (width, height, quantities)) in regions.iter().enumerate() {
|
||||
// Calculate total cells needed by all presents
|
||||
let total_cells: usize = quantities
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &qty)| {
|
||||
if i < shapes.len() && !shapes[i].is_empty() {
|
||||
qty * shapes[i][0].len()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
})
|
||||
.sum();
|
||||
|
||||
let area = width * height;
|
||||
|
||||
// Heuristic 1: If total cells needed > area available, region is INVALID
|
||||
if total_cells > area {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Count total number of presents
|
||||
let total_presents: usize = quantities.iter().sum();
|
||||
|
||||
// Heuristic 2: If we have >= as many non-overlapping 3x3 spaces as presents, region is VALID
|
||||
// Since all shapes fit in a 3x3 bounding box, we can definitely place them
|
||||
let num_3x3_spaces = (width / 3) * (height / 3);
|
||||
if num_3x3_spaces >= total_presents {
|
||||
count += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// For edge cases, run DLX to verify
|
||||
if can_fit_dlx(&shapes, *width, *height, quantities) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Some(count as u64)
|
||||
}
|
||||
|
||||
pub fn part_two(input: &str) -> Option<u64> {
|
||||
let _ = input;
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_part_one() {
|
||||
let result = part_one(&advent_of_code::template::read_file("examples", DAY));
|
||||
assert_eq!(result, Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_part_two() {
|
||||
let result = part_two(&advent_of_code::template::read_file("examples", DAY));
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user