diff --git a/Cargo.lock b/Cargo.lock index 5395d3c..5bdabe1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,6 +75,7 @@ dependencies = [ "bstr", "clap", "color-eyre", + "good_lp", "humantime", "rustc-hash", ] @@ -157,6 +158,25 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +[[package]] +name = "coin_cbc" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d602045cd2e7ad02608a71492af94357f493a6f3c934ce854c03bf10fddc5780" +dependencies = [ + "coin_cbc_sys", + "lazy_static", +] + +[[package]] +name = "coin_cbc_sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085619f8bdc38e24e25c6336ecc3f2e6c0543d67566dff6daef0e32f7ac20f76" +dependencies = [ + "pkg-config", +] + [[package]] name = "color-eyre" version = "0.6.5" @@ -200,12 +220,28 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "gimli" version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "good_lp" +version = "1.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "776aa1ba88ac058e78408c17f4dbff826a51ae08ed6642f71ca0edd7fe9383f3" +dependencies = [ + "coin_cbc", + "fnv", +] + [[package]] name = "heck" version = "0.5.0" @@ -290,6 +326,12 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "proc-macro2" version = "1.0.103" diff --git a/Cargo.toml b/Cargo.toml index 962f7e2..d253cb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ arrayvec = "0.7.6" bstr = "1.11.0" clap = { version = "4.5.21", features = ["derive"] } color-eyre = "0.6.3" +good_lp = "1.14.2" humantime = "2.1.0" rustc-hash = "2.1.1" diff --git a/flake.nix b/flake.nix index 8c3b22a..aea3194 100644 --- a/flake.nix +++ b/flake.nix @@ -39,10 +39,13 @@ hyperfine cargo-flamegraph cargo-show-asm + cbc + pkg-config ]); RUST_PATH = "${rust}"; RUST_DOC_PATH = "${rust}/share/doc/rust/html/std/index.html"; AOC_YEAR = "2025"; + LD_LIBRARY_PATH="${pkgs.cbc}/lib"; }; defaultPackage = craneLib.buildPackage { diff --git a/src/bin/day10.rs b/src/bin/day10.rs new file mode 100644 index 0000000..c1c9fa6 --- /dev/null +++ b/src/bin/day10.rs @@ -0,0 +1,187 @@ +use std::{str::FromStr, time::Instant}; + +use aoc_2025::{load, print_res}; +use arrayvec::ArrayVec; +use bstr::BString; +use good_lp::{ProblemVariables, Solution, SolverModel}; + +#[derive(Debug)] +pub struct Machine { + pattern: Vec, + buttons: Vec>, + joltage: ArrayVec, +} + +type Parsed = Vec; + +#[inline(never)] +pub fn parsing(input: &BString) -> color_eyre::Result { + str::from_utf8(input)? + .lines() + .map(|line| { + let mut parts = line.split_whitespace(); + let Some(pattern) = parts.next() else { + color_eyre::eyre::bail!("Empty line supplied"); + }; + + let pattern = pattern.as_bytes(); + color_eyre::eyre::ensure!(pattern.len() >= 3); + color_eyre::eyre::ensure!(*pattern.first().unwrap() == b'['); + color_eyre::eyre::ensure!(*pattern.last().unwrap() == b']'); + + let pattern = pattern + .iter() + .skip(1) + .take(pattern.len() - 2) + .map(|&c| match c { + b'.' => Ok(false), + b'#' => Ok(true), + _ => color_eyre::eyre::bail!("Invalid pattern entry: {}", c as char), + }) + .collect::, _>>()?; + + let mut buttons = Vec::new(); + loop { + let Some(group) = parts.next() else { + color_eyre::eyre::bail!("Unexpected last group for `{line}`") + }; + + color_eyre::eyre::ensure!(group.len() >= 3); + + fn group_content(group: &str) -> color_eyre::Result + where + T: FromStr, + T::Err: std::error::Error + Send + Sync + 'static, + I: FromIterator, + { + Ok(group[1..group.len() - 1] + .split(',') + .map(str::parse) + .collect::>()?) + } + + if group.as_bytes()[0] == b'{' { + color_eyre::eyre::ensure!(*group.as_bytes().last().unwrap() == b'}'); + color_eyre::eyre::ensure!(parts.next().is_none()); + break Ok(Machine { + pattern, + buttons, + joltage: group_content(group)?, + }); + } + + color_eyre::eyre::ensure!(*group.as_bytes().first().unwrap() == b'('); + color_eyre::eyre::ensure!(*group.as_bytes().last().unwrap() == b')'); + + buttons.push(group_content(group)?); + } + }) + .collect() +} + +impl Machine { + fn start_press_count(&self) -> usize { + assert!(self.pattern.len() <= 16); + + let target = self.pattern.iter().rev().fold(0, |c, &b| c << 1 | b as u16); + let buttons: Vec<_> = self + .buttons + .iter() + .map(|b| b.iter().fold(0, |c, i| c | 1 << i)) + .collect(); + + let mut seen = vec![false; 1 << self.pattern.len()]; + seen[0] = true; + let mut patterns = vec![0u16]; + for count in 1.. { + let mut new_patterns = Vec::new(); + for pattern in patterns { + for button in &buttons { + let new = pattern ^ button; + if new == target { + return count; + } else if !seen[new as usize] { + seen[new as usize] = true; + new_patterns.push(new); + } + } + } + patterns = new_patterns; + } + + unreachable!() + } + + fn joltage_press_count(&self) -> usize { + let mut problem = ProblemVariables::new(); + + let mut presses = good_lp::Expression::with_capacity(self.buttons.len()); + let mut joltage = + vec![good_lp::Expression::with_capacity(self.buttons.len()); self.joltage.len()]; + + for button in &self.buttons { + let var = good_lp::variable() + .integer() + .min(0) + .name(format!("{button:?}")); + let press = problem.add(var); + presses += press; + + for &slot in button { + joltage[slot] += press; + } + } + + let constraints = joltage + .into_iter() + .zip(&self.joltage) + .map(|(j, &t)| j.eq(t)); + + let mut model = problem + .minimise(&presses) + .using(good_lp::default_solver) + .with_all(constraints); + + model.set_parameter("loglevel", "0"); + + let solution = model.solve().unwrap(); + + let total_presses = solution.eval(presses); + total_presses as usize + } +} + +#[inline(never)] +pub fn part1(input: Parsed) { + let total: usize = input.iter().map(Machine::start_press_count).sum(); + + print_res!("Total presses to start: {total}"); +} + +#[inline(never)] +pub fn part2(input: Parsed) { + let total: usize = input.iter().map(Machine::joltage_press_count).sum(); + + print_res!("Total presses to set joltage: {total}"); +} + +pub fn main() -> color_eyre::Result<()> { + let context = load()?; + + let start = Instant::now(); + let parsed = parsing(&context.input)?; + let elapsed = humantime::format_duration(start.elapsed()); + + let start = Instant::now(); + if context.part == 1 { + part1(parsed); + } else { + part2(parsed); + } + let elapsed_part = humantime::format_duration(start.elapsed()); + + println!(" Parsing: {elapsed}"); + println!(" Solving: {elapsed_part}"); + + Ok(()) +}