diff --git a/README.md b/README.md index 9b25655..86ad5b2 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ Performance is reasonable even on older hardware, for example a 2011 MacBook Pro | 22 | [Monkey Market](https://adventofcode.com/2024/day/22) | [Source](src/year2024/day22.rs) | 1350 | | 23 | [LAN Party](https://adventofcode.com/2024/day/23) | [Source](src/year2024/day23.rs) | 43 | | 24 | [Crossed Wires](https://adventofcode.com/2024/day/24) | [Source](src/year2024/day24.rs) | 23 | -| 25 | [Code Chronicle](https://adventofcode.com/2024/day/25) | [Source](src/year2024/day25.rs) | 11 | +| 25 | [Code Chronicle](https://adventofcode.com/2024/day/25) | [Source](src/year2024/day25.rs) | 8 | ## 2023 diff --git a/src/year2024/day20.rs b/src/year2024/day20.rs index d051ee9..fb49d3c 100644 --- a/src/year2024/day20.rs +++ b/src/year2024/day20.rs @@ -1,9 +1,42 @@ //! # Race Condition +//! +//! Examining the input shows that there is only a single path from start to finish with +//! no branches. This simplifies checking for shortcuts as any empty space will be on the shortest +//! path from start to end. The cheating rules allow us to "warp" up to `n` squares away to any +//! empty space as measured by [manhattan distance](https://en.wikipedia.org/wiki/Taxicab_geometry). +//! +//! For part one this is a distance of 2. When checking surrounding squares we make 2 optimizations: +//! +//! * Don't check any squares only 1 away as we can always just move to these normally. +//! * Checking from point `p => q` is always the negative of `q => p`, e.g if `p = 30, q = 50` then +//! `p => q = 20` and `q => p = -20`. This means we only ever need to check any pair once. +//! * Additionally we only need to check down and right. Previous rows and columns will already +//! have checked points above and to the left when we reach an empty square. +//! +//! ```none +//! # . +//! ### ... +//! ##P## => ....# +//! ### ... +//! # # +//! ``` +//! +//! For part two the distance is increased to 20 and the shape now resembles a wonky diamond. +//! This shape ensures complete coverage without duplicating checks. +//! +//! ```none +//! # . +//! ### ... +//! ##P## => ..P## +//! ### ### +//! # # +//! ``` use crate::util::grid::*; use crate::util::point::*; use crate::util::thread::*; use std::sync::atomic::{AtomicU32, Ordering}; +/// Create a grid the same size as input with the time taken from start to any location. pub fn parse(input: &str) -> Grid { let grid = Grid::parse(input); let start = grid.find(b'S').unwrap(); @@ -12,6 +45,7 @@ pub fn parse(input: &str) -> Grid { let mut time = grid.same_size_with(i32::MAX); let mut elapsed = 0; + // Find starting direction, assuming start position is surrounded by 3 walls. let mut position = start; let mut direction = ORTHOGONAL.into_iter().find(|&o| grid[position + o] != b'#').unwrap(); @@ -19,6 +53,7 @@ pub fn parse(input: &str) -> Grid { time[position] = elapsed; elapsed += 1; + // There are no branches so we only ever need to go straight ahead or turn left or right. direction = [direction, direction.clockwise(), direction.counter_clockwise()] .into_iter() .find(|&d| grid[position + d] != b'#') @@ -37,6 +72,8 @@ pub fn part1(time: &Grid) -> u32 { for x in 1..time.width - 1 { let point = Point::new(x, y); + // We only need to check 2 points to the right and down as previous empty squares + // have already checked up and to the left. if time[point] != i32::MAX { cheats += check(time, point, Point::new(2, 0)); cheats += check(time, point, Point::new(0, 2)); @@ -47,6 +84,7 @@ pub fn part1(time: &Grid) -> u32 { cheats } +/// Searches for all cheats up to distance 20, parallelizing the work over multiple threads. pub fn part2(time: &Grid) -> u32 { let mut items = Vec::with_capacity(10_000); @@ -68,13 +106,7 @@ pub fn part2(time: &Grid) -> u32 { fn worker(time: &Grid, total: &AtomicU32, batch: Vec) { let mut cheats = 0; - // (p1, p2) is the reciprocal of (p2, p1) so we only need to check each pair once. Checking the - // wonky diamond shape on the right ensures complete coverage without duplicating checks. - // # . - // ### ... - // ##### => ..### - // ### ### - // # # + // (p1, p2) is the reciprocal of (p2, p1) so we only need to check each pair once. for point in batch { for x in 2..21 { cheats += check(time, point, Point::new(x, 0)); @@ -87,9 +119,11 @@ fn worker(time: &Grid, total: &AtomicU32, batch: Vec) { } } + // Update global total. total.fetch_add(cheats, Ordering::Relaxed); } +// Check if we save enough time warping to another square. #[inline] fn check(time: &Grid, first: Point, delta: Point) -> u32 { let second = first + delta; diff --git a/src/year2024/day22.rs b/src/year2024/day22.rs index 4599898..6cc27f2 100644 --- a/src/year2024/day22.rs +++ b/src/year2024/day22.rs @@ -1,4 +1,22 @@ //! # Monkey Market +//! +//! Solves both parts simultaneously, parallelizing the work over multiple threads since +//! each secret number is independent. The process of generating the next secret number is a +//! [linear feedback shift register](https://en.wikipedia.org/wiki/Linear-feedback_shift_register). +//! with a cycle of 2²⁴. +//! +//! Interestingly this means that with some clever math it's possible to generate the `n`th number +//! from any starting secret number with only 24 calculations. Unfortunately this doesn't help for +//! part two since we need to check every possible price change. However to speed things up we can +//! make several optimizations: +//! +//! * First the sequence of 4 prices is converted from -9..9 to a base 19 index of 0..19. +//! * Whether a monkey has seen a sequence before and the total bananas for each sequence are +//! stored in an array. This is much faster than a `HashMap`. Using base 19 gives much better +//! cache locality needing only 130321 elements, for example compared to shifting each new cost +//! by 5 bits and storing in an array of 2²⁰ = 1048675 elements. Multiplication on modern +//! processors is cheap (and several instructions can issue at once) but random memory access +//! is expensive. use crate::util::parse::*; use crate::util::thread::*; use std::sync::Mutex; @@ -53,9 +71,11 @@ fn worker(mutex: &Mutex, batch: &[usize]) { number = hash(number); let price = number % 10; + // Compute index into the array. (a, b, c, d) = (b, c, d, 9 + price - previous); let index = 6859 * a + 361 * b + 19 * c + d; + // Only sell the first time we see a sequence. if seen[index] != id { part_two[index] += price as u16; seen[index] = id; @@ -67,11 +87,14 @@ fn worker(mutex: &Mutex, batch: &[usize]) { part_one += number; } + // Merge into global results. let mut exclusive = mutex.lock().unwrap(); exclusive.part_one += part_one; exclusive.part_two.iter_mut().zip(part_two).for_each(|(a, b)| *a += b); } +/// Compute next secret number using a +/// [Xorshift LFSR](https://en.wikipedia.org/wiki/Linear-feedback_shift_register#Xorshift_LFSRs). fn hash(mut n: usize) -> usize { n = (n ^ (n << 6)) & 0xffffff; n = (n ^ (n >> 5)) & 0xffffff; diff --git a/src/year2024/day23.rs b/src/year2024/day23.rs index eb09089..7fd1213 100644 --- a/src/year2024/day23.rs +++ b/src/year2024/day23.rs @@ -1,8 +1,21 @@ //! # LAN Party +//! +//! This is the [Clique problem](https://en.wikipedia.org/wiki/Clique_problem). For part one we +//! find triangles (cliques of size 3) for each node by checking if there's an edge between any +//! distinct pair of neighbouring nodes. +//! +//! Part two asks to find the maximum clique, for which we could use the +//! [Bron–Kerbosch algorithm](https://en.wikipedia.org/wiki/Bron%E2%80%93Kerbosch_algorithm). +//! However the input has a specific structure that enables a simpler approach of finding the +//! largest *maximal* clique using a greedy algorithm. Nodes are arranged in clusters of 13 and +//! the maximum clique is size 14. This approach will not necessarily work for any general graph, +//! but will work for the inputs provided. use crate::util::hash::*; type Input = (FastMap>, Vec<[bool; 676]>); +/// Convert each character pair `xy` to an index from 0..676 so that we can use much faster array +/// lookup instead of a `HashMap`. pub fn parse(input: &str) -> Input { let mut nodes = FastMap::with_capacity(1_000); let mut edges = vec![[false; 676]; 676]; @@ -14,9 +27,11 @@ pub fn parse(input: &str) -> Input { let from = to_index(&edge[..2]); let to = to_index(&edge[3..]); + // Graph is undirected so add edges to both nodes. nodes.entry(from).or_insert_with(empty).push(to); nodes.entry(to).or_insert_with(empty).push(from); + // https://en.wikipedia.org/wiki/Adjacency_matrix edges[from][to] = true; edges[to][from] = true; } @@ -29,12 +44,14 @@ pub fn part1(input: &Input) -> usize { let mut seen = [false; 676]; let mut triangles = 0; + // Only consider nodes starting with `t`. for n1 in 494..520 { if let Some(neighbours) = nodes.get(&n1) { seen[n1] = true; for (i, &n2) in neighbours.iter().enumerate() { for &n3 in neighbours.iter().skip(i) { + // Skip nodes if we've already seen them. if !seen[n2] && !seen[n3] && edges[n2][n3] { triangles += 1; } @@ -52,11 +69,13 @@ pub fn part2(input: &Input) -> String { let mut clique = Vec::new(); let mut largest = Vec::new(); + // Greedy algorithm to find *maximal* (not maximum) cliques. for (&n1, neighbours) in nodes { if !seen[n1] { clique.clear(); clique.push(n1); + // Add nodes if they're connected to every node already in the clique. for &n2 in neighbours { if clique.iter().all(|&c| edges[n2][c]) { seen[n2] = true; @@ -64,12 +83,15 @@ pub fn part2(input: &Input) -> String { } } + // For the specific graphs given in the input + // finding the largest maximal clique will work. if clique.len() > largest.len() { largest.clone_from(&clique); } } } + // Convert each index back into 2 character identifiers sorted alphabetically. let mut result = String::new(); largest.sort_unstable(); diff --git a/src/year2024/day24.rs b/src/year2024/day24.rs index e483eec..cf8909d 100644 --- a/src/year2024/day24.rs +++ b/src/year2024/day24.rs @@ -1,4 +1,55 @@ //! # Crossed Wires +//! +//! Part one is a straightforward simulation of the gates. Part two asks us to fix a broken +//! [ripple carry adder](https://en.wikipedia.org/wiki/Adder_(electronics)). +//! +//! The structure of the adder is: +//! +//! * Half adder for bits `x00` and `y00`. Outputs sum to `z00` and carry to `z01`. +//! * Full adder for bits `x01..x44` and `y01..y44`. Outputs carry to next bit in the chain +//! "rippling" up to final bit. +//! * `z45` is the carry output from `x44` and `y44`. +//! +//! Implemented in logic gates this looks like: +//! +//! ```none +//! Half Adder Full Adder +//! ┌───┐ ┌───┐ ┌───┐ ┌───┐ +//! |x00| |y00| |x01| |y01| +//! └───┘ └───┘ └───┘ └───┘ +//! | | ┌─┘ | | | ┌─┘ | +//! | └───┐ | | └───┐ | +//! | ┌-┘ | | | ┌-┘ | | +//! ┌───┐ ┌───┐ ┌───┐ ┌───┐ +//! |XOR| |AND| |XOR| |AND| +//! └───┘ └───┘ └───┘ └───┘ +//! | | ┌───┴┐ | +//! | └──┬────┐ | | +//! | Carry| | ┌───┐ | +//! | out | | |AND| | +//! | | | └───┘ | +//! | | | └────┐ | +//! | | └────┐ | | +//! | └────┐ | | | +//! | ┌───┐ ┌───┐ +//! | |XOR| |OR | Carry +//! | └───┘ └───┘ out +//! | | | | +//! ┌───┐ ┌───┐ | ┌───┐ +//! |z00| |z01| Carry ...repeat for z01 to z44... |z45| +//! └───┘ └───┘ out └───┘ +//! ``` +//! +//! Then we can deduce some rules for the output of each gate type: +//! +//! 1. **XOR** If inputs are `x` and `y` then output must be another XOR gate +//! (except for inputs `x00` and `y00`) otherwise output must be `z`. +//! 2. **AND** Output must be an OR gate (except for inputs `x00` and `y00`). +//! 3. **OR** Output must be both AND and XOR gate, except for final carry +//! which must output to `z45`. +//! +//! We only need to find swapped outputs (not fix them) so the result is the labels of gates +//! that breaks the rules in alphabetical order. use crate::util::hash::*; use crate::util::iter::*; use crate::util::parse::*; @@ -15,21 +66,27 @@ pub fn parse(input: &str) -> Input<'_> { pub fn part1(input: &Input<'_>) -> u64 { let (prefix, gates) = input; + // Using an array to store already computed values is much faster than a `HashMap`. let mut todo: VecDeque<_> = gates.iter().copied().collect(); let mut cache = vec![u8::MAX; 1 << 15]; let mut result = 0; + // Convert each character to a 5 bit number from 0..31 + // then each group of 3 to a 15 bit index from 0..32768. let to_index = |s: &str| { let b = s.as_bytes(); ((b[0] as usize & 31) << 10) + ((b[1] as usize & 31) << 5) + (b[2] as usize & 31) }; + // Add input signals to cache. for line in prefix.lines() { let prefix = &line[..3]; let suffix = &line[5..]; cache[to_index(prefix)] = suffix.unsigned(); } + // If both inputs are available then add gate output to cache + // otherwise push back to end of queue for reprocessing later. while let Some(gate @ [left, kind, right, _, to]) = todo.pop_front() { let left = cache[to_index(left)]; let right = cache[to_index(right)]; @@ -46,7 +103,8 @@ pub fn part1(input: &Input<'_>) -> u64 { } } - for i in (to_index("z00")..to_index("z64")).rev() { + // Output 46 bit result. + for i in (to_index("z00")..to_index("z46")).rev() { if cache[i] != u8::MAX { result = (result << 1) | (cache[i] as u64); } @@ -58,18 +116,19 @@ pub fn part1(input: &Input<'_>) -> u64 { pub fn part2(input: &Input<'_>) -> String { let (_, gates) = input; - let mut lookup = FastSet::new(); + let mut output = FastSet::new(); let mut swapped = FastSet::new(); + // Track the kind of gate that each wire label outputs to. for &[left, kind, right, _, _] in gates { - lookup.insert((left, kind)); - lookup.insert((right, kind)); + output.insert((left, kind)); + output.insert((right, kind)); } for &[left, kind, right, _, to] in gates { if kind == "AND" { // Check that all AND gates point to an OR, except for first AND. - if left != "x00" && right != "x00" && !lookup.contains(&(to, "OR")) { + if left != "x00" && right != "x00" && !output.contains(&(to, "OR")) { swapped.insert(to); } } @@ -80,7 +139,7 @@ pub fn part2(input: &Input<'_>) -> String { swapped.insert(to); } // OR can never point to OR. - if lookup.contains(&(to, "OR")) { + if output.contains(&(to, "OR")) { swapped.insert(to); } } @@ -88,7 +147,7 @@ pub fn part2(input: &Input<'_>) -> String { if kind == "XOR" { if left.starts_with('x') || right.starts_with('x') { // Check that first level XOR points to second level XOR, except for first XOR. - if left != "x00" && right != "x00" && !lookup.contains(&(to, "XOR")) { + if left != "x00" && right != "x00" && !output.contains(&(to, "XOR")) { swapped.insert(to); } } else { diff --git a/src/year2024/day25.rs b/src/year2024/day25.rs index 04a8598..e313795 100644 --- a/src/year2024/day25.rs +++ b/src/year2024/day25.rs @@ -1,6 +1,20 @@ //! # Code Chronicle -const MASK: u64 = 0b_011111_011111_011111_011111_011111; - +//! +//! Efficiently checks if locks and keys overlap using bitwise logic. The ASCII character +//! `#` (35) is odd and `.` (46) is even so bitwise AND with 1 results in either 1 or 0. +//! The newline character `\n` (10) is even so will result in 0 and not contribute to matches. +//! There are 25 bits plus 4 newline bits so each lock or key can be stored in an `u32`. +//! For example: +//! +//! ```none +//! ##### +//! ##.## 11011 +//! .#.## 01011 +//! ...## => 00011 => 110110_010110_000110_000100_00010 +//! ...#. 00010 +//! ...#. 00010 +//! ..... +//! ``` pub fn parse(input: &str) -> &str { input } @@ -12,12 +26,12 @@ pub fn part1(input: &str) -> u32 { let mut result = 0; while !slice.is_empty() { - let bits = slice[6..35].iter().fold(0, |bits, &n| (bits << 1) | (n & 1) as u64); + let bits = slice[6..35].iter().fold(0, |bits, &n| (bits << 1) | (n & 1) as u32); if slice[0] == b'#' { - locks.push(bits & MASK); + locks.push(bits); } else { - keys.push(bits & MASK); + keys.push(bits); } slice = &slice[43.min(slice.len())..];