Skip to content

Commit

Permalink
Document day 20 and days 22 to 25
Browse files Browse the repository at this point in the history
  • Loading branch information
maneatingape committed Dec 28, 2024
1 parent 888468d commit c1ce2c7
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 20 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
48 changes: 41 additions & 7 deletions src/year2024/day20.rs
Original file line number Diff line number Diff line change
@@ -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<i32> {
let grid = Grid::parse(input);
let start = grid.find(b'S').unwrap();
Expand All @@ -12,13 +45,15 @@ pub fn parse(input: &str) -> Grid<i32> {
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();

while position != end {
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'#')
Expand All @@ -37,6 +72,8 @@ pub fn part1(time: &Grid<i32>) -> 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));
Expand All @@ -47,6 +84,7 @@ pub fn part1(time: &Grid<i32>) -> u32 {
cheats
}

/// Searches for all cheats up to distance 20, parallelizing the work over multiple threads.
pub fn part2(time: &Grid<i32>) -> u32 {
let mut items = Vec::with_capacity(10_000);

Expand All @@ -68,13 +106,7 @@ pub fn part2(time: &Grid<i32>) -> u32 {
fn worker(time: &Grid<i32>, total: &AtomicU32, batch: Vec<Point>) {
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));
Expand All @@ -87,9 +119,11 @@ fn worker(time: &Grid<i32>, total: &AtomicU32, batch: Vec<Point>) {
}
}

// Update global total.
total.fetch_add(cheats, Ordering::Relaxed);
}

// Check if we save enough time warping to another square.
#[inline]
fn check(time: &Grid<i32>, first: Point, delta: Point) -> u32 {
let second = first + delta;
Expand Down
23 changes: 23 additions & 0 deletions src/year2024/day22.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -53,9 +71,11 @@ fn worker(mutex: &Mutex<Exclusive>, 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;
Expand All @@ -67,11 +87,14 @@ fn worker(mutex: &Mutex<Exclusive>, 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;
Expand Down
22 changes: 22 additions & 0 deletions src/year2024/day23.rs
Original file line number Diff line number Diff line change
@@ -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<usize, Vec<usize>>, 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];
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -52,24 +69,29 @@ 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;
clique.push(n2);
}
}

// 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();

Expand Down
73 changes: 66 additions & 7 deletions src/year2024/day24.rs
Original file line number Diff line number Diff line change
@@ -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::*;
Expand All @@ -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)];
Expand All @@ -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);
}
Expand All @@ -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);
}
}
Expand All @@ -80,15 +139,15 @@ 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);
}
}

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 {
Expand Down
Loading

0 comments on commit c1ce2c7

Please sign in to comment.