From e291f48c84db4b25deef18e428c57de9b519d781 Mon Sep 17 00:00:00 2001 From: Markus Brueckner Date: Wed, 11 Dec 2024 20:02:09 +0100 Subject: [PATCH] Day 11 --- 2024/day11/Cargo.lock | 61 +++++++++++++++++++++++++++++++++ 2024/day11/Cargo.toml | 7 ++++ 2024/day11/README.md | 26 ++++++++++++++ 2024/day11/src/main.rs | 78 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 172 insertions(+) create mode 100644 2024/day11/Cargo.lock create mode 100644 2024/day11/Cargo.toml create mode 100644 2024/day11/README.md create mode 100644 2024/day11/src/main.rs diff --git a/2024/day11/Cargo.lock b/2024/day11/Cargo.lock new file mode 100644 index 0000000..1b8f98c --- /dev/null +++ b/2024/day11/Cargo.lock @@ -0,0 +1,61 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "day11" +version = "0.1.0" +dependencies = [ + "rayon", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] diff --git a/2024/day11/Cargo.toml b/2024/day11/Cargo.toml new file mode 100644 index 0000000..458e621 --- /dev/null +++ b/2024/day11/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "day11" +version = "0.1.0" +edition = "2021" + +[dependencies] +rayon = "1.10.0" diff --git a/2024/day11/README.md b/2024/day11/README.md new file mode 100644 index 0000000..1f7f4ee --- /dev/null +++ b/2024/day11/README.md @@ -0,0 +1,26 @@ +# Solution Day 11 + +This one was a tricky one. I actually needed a bit of inspiration. The initial idea was rather straightforward: implement the simulation rules as a single step, simulate +them x times for all stones in the input and count the number of stones in the end. But... + +## Task 1 + +This one is easy. Implement the simple approach outlined above, get the result. Nothing special, because it's "just" 25 rounds, so we're good. My original implementation +was actually done based on the string values, because two of the rules are easy enough this was: comparing with `"0"` is just as easy as using a numerical value, +splitting the number of even length is even easier. The only issue is correctly handling the leading `0`, so in the end I did just do everything with `u64`, only +converting to string in order to split (which could technically also have been done with clever use of `log10()`, `/` and `%`, but I'm lazy). + +## Task 2 + +This is where it all came crashing down. I do have a _very_ beefy laptop with 32 GB of RAM and still my naive approach was killed by the OOM killer after significant +amounts of runtime. Since the stones can be simulated in isolation, we don't actually have to store all of the results, but rather can just count them, so I rewrote +the algorithm to work recursivly, calculating the count of a stone depth first. This means, that we will never have to store the actual stones (we'll just hand them +into the recursive call), which keeps the memory under control. It doesn't solve the runtime issue, though. In the iterative program I had printed the round index +at some point and that made it abundantly clear, that this program would probably run the whole night. + +Here is where I needed a bit of inspiration. I read through a few of the posts under the hashtag #adventofcode on the Fediverse and someone mentioned memoisation issues +they were having. So, a cache it is then? I implemented a simple in-memory cache, that maps `(stone, run number)` => `count in the end`. In order to keep the cache +size under control, I decided to go with `stone`s < 1000 for a start (that would at most give us ~75,000 entries in the cache, which should be OK). Well, turns out that +this is plenty. The program finished so quickly, that I initially thought I had made a mistake with the cache and didn't actually want to try the result. Much to my +surprise (I honestly let out a little scream of surprise, much to my wife's surprise in turn), the result was correct. The small cache is so efficient, that the +calculation runs way, way below a second on my laptop. Someone smarter than me can probably calculate the cache hit rate and why this thing is so efficient. \ No newline at end of file diff --git a/2024/day11/src/main.rs b/2024/day11/src/main.rs new file mode 100644 index 0000000..dbfc31e --- /dev/null +++ b/2024/day11/src/main.rs @@ -0,0 +1,78 @@ +use std::collections::HashMap; + +fn load_input() -> Vec { + std::fs::read_to_string("./input.txt") + .expect("Read file failed") + .trim() + .split(" ") + .filter(|s| s.len() > 0) + .map(|s| s.parse().unwrap()) + .collect() +} + +fn cache_value(cache: &mut HashMap<(u64, u32), u64>, stone: u64, run: u32, count: u64) { + if stone < 1000 { + cache.insert((stone, run), count); + } +} + +fn count(stone: u64, run: u32, cache: &mut HashMap<(u64, u32), u64>) -> u64 { + if cache.contains_key(&(stone, run)) { + return cache.get(&(stone, run)).unwrap().clone(); + } + + if run == 0 { + return 1; + } + + if stone == 0 { + let c = count(1, run - 1, cache); + cache_value(cache, stone, run, c); + return c; + } + + let val_str = stone.to_string(); + if val_str.len() % 2 == 0 { + let c = count( + val_str[0..val_str.len() / 2].parse::().unwrap(), + run - 1, + cache, + ) + count( + val_str[val_str.len() / 2..].parse::().unwrap(), + run - 1, + cache, + ); + cache_value(cache, stone, run, c); + return c; + } + let c = count(stone * 2024, run - 1, cache); + cache_value(cache, stone, run, c); + return c; +} + +fn task1() { + let stones = load_input(); + + let mut cache: HashMap<(u64, u32), u64> = HashMap::new(); + let count = stones + .iter() + .fold(0, |sum, &stone| sum + count(stone, 25, &mut cache)); + + println!("Task 1: # stones: {}", count); +} + +fn task2() { + let stones = load_input(); + + let mut cache: HashMap<(u64, u32), u64> = HashMap::new(); + let count = stones + .iter() + .fold(0, |sum, &stone| sum + count(stone, 75, &mut cache)); + + println!("Task 2: # stones: {}", count); +} + +fn main() { + task1(); + task2(); +}