From b80d1098397d580372a6796f525677796f1b6658 Mon Sep 17 00:00:00 2001 From: timeshifter Date: Thu, 25 Jun 2026 19:47:28 +0200 Subject: [PATCH] solved day 19 part 2 with claude --- day19/Cargo.lock | 70 ++++++++++ day19/Cargo.toml | 1 + day19/src/main.rs | 325 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 395 insertions(+), 1 deletion(-) diff --git a/day19/Cargo.lock b/day19/Cargo.lock index 0cb741d..d072eaa 100644 --- a/day19/Cargo.lock +++ b/day19/Cargo.lock @@ -2,6 +2,76 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "day19" version = "0.1.0" +dependencies = [ + "rand", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" diff --git a/day19/Cargo.toml b/day19/Cargo.toml index b0e76b5..5995e90 100644 --- a/day19/Cargo.toml +++ b/day19/Cargo.toml @@ -4,3 +4,4 @@ version = "0.1.0" edition = "2024" [dependencies] +rand = "*" diff --git a/day19/src/main.rs b/day19/src/main.rs index e7a11a9..4e515fe 100644 --- a/day19/src/main.rs +++ b/day19/src/main.rs @@ -1,3 +1,326 @@ +use std::collections::HashSet; + +use rand::{ + prelude::ThreadRng, + seq::{IndexedRandom, SliceRandom}, +}; + fn main() { - println!("Hello, world!"); + let content = std::fs::read_to_string("input.txt").unwrap(); + + let lines = content.lines(); + + let conversions: &Vec<_> = &lines + .clone() + .take_while(|l| !l.is_empty()) + .map(|l| { + let mut iter = l.split_whitespace(); + let a = iter.next().unwrap(); + let b = iter.next_back().unwrap(); + (a, b) + }) + .collect(); + + let formula = lines.skip_while(|l| !l.is_empty()).nth(1).unwrap(); + + let mut capital_idxs: Vec<_> = formula + .chars() + .enumerate() + .flat_map(|(i, c)| c.is_uppercase().then_some(i)) + .collect(); + + capital_idxs.push(formula.len()); // otherwise we are missing the last chemical + + let formula_chars: Vec<_> = formula.chars().collect(); + + let separate: Vec<_> = capital_idxs + .iter() + .zip(capital_idxs.iter().skip(1)) + .map(|(a, b)| formula_chars[*a..*b].iter().collect::()) + .collect(); + + part1(conversions, &separate); // 576 + + main2(); +} + +// fn _part2(conversions: &[(&str, &str)], formula: &str) { +// let mut rng = rand::rng(); + +// let mut rand_conversions = conversions.to_vec(); +// let mut old_conversion_count = 10000; // large +// let mut conversion_count; + +// loop { +// conversion_count = 0; +// let mut formula_replaced = formula.to_string(); + +// loop { +// let old_formula_replaced = formula_replaced.clone(); + +// rand_conversions.shuffle(&mut rng); + +// for (short, long) in conversions { +// if formula_replaced.contains(long) { +// conversion_count += 1; +// } + +// formula_replaced = replace_any(formula_replaced, long, short, &mut rng); +// } +// if formula_replaced == old_formula_replaced { +// break; +// } +// } +// if conversion_count < old_conversion_count { +// println!("{conversion_count}"); +// old_conversion_count = conversion_count; +// } +// } +// } + +// fn replace_any(input: String, long: &str, short: &str, rng: &mut ThreadRng) -> String { +// let haystack: Vec<_> = input.chars().collect(); +// let needle: Vec<_> = long.chars().collect(); +// let short_chars: Vec<_> = short.chars().collect(); + +// let length = needle.len(); + +// let mut count = 0; + +// for i in 0..haystack.len() - length { +// if needle == haystack[i..i + length] { +// count += 1; +// } +// } + +// let possible_indices: Vec<_> = (0..count).collect(); +// let take_amount = possible_indices.choose(rng).unwrap(); + +// let idxs_chosen: Vec<_> = possible_indices +// .sample(rng, *take_amount) +// .copied() +// .collect(); + +// let mut result: Vec = Vec::new(); +// let mut idx = 0; +// let mut i = 0; +// loop { +// if needle == haystack[i..i + length] { +// if idxs_chosen.contains(&idx) { +// for (j, c) in short_chars.iter().enumerate() { +// result[j] = *c; +// } +// } +// idx += 1; +// } else { +// result.push(haystack[i]); +// } + +// todo!() +// } + +// haystack.into_iter().collect() +// } + +fn part1(conversions: &[(&str, &str)], formula: &[String]) { + let mut results = HashSet::new(); + + for (i, chemical) in formula.iter().enumerate() { + for (old_molecule, new_molecule) in conversions { + if chemical.as_str() == *old_molecule { + let mut new_chemical = formula.to_vec(); + + new_chemical[i] = new_molecule.to_string(); + + let new_chemical_string = new_chemical + .into_iter() + .reduce(|mut a, b| { + a.push_str(&b); + a + }) + .unwrap(); + + results.insert(new_chemical_string); + } + } + } + println!("{}", results.len()); +} + +// ------------------------------------------------------------------------------------ + +use std::fs; + +/// A single replacement rule as it appears in the puzzle input: `from => to`. +struct Rule { + from: String, + to: String, +} + +/// Parse the puzzle input into (rules, medicine molecule). +fn parse_input(input: &str) -> (Vec, String) { + let mut rules = Vec::new(); + let mut molecule = String::new(); + + for line in input.lines() { + let line = line.trim(); + if line.is_empty() { + continue; + } + if let Some((from, to)) = line.split_once(" => ") { + rules.push(Rule { + from: from.to_string(), + to: to.to_string(), + }); + } else { + // The one line with no " => " is the medicine molecule. + molecule = line.to_string(); + } + } + + (rules, molecule) +} + +/// Very small xorshift-based PRNG so we don't need an external crate just to +/// shuffle the rule order between restart attempts. +struct Rng(u64); + +impl Rng { + fn new(seed: u64) -> Self { + // Avoid a zero seed, which would make xorshift degenerate. + Rng(seed | 1) + } + + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + /// Random index in [0, n). + fn gen_range(&mut self, n: usize) -> usize { + (self.next_u64() % n as u64) as usize + } + + fn shuffle(&mut self, slice: &mut [T]) { + // Fisher-Yates shuffle. + for i in (1..slice.len()).rev() { + let j = self.gen_range(i + 1); + slice.swap(i, j); + } + } +} + +/// Try a single greedy-reduction pass: starting from `molecule`, repeatedly +/// replace the *first* substring match (scanning rules in the given order, +/// and within the string left-to-right) of some rule's `to` with its `from`, +/// counting each replacement as one step, until either: +/// - the molecule becomes exactly "e" (success), or +/// - no rule matches anywhere in the current molecule (stuck). +/// +/// Returns `Some(steps)` on success, `None` if it got stuck. +fn try_greedy_reduce(molecule: &str, rules: &[&Rule]) -> Option { + let mut current = molecule.to_string(); + let mut steps = 0usize; + + while current != "e" { + let mut reduced_this_round = false; + + for rule in rules { + if let Some(pos) = current.find(rule.to.as_str()) { + // Replace just the first occurrence found. + current.replace_range(pos..pos + rule.to.len(), &rule.from); + steps += 1; + reduced_this_round = true; + break; + } + } + + if !reduced_this_round { + // No rule applies anywhere; this attempt is stuck. + return None; + } + } + + Some(steps) +} + +/// Repeatedly attempt greedy reduction with randomly reshuffled rule order +/// until one attempt successfully collapses the molecule down to "e". +/// +/// This restart-on-stuck strategy works because, for these puzzle inputs, +/// *some* greedy left-to-right reduction order reaches "e" in the minimal +/// number of steps; we just need to find an order that doesn't get stuck. +fn solve_part2(rules: &[Rule], molecule: &str) -> usize { + let mut rule_refs: Vec<&Rule> = rules.iter().collect(); + let mut rng = Rng::new(0x5EED_1234_u64); + + loop { + if let Some(steps) = try_greedy_reduce(molecule, &rule_refs) { + return steps; + } + rng.shuffle(&mut rule_refs); + } +} + +fn main2() { + let input = fs::read_to_string("input.txt").expect("failed to read input.txt"); + let (rules, molecule) = parse_input(&input); + + let steps = solve_part2(&rules, &molecule); + println!("Part 2: fewest steps to fabricate the medicine = {}", steps); +} + +#[cfg(test)] +mod tests { + use super::*; + + fn rules_from_pairs(pairs: &[(&str, &str)]) -> Vec { + pairs + .iter() + .map(|(from, to)| Rule { + from: from.to_string(), + to: to.to_string(), + }) + .collect() + } + + #[test] + fn example_hoh_takes_3_steps() { + let rules = rules_from_pairs(&[ + ("e", "H"), + ("e", "O"), + ("H", "HO"), + ("H", "OH"), + ("O", "HH"), + ]); + let steps = solve_part2(&rules, "HOH"); + assert_eq!(steps, 3); + } + + #[test] + fn example_hohoho_takes_6_steps() { + let rules = rules_from_pairs(&[ + ("e", "H"), + ("e", "O"), + ("H", "HO"), + ("H", "OH"), + ("O", "HH"), + ]); + let steps = solve_part2(&rules, "HOHOHO"); + assert_eq!(steps, 6); + } + + #[test] + fn parse_input_splits_rules_and_molecule() { + let input = "e => H\ne => O\nH => HO\nH => OH\nO => HH\n\nHOH\n"; + let (rules, molecule) = parse_input(input); + assert_eq!(rules.len(), 5); + assert_eq!(molecule, "HOH"); + assert_eq!(rules[0].from, "e"); + assert_eq!(rules[0].to, "H"); + } }