From b83e13d0bb6c3b63896acf02ad1e64c2e3ccbd12 Mon Sep 17 00:00:00 2001 From: Markus Brueckner Date: Sun, 22 Dec 2024 17:32:20 +0100 Subject: [PATCH] Day 22 --- 2024/day22/Cargo.lock | 7 ++++ 2024/day22/Cargo.toml | 6 ++++ 2024/day22/README.md | 26 ++++++++++++++ 2024/day22/src/main.rs | 81 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+) create mode 100644 2024/day22/Cargo.lock create mode 100644 2024/day22/Cargo.toml create mode 100644 2024/day22/README.md create mode 100644 2024/day22/src/main.rs diff --git a/2024/day22/Cargo.lock b/2024/day22/Cargo.lock new file mode 100644 index 0000000..e93fdda --- /dev/null +++ b/2024/day22/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "day22" +version = "0.1.0" diff --git a/2024/day22/Cargo.toml b/2024/day22/Cargo.toml new file mode 100644 index 0000000..07fb531 --- /dev/null +++ b/2024/day22/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "day22" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/2024/day22/README.md b/2024/day22/README.md new file mode 100644 index 0000000..456397f --- /dev/null +++ b/2024/day22/README.md @@ -0,0 +1,26 @@ +# Solution Day 22 + +One of the few later ones I actually did on the day. The pseudo-random number generator is easy enough to +implement. If you read correctly, that is… I read "round to the nearest integer" first and was slightly off +with my numbers, until I saw that little "down" in there, which makes the whole thing _much_ easier. You don't +actually need to round, you can just do integer division, which will cut off the fractional part, leaving you +with the already rounded-down number. + +## Task 1 + +After implementing the pseudo-random generator, it's easy enough to run it 2000x for each start number and +sum up the resulting numbers. Nothing special to see here. + +## Task 2 + +This one was more complex. My initial naïve approach was to calculate all 4-digit sub-sequences, search for them +in all sequences and sum up the assiciated price. While this would probably yield the result _at some point_, the +complexity is _O(n² * m²)_ (with _n_ being the number of starting numbers and _m_ being the sequence length for each +starting number, assuming I got my maths right). Let's just say: not fun, definitely too slow. + +My less naïve approach involves pre-calculating the first price of each sub-sequence for each starting number and +then afterwards going through them, summing them up grouped by sub-sequence and finding the maximum number of those +sums. This involves the heavy use of `HashMap`s (to keep track of the found sub-sequences and their associated number), +but brings complexity down to _ O(n * m)_ (for all three steps, actually. Pre-calculating the sequences and prices is _O(n * m)_, +calculating the sums and then finding the maximum is as well). This is then actually fast enough to finish before I +get bored. \ No newline at end of file diff --git a/2024/day22/src/main.rs b/2024/day22/src/main.rs new file mode 100644 index 0000000..1ba8557 --- /dev/null +++ b/2024/day22/src/main.rs @@ -0,0 +1,81 @@ +#![feature(map_try_insert)] + +use std::collections::{HashMap, VecDeque}; + +fn mix_and_prune(intermediate: i64, number: i64) -> i64 { + (intermediate ^ number) % 16777216 +} + +fn next_secret_number(number: i64) -> i64 { + let mut result = mix_and_prune(number * 64, number); + result = mix_and_prune(result / 32, result); + result = mix_and_prune(result * 2048, result); + result +} + +fn nth_secret_number(start: i64, n: u32) -> i64 { + let mut result = start; + for _ in 0..n { + result = next_secret_number(result); + } + result +} + +fn task1() { + let numbers = include_str!("../input.txt") + .lines() + .map(|line| line.parse::().unwrap()); + + let sum: i64 = numbers.map(|number| nth_secret_number(number, 2000)).sum(); + + println!("Task 1: sum {sum}"); +} + +fn calculate_sequence(number: i64) -> HashMap, i64> { + let mut current_number = number; + let mut result = HashMap::new(); + let mut current_pattern = VecDeque::new(); + for _ in 0..2000 { + let next_number = next_secret_number(current_number); + let next_last_digit = next_number % 10; + let current_last_digit = current_number % 10; + current_pattern.push_back(next_last_digit - current_last_digit); + if current_pattern.len() == 4 { + // we're only ever interested in the first occurence of a pattern in the sequence, because that's what counts + let _ = result.try_insert( + Vec::from_iter(current_pattern.iter().cloned()), + next_last_digit, + ); + current_pattern.pop_front(); // throw away the first digit. The pattern is a sliding window over the sequence + } + current_number = next_number; + } + result +} + +fn task2() { + let numbers = include_str!("../input.txt") + .lines() + .map(|line| line.parse::().unwrap()); + + let sequences: Vec<_> = numbers.map(calculate_sequence).collect(); + + let mut banana_counters = HashMap::new(); + for sequence in sequences.iter() { + for (pattern, price) in sequence.iter() { + banana_counters.insert( + pattern, + banana_counters.get(pattern).unwrap_or(&0_i64) + price, + ); + } + } + + let sum = banana_counters.values().max(); + + println!("Task 2: sum: {sum:?}"); +} + +fn main() { + task1(); + task2(); +}