diff --git a/2024/day19/Cargo.lock b/2024/day19/Cargo.lock new file mode 100644 index 0000000..a892a08 --- /dev/null +++ b/2024/day19/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "day19" +version = "0.1.0" diff --git a/2024/day19/Cargo.toml b/2024/day19/Cargo.toml new file mode 100644 index 0000000..27216fa --- /dev/null +++ b/2024/day19/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "day19" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/2024/day19/README.txt b/2024/day19/README.txt new file mode 100644 index 0000000..dfbd362 --- /dev/null +++ b/2024/day19/README.txt @@ -0,0 +1,25 @@ +# Solution Day 19 + +The full solutions are getting rarer and rarer. Today's topic was building strings from a fixed set of substrings. + +## Task 1 + +The initial solution to task 1 stopped after finding one solution (see the commented out code for `is_pattern_possible(...)`). +My main idea was checking, whether a substring (towel) started the pattern and then recursively check whether the remaining +pattern could also be formed. If so, the pattern would be valid. + +Initially I ran into an endless loop with one of my inputs. Reason being: the recursive check lead to a situation, where the +implementation would check the same invalid suffix again and again, every time finding a way to start it, but not finish it. +I could solve this by tracking the known bad suffixes in a separate `HashMap` and short-circuiting before entering the +recursive loop again. + +## Task 2 + +Task 2 looks like a slight extension of Task 1 (just count all combinations instead of stopping at the first), but it proved +to be much harder initially. With my approach of tracking the known bad suffixes, I simply could not prevent the endless +loop even at the first input. Even now I'm not 100% sure, why. I'm not even sure, whether this is really an endless loop or just +very, very inefficient checking the same suffix again and again. + +After some more thought I decided to switch to a memoization technique: for each string store the number of known solutions and +short-circuit when you already know the solution. This did the trick and I got my solution. This way I'm even able to express +Task 1 in terms of the solver for Task 2: a valid pattern is any pattern, where the number of possible combinations is greater 0. \ No newline at end of file diff --git a/2024/day19/src/main.rs b/2024/day19/src/main.rs new file mode 100644 index 0000000..ee2468e --- /dev/null +++ b/2024/day19/src/main.rs @@ -0,0 +1,100 @@ +use std::collections::{HashMap, HashSet}; + +fn load_input() -> (Vec, Vec) { + let mut data = include_str!("../input.txt").lines(); + + let mut available_towels: Vec = data + .next() + .unwrap() + .split(", ") + .map(|s| s.trim().to_string()) + .filter(|s| s.len() > 0) + .collect(); + available_towels.sort_by_key(|s| 100 - s.len()); + + data.next(); + + let desired_patterns: Vec = data.map(|s| s.to_string()).collect(); + + (available_towels, desired_patterns) +} + +// fn is_pattern_possible( +// desired_pattern: &str, +// available_towels: &Vec, +// known_bad: &mut HashSet, // known bad patterns that cannot be built +// ) -> bool { +// if known_bad.contains(desired_pattern) { +// return false; +// } +// for towel in available_towels { +// if desired_pattern.starts_with(towel) { +// if desired_pattern.len() == towel.len() { +// return true; // complete match, we found something +// } + +// if is_pattern_possible(&desired_pattern[towel.len()..], available_towels, known_bad) { +// return true; +// } +// } +// } +// known_bad.insert(desired_pattern.to_string()); +// return false; +// } + +fn find_possible_solutions( + desired_pattern: &str, + available_towels: &Vec, + known_solutions: &mut HashMap, +) -> usize { + if let Some(known) = known_solutions.get(desired_pattern) { + return *known; + } + + let mut possible_solutions = 0; + for towel in available_towels { + if desired_pattern.starts_with(towel) { + if desired_pattern.len() == towel.len() { + // exact match, count and continue + possible_solutions += 1; + } else { + possible_solutions += find_possible_solutions( + &desired_pattern[towel.len()..], + &available_towels, + known_solutions, + ); + } + } + } + + known_solutions.insert(desired_pattern.to_string(), possible_solutions); + possible_solutions +} + +fn task1() { + let (available_towels, desired_patterns) = load_input(); + + let total_possible_patterns = desired_patterns + .iter() + .filter(|&pattern| { + find_possible_solutions(&pattern, &available_towels, &mut HashMap::new()) > 0 + }) + .count(); + + println!("Task 1: possible patterns: {total_possible_patterns}"); +} + +fn task2() { + let (available_towels, desired_patterns) = load_input(); + + let total_possible_combinations = desired_patterns.iter().fold(0, |existing, pattern| { + existing + find_possible_solutions(&pattern, &available_towels, &mut HashMap::new()) + }); + + println!("Task 2: possible combinations: {total_possible_combinations}"); +} + +fn main() { + task1(); + task2(); +}