Created
December 10, 2025 21:40
-
-
Save icub3d/16eea2a8b4a94d193a148fef908779a9 to your computer and use it in GitHub Desktop.
Solution for Advent of Code 2025 Day 10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| use std::{collections::VecDeque, time::Instant}; | |
| use rayon::iter::{ParallelBridge, ParallelIterator}; | |
| use rustc_hash::FxHashSet; | |
| const INPUT: &str = include_str!("inputs/day10.txt"); | |
| const EPSILON: f64 = 1e-9; | |
| fn parse(input: &str) -> impl Iterator<Item = Machine> { | |
| input.trim().lines().map(Machine::from) | |
| } | |
| #[derive(Debug)] | |
| struct Machine { | |
| lights: usize, | |
| buttons: Vec<Vec<usize>>, | |
| joltages: Vec<usize>, | |
| } | |
| impl From<&str> for Machine { | |
| fn from(value: &str) -> Self { | |
| let mut parts = value.split_whitespace(); | |
| let lights = parts | |
| .next() | |
| .map(|l| { | |
| // We rev here to make calculating below easier. | |
| l.trim_matches(['[', ']']) | |
| .chars() | |
| .rev() | |
| .fold(0, |acc, c| (acc << 1) | if c == '#' { 1 } else { 0 }) | |
| }) | |
| .unwrap(); | |
| let mut parts: Vec<&str> = parts.collect(); | |
| let joltages = parts | |
| .pop() | |
| .unwrap() | |
| .trim_matches(['{', '}']) | |
| .split(',') | |
| .map(|v| v.parse().unwrap()) | |
| .collect(); | |
| let mut buttons: Vec<Vec<usize>> = parts | |
| .iter() | |
| .map(|b| { | |
| b.trim_matches(['(', ')']) | |
| .split(',') | |
| .map(|v| v.parse().unwrap()) | |
| .collect() | |
| }) | |
| .collect(); | |
| // Sorting seems to help here. Not sure why, was just trying stuff. | |
| buttons.sort_by_key(|b| std::cmp::Reverse(b.len())); | |
| Self { | |
| lights, | |
| buttons, | |
| joltages, | |
| } | |
| } | |
| } | |
| fn p1(input: &str) -> usize { | |
| parse(input) | |
| .map(|machine| { | |
| let mut frontier = VecDeque::new(); | |
| frontier.push_back((0, 0)); | |
| let mut seen = FxHashSet::default(); | |
| seen.insert(0); | |
| while let Some((lights, dist)) = frontier.pop_front() { | |
| if lights == machine.lights { | |
| return dist; | |
| } | |
| for neighbor in machine.buttons.iter() { | |
| let neighbor = neighbor.iter().fold(lights, |acc, n| acc ^ (1 << n)); | |
| if seen.insert(neighbor) { | |
| frontier.push_back((neighbor, dist + 1)); | |
| } | |
| } | |
| } | |
| unreachable!() | |
| }) | |
| .sum() | |
| } | |
| struct Matrix { | |
| data: Vec<Vec<f64>>, | |
| rows: usize, | |
| cols: usize, | |
| dependents: Vec<usize>, | |
| independents: Vec<usize>, | |
| } | |
| impl Matrix { | |
| // Make a matrix, do a Gaussian elimination and setup the fixed and free variables. | |
| fn from_machine(machine: &Machine) -> Self { | |
| let rows = machine.joltages.len(); | |
| let cols = machine.buttons.len(); | |
| let mut data = vec![vec![0.0; cols + 1]; rows]; | |
| // Add all of our buttons. | |
| for (c, button) in machine.buttons.iter().enumerate() { | |
| for &r in button { | |
| if r < rows { | |
| data[r][c] = 1.0; | |
| } | |
| } | |
| } | |
| // Add our joltages to the last column | |
| for (r, &val) in machine.joltages.iter().enumerate() { | |
| data[r][cols] = val as f64; | |
| } | |
| let mut matrix = Self { | |
| data, | |
| rows, | |
| cols, | |
| dependents: Vec::new(), | |
| independents: Vec::new(), | |
| }; | |
| matrix.gaussian_elimination(); | |
| matrix | |
| } | |
| // https://en.wikipedia.org/wiki/Gaussian_elimination | |
| fn gaussian_elimination(&mut self) { | |
| let mut pivot = 0; | |
| let mut col = 0; | |
| while pivot < self.rows && col < self.cols { | |
| // Find the best pivot row for this column. | |
| let (best_row, best_value) = self | |
| .data | |
| .iter() | |
| .enumerate() | |
| .skip(pivot) | |
| .map(|(r, row)| (r, row[col].abs())) | |
| .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap()) | |
| .unwrap(); | |
| // If the best value is zero, this is a free variable. | |
| if best_value < EPSILON { | |
| self.independents.push(col); | |
| col += 1; | |
| continue; | |
| } | |
| // Swap rows and mark this column as dependent. | |
| self.data.swap(pivot, best_row); | |
| self.dependents.push(col); | |
| // Normalize pivot row. | |
| let pivot_value = self.data[pivot][col]; | |
| for val in &mut self.data[pivot][col..=self.cols] { | |
| *val /= pivot_value; | |
| } | |
| // Eliminate this column in all other rows. | |
| for r in 0..self.rows { | |
| if r != pivot { | |
| let factor = self.data[r][col]; | |
| if factor.abs() > EPSILON { | |
| let pivot_row = self.data[pivot][col..=self.cols].to_vec(); | |
| self.data[r][col..=self.cols] | |
| .iter_mut() | |
| .zip(&pivot_row) | |
| .for_each(|(val, &pivot_val)| { | |
| *val -= factor * pivot_val; | |
| }); | |
| } | |
| } | |
| } | |
| pivot += 1; | |
| col += 1; | |
| } | |
| // Any remaining columns are free variables | |
| self.independents.extend(col..self.cols); | |
| } | |
| // Check if the given values for our independent variables are valid. If so, return the total button presses. | |
| fn valid(&self, values: &[usize]) -> Option<usize> { | |
| // We start with how many times we've pressed the free variables. | |
| let mut total = values.iter().sum::<usize>(); | |
| // Calculate dependent variable values based on independent variables. | |
| for row in 0..self.dependents.len() { | |
| // Calculate this dependent by subtracting the sum of the free variable pushes from the solution. | |
| let val = self | |
| .independents | |
| .iter() | |
| .enumerate() | |
| .fold(self.data[row][self.cols], |acc, (i, &col)| { | |
| acc - self.data[row][col] * (values[i] as f64) | |
| }); | |
| // We need non-negative, whole numbers for a valid solution. | |
| if val < -EPSILON { | |
| return None; | |
| } | |
| let rounded = val.round(); | |
| if (val - rounded).abs() > EPSILON { | |
| return None; | |
| } | |
| total += rounded as usize; | |
| } | |
| Some(total) | |
| } | |
| } | |
| fn dfs(matrix: &Matrix, idx: usize, values: &mut [usize], min: &mut usize, max: usize) { | |
| // When we've assigned all independent variables, check if it's a valid solution. | |
| if idx == matrix.independents.len() { | |
| if let Some(total) = matrix.valid(values) { | |
| *min = (*min).min(total); | |
| } | |
| return; | |
| } | |
| // Try different values for the current independent variable. | |
| let total: usize = values[..idx].iter().sum(); | |
| for val in 0..max { | |
| // Optimization: If we ever go above our min, we can't possibly do better. | |
| if total + val >= *min { | |
| break; | |
| } | |
| values[idx] = val; | |
| dfs(matrix, idx + 1, values, min, max); | |
| } | |
| } | |
| fn p2(input: &str) -> usize { | |
| parse(input) | |
| .par_bridge() | |
| .map(|machine| { | |
| let matrix = Matrix::from_machine(&machine); | |
| // Now we can DFS over a much smaller solution space. | |
| let max = *machine.joltages.iter().max().unwrap() + 1; | |
| let mut min = usize::MAX; | |
| let mut values = vec![0; matrix.independents.len()]; | |
| dfs(&matrix, 0, &mut values, &mut min, max); | |
| min | |
| }) | |
| .sum() | |
| } | |
| fn main() { | |
| let now = Instant::now(); | |
| let solution = p1(INPUT); | |
| println!("p1 {:?} {}", now.elapsed(), solution); | |
| assert_eq!(solution, 486); | |
| let now = Instant::now(); | |
| let solution = p2(INPUT); | |
| println!("p2 {:?} {}", now.elapsed(), solution); | |
| assert_eq!(solution, 17820); | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| const INPUT: &str = include_str!("inputs/day10-sample.txt"); | |
| #[test] | |
| fn test_p1() { | |
| assert_eq!(p1(INPUT), 7); | |
| } | |
| #[test] | |
| fn test_p2() { | |
| assert_eq!(p2(INPUT), 33); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment