Same person as @[email protected], different instance.

  • 5 Posts
  • 101 Comments
Joined 9 months ago
cake
Cake day: April 3rd, 2024

help-circle

  • Rust

    Nice ending for this year. Lock and key arrays are just added together and all elements must be <= 5. Merry Christmas!

    Solution
    fn flatten_block(block: Vec<Vec<bool>>) -> [u8; 5] {
        let mut flat = [0; 5];
        for row in &block[1..=5] {
            for x in 0..5 {
                if row[x] {
                    flat[x] += 1;
                }
            }
        }
        flat
    }
    
    fn parse(input: &str) -> (Vec<[u8; 5]>, Vec<[u8; 5]>) {
        let mut locks = Vec::new();
        let mut keys = Vec::new();
        for block_s in input.split("\n\n") {
            let block: Vec<Vec<bool>> = block_s
                .lines()
                .map(|l| l.bytes().map(|b| b == b'#').collect::<Vec<bool>>())
                .collect();
            assert_eq!(block.len(), 7);
            // Lock
            if block[0].iter().all(|e| *e) {
                locks.push(flatten_block(block));
            } else {
                keys.push(flatten_block(block));
            }
        }
        (locks, keys)
    }
    
    fn part1(input: String) {
        let (locks, keys) = parse(&input);
        let mut count = 0u32;
        for l in locks {
            for k in &keys {
                if l.iter().zip(k).map(|(li, ki)| li + ki).all(|sum| sum <= 5) {
                    count += 1;
                }
            }
        }
        println!("{count}");
    }
    
    fn part2(_input: String) {
        println!("⭐");
    }
    
    util::aoc_main!();
    

    Also on github


  • Rust + Pen and Paper

    Yikers. Part 2 took a while, staring at this diagram for hours. Eventually I noticed that each of these blocks has two pairs of (XOR, AND) gates sharing the same inputs (and inputs aren’t changed). So I matched up these pairs based on a distance metric of how much needs to be swapped to fit together. This helped me identify 4 blocks with errors, the rest was solved using pen and paper (one block is missing as it became apparent at that point):

    shaky diagrams for full adder with wrong outputs

    There is also some code, but do yourself and me a favor and don’t look at it. While it does turn up the correct solution, it probably won’t with any other input, especially not the examples.


  • Rust

    Finding cliques in a graph, which is actually NP-comlete. For part two I did look up how to do it and implemented the Bron-Kerbosch algorithm. Adding the pivoting optimization improved the runtime from 134ms to 7.4ms, so that is definitely worth it (in some sense, of course I already had the correct answer without pivoting).

    Solution
    use rustc_hash::{FxHashMap, FxHashSet};
    
    fn parse(input: &str) -> (Vec<Vec<usize>>, FxHashMap<&str, usize>) {
        let mut graph = Vec::new();
        let mut names: FxHashMap<&str, usize> = FxHashMap::default();
        for l in input.lines() {
            let (vs, ws) = l.split_once('-').unwrap();
            let v = *names.entry(vs).or_insert_with(|| {
                graph.push(vec![]);
                graph.len() - 1
            });
            let w = *names.entry(ws).or_insert_with(|| {
                graph.push(vec![]);
                graph.len() - 1
            });
            graph[v].push(w);
            graph[w].push(v);
        }
        (graph, names)
    }
    
    fn part1(input: String) {
        let (graph, names) = parse(&input);
        let mut triples: FxHashSet<[usize; 3]> = FxHashSet::default();
        for (_, &v) in names.iter().filter(|(name, _)| name.starts_with('t')) {
            for (i, &u) in graph[v].iter().enumerate().skip(1) {
                for w in graph[v].iter().take(i) {
                    if graph[u].contains(w) {
                        let mut triple = [u, v, *w];
                        triple.sort();
                        triples.insert(triple);
                    }
                }
            }
        }
        println!("{}", triples.len());
    }
    
    // Bron-Kerbosch algorithm for finding all maximal cliques in a graph
    fn bron_kerbosch(
        graph: &[Vec<usize>],
        r: &mut Vec<usize>,
        mut p: FxHashSet<usize>,
        mut x: FxHashSet<usize>,
    ) -> Vec<Vec<usize>> {
        if p.is_empty() && x.is_empty() {
            return vec![r.to_vec()];
        }
        let mut maximal_cliques = Vec::new();
        let Some(&u) = p.iter().next() else {
            return maximal_cliques;
        };
        let mut p_pivot = p.clone();
        for w in &graph[u] {
            p_pivot.remove(w);
        }
        for v in p_pivot {
            let pn = graph[v].iter().filter(|w| p.contains(w)).copied().collect();
            let xn = graph[v].iter().filter(|w| x.contains(w)).copied().collect();
            r.push(v);
            let new_cliques = bron_kerbosch(graph, r, pn, xn);
            r.pop();
            maximal_cliques.extend(new_cliques);
            p.remove(&v);
            x.insert(v);
        }
        maximal_cliques
    }
    
    fn part2(input: String) {
        let (graph, names) = parse(&input);
        let p = (0..graph.len()).collect();
        let mut r = Vec::new();
        let maximal_cliques = bron_kerbosch(&graph, &mut r, p, FxHashSet::default());
        let maximum_clique = maximal_cliques
            .iter()
            .max_by_key(|clique| clique.len())
            .unwrap();
        let mut lan_names: Vec<&str> = names
            .iter()
            .filter(|(_, v)| maximum_clique.contains(v))
            .map(|(name, _)| *name)
            .collect();
        lan_names.sort_unstable();
        println!("{}", lan_names.join(","));
    }
    
    util::aoc_main!();
    

    Also on github



  • Rust

    Nice breather today (still traumatized from the robots). At some point I thought you had to do some magic for predicting special properties of the pseudorandom function, but no, just collect all values, have a big table for all sequences and in the end take the maximum value in that table. Part 1 takes 6.7ms, part 2 19.2ms.

    Solution
    fn step(n: u32) -> u32 {
        let a = (n ^ (n << 6)) % (1 << 24);
        let b = a ^ (a >> 5);
        (b ^ (b << 11)) % (1 << 24)
    }
    
    fn part1(input: String) {
        let sum = input
            .lines()
            .map(|l| {
                let n = l.parse().unwrap();
                (0..2000).fold(n, |acc, _| step(acc)) as u64
            })
            // More than 2¹⁰ 24-bit numbers requires 35 bits
            .sum::<u64>();
        println!("{sum}");
    }
    
    const N_SEQUENCES: usize = 19usize.pow(4);
    
    fn sequence_key(sequence: &[i8]) -> usize {
        sequence
            .iter()
            .enumerate()
            .map(|(i, x)| (x + 9) as usize * 19usize.pow(i as u32))
            .sum()
    }
    
    fn part2(input: String) {
        // Table for collecting the amount of bananas for every possible sequence
        let mut table = vec![0; N_SEQUENCES];
        // Mark the sequences we encountered in a round to ensure that only the first occurence is used
        let mut seen = vec![false; N_SEQUENCES];
        for l in input.lines() {
            let n = l.parse().unwrap();
            let (diffs, prices): (Vec<i8>, Vec<u8>) = (0..2000)
                .scan(n, |acc, _| {
                    let next = step(*acc);
                    let diff = (next % 10) as i8 - (*acc % 10) as i8;
                    *acc = next;
                    Some((diff, (next % 10) as u8))
                })
                .unzip();
            for (window, price) in diffs.windows(4).zip(prices.iter().skip(3)) {
                let key = sequence_key(window);
                if !seen[key] {
                    seen[key] = true;
                    table[key] += *price as u32;
                }
            }
            // Reset seen sequences for next round
            seen.fill(false);
        }
        let bananas = table.iter().max().unwrap();
        println!("{bananas}");
    }
    
    util::aoc_main!();
    

    Also on github


  • Rust

    For me this was the hardest puzzle so far this year. First I did a bunch of things that turned out not to work properly. For example I tried to solve it with a greedy algorithm that always moved horizontally first then vertically, but that ignores the gaps that need to be avoided (what a sneaky requirement) and also somehow doesn’t guarantee the shortest sequence.

    After reaching part 2 it became clear that a recursive function (with memoization) is needed again, and of course in the end it turned out a lot simpler than what I had cooked up before (you don’t want to see that). Now even part 2 takes just 1.6ms.

    Solution
    use euclid::{default::*, point2, vec2};
    use rustc_hash::FxHashMap;
    use std::iter;
    
    type Move = Option<Vector2D<i32>>;
    
    const KEYPAD_GAP: Point2D<i32> = point2(0, 3);
    const DPAD_GAP: Point2D<i32> = point2(0, 0);
    
    fn keypad_pos(n: u8) -> Point2D<i32> {
        match n {
            b'7' => point2(0, 0),
            b'8' => point2(1, 0),
            b'9' => point2(2, 0),
            b'4' => point2(0, 1),
            b'5' => point2(1, 1),
            b'6' => point2(2, 1),
            b'1' => point2(0, 2),
            b'2' => point2(1, 2),
            b'3' => point2(2, 2),
            b'0' => point2(1, 3),
            b'A' => point2(2, 3),
            other => panic!("Invalid keypad symbol {other}"),
        }
    }
    
    // `None` is used for A
    fn dpad_pos(d: Move) -> Point2D<i32> {
        match d {
            Some(Vector2D { x: 0, y: -1, .. }) => point2(1, 0),
            None => point2(2, 0),
            Some(Vector2D { x: -1, y: 0, .. }) => point2(0, 1),
            Some(Vector2D { x: 0, y: 1, .. }) => point2(1, 1),
            Some(Vector2D { x: 1, y: 0, .. }) => point2(2, 1),
            other => panic!("Invalid dpad symbol {other:?}"),
        }
    }
    
    fn moves_for_diff(diff: Vector2D<i32>, pos: Point2D<i32>, gap: Point2D<i32>) -> Vec<Vec<Move>> {
        let horizontal = iter::repeat_n(
            Some(vec2(diff.x.signum(), 0)),
            diff.x.unsigned_abs() as usize,
        );
        let vertical = iter::repeat_n(
            Some(vec2(0, diff.y.signum())),
            diff.y.unsigned_abs() as usize,
        );
        if pos + vec2(diff.x, 0) == gap {
            // Must not move horizontal first, or we hit the gap
            vec![vertical.chain(horizontal).chain(iter::once(None)).collect()]
        } else if pos + vec2(0, diff.y) == gap {
            vec![horizontal.chain(vertical).chain(iter::once(None)).collect()]
        } else {
            // Try both horizontal first and vertical first
            vec![
                horizontal
                    .clone()
                    .chain(vertical.clone())
                    .chain(iter::once(None))
                    .collect(),
                vertical.chain(horizontal).chain(iter::once(None)).collect(),
            ]
        }
    }
    
    fn dpad_sequence_len(
        start: Move,
        end: Move,
        rounds: u32,
        cache: &mut FxHashMap<(Move, Move, u32), u64>,
    ) -> u64 {
        if rounds == 0 {
            return 1;
        }
        if let Some(len) = cache.get(&(start, end, rounds)) {
            return *len;
        }
        let start_pos = dpad_pos(start);
        let end_pos = dpad_pos(end);
        let diff = end_pos - start_pos;
        let possible_paths = moves_for_diff(diff, start_pos, DPAD_GAP);
        let shortest_sequence = possible_paths
            .iter()
            .map(|moves| {
                moves
                    .iter()
                    .fold((0, None), |(cost, prev), &m| {
                        (cost + dpad_sequence_len(prev, m, rounds - 1, cache), m)
                    })
                    .0
            })
            .min()
            .unwrap();
        cache.insert((start, end, rounds), shortest_sequence);
        shortest_sequence
    }
    
    fn keypad_sequence_len(start: u8, end: u8, rounds: u32) -> u64 {
        let start_pos = keypad_pos(start);
        let end_pos = keypad_pos(end);
        let diff = end_pos - start_pos;
        let possible_paths = moves_for_diff(diff, start_pos, KEYPAD_GAP);
        let mut cache = FxHashMap::default();
        possible_paths
            .iter()
            .map(|moves| {
                moves
                    .iter()
                    .fold((0, None), |(cost, prev), &m| {
                        (cost + dpad_sequence_len(prev, m, rounds, &mut cache), m)
                    })
                    .0
            })
            .min()
            .unwrap()
    }
    
    fn solve(input: &str, rounds: u32) -> u64 {
        let mut sum: u64 = 0;
        for l in input.lines() {
            let mut prev = b'A';
            let mut len = 0;
            for b in l.bytes() {
                len += keypad_sequence_len(prev, b, rounds);
                prev = b;
            }
            let code_n: u64 = l.strip_suffix('A').unwrap().parse().unwrap();
            sum += code_n * len;
        }
        sum
    }
    
    fn part1(input: String) {
        println!("{}", solve(&input, 2));
    }
    
    fn part2(input: String) {
        println!("{}", solve(&input, 25));
    }
    
    util::aoc_main!();
    

    Also on github



  • Rust

    The important part here is to build two maps first that hold for every point on the path the distance to the start and the distance to the end respectively. Then calculating the path length for a cheat vector is a simple lookup. For part 2 I first generated all vectors with manhattan distance <= 20, or more specifically, exactly half of those vectors to avoid checking the same vector in both directions.

    Part 2 takes 15ms. The code looks a bit unwieldy at parts and uses the pyramid of doom paradigm but works pretty well.

    Solution
    use euclid::{default::*, vec2};
    
    const DIRS: [Vector2D<i32>; 4] = [vec2(1, 0), vec2(0, 1), vec2(-1, 0), vec2(0, -1)];
    const MIN_SAVE: u32 = 100;
    const MAX_DIST: i32 = 20;
    
    fn parse(input: &str) -> (Vec<Vec<bool>>, Point2D<i32>, Point2D<i32>) {
        let mut start = None;
        let mut end = None;
        let mut field = Vec::new();
        for (y, line) in input.lines().enumerate() {
            let mut row = Vec::new();
            for (x, b) in line.bytes().enumerate() {
                row.push(b == b'#');
                if b == b'S' {
                    start = Some(Point2D::new(x, y).to_i32());
                } else if b == b'E' {
                    end = Some(Point2D::new(x, y).to_i32());
                }
            }
            field.push(row);
        }
        (field, start.unwrap(), end.unwrap())
    }
    
    fn distances(
        field: &[Vec<bool>],
        start: Point2D<i32>,
        end: Point2D<i32>,
    ) -> (Vec<Vec<u32>>, Vec<Vec<u32>>) {
        let width = field[0].len();
        let height = field.len();
        let mut dist_to_start = vec![vec![u32::MAX; width]; height];
        let bounds = Rect::new(Point2D::origin(), Size2D::new(width, height)).to_i32();
    
        let mut cur = start;
        let mut dist = 0;
        dist_to_start[cur.y as usize][cur.x as usize] = dist;
        while cur != end {
            for dir in DIRS {
                let next = cur + dir;
                if bounds.contains(next)
                    && !field[next.y as usize][next.x as usize]
                    && dist_to_start[next.y as usize][next.x as usize] == u32::MAX
                {
                    cur = next;
                    break;
                }
            }
            dist += 1;
            dist_to_start[cur.y as usize][cur.x as usize] = dist;
        }
        let total_dist = dist_to_start[end.y as usize][end.x as usize];
        let dist_to_end = dist_to_start
            .iter()
            .map(|row| {
                row.iter()
                    .map(|&d| {
                        if d == u32::MAX {
                            u32::MAX
                        } else {
                            total_dist - d
                        }
                    })
                    .collect()
            })
            .collect();
        (dist_to_start, dist_to_end)
    }
    
    fn cheats(
        field: &[Vec<bool>],
        dist_to_start: &[Vec<u32>],
        dist_to_end: &[Vec<u32>],
        total_dist: u32,
    ) -> u32 {
        let width = field[0].len();
        let height = field.len();
        let bounds = Rect::new(Point2D::origin(), Size2D::new(width, height)).to_i32();
        let mut count = 0;
        for (y, row) in field.iter().enumerate() {
            for (x, _w) in row.iter().enumerate().filter(|&(_i, w)| *w) {
                let pos = Point2D::new(x, y).to_i32();
                for (d0, &dir0) in DIRS.iter().enumerate().skip(1) {
                    for &dir1 in DIRS.iter().take(d0) {
                        let p0 = pos + dir0;
                        let p1 = pos + dir1;
                        if bounds.contains(p0) && bounds.contains(p1) {
                            let p0 = p0.to_usize();
                            let p1 = p1.to_usize();
                            if !field[p0.y][p0.x] && !field[p1.y][p1.x] {
                                let dist = dist_to_start[p0.y][p0.x].min(dist_to_start[p1.y][p1.x])
                                    + dist_to_end[p1.y][p1.x].min(dist_to_end[p0.y][p0.x])
                                    + 2; // Add 2 for cutting across the wall
                                if total_dist - dist >= MIN_SAVE {
                                    count += 1;
                                }
                            }
                        }
                    }
                }
            }
        }
        count
    }
    
    fn part1(input: String) {
        let (field, start, end) = parse(&input);
        let (dist_to_start, dist_to_end) = distances(&field, start, end);
        let total_dist = dist_to_start[end.y as usize][end.x as usize];
        println!(
            "{}",
            cheats(&field, &dist_to_start, &dist_to_end, total_dist)
        );
    }
    
    // Half of all vectors with manhattan distance <= MAX_DIST.
    // Only vectors with positive x or going straight down are considered to avoid using the same
    // vector twice in both directions.
    fn cheat_vectors() -> Vec<Vector2D<i32>> {
        let mut vectors = Vec::new();
        for y in -MAX_DIST..=MAX_DIST {
            let start = if y > 0 { 0 } else { 1 };
            for x in start..=(MAX_DIST - y.abs()) {
                assert!(x + y <= MAX_DIST);
                vectors.push(vec2(x, y));
            }
        }
        vectors
    }
    
    fn cheats20(
        field: &[Vec<bool>],
        dist_to_start: &[Vec<u32>],
        dist_to_end: &[Vec<u32>],
        total_dist: u32,
    ) -> u32 {
        let vectors = cheat_vectors();
        let width = field[0].len();
        let height = field.len();
        let bounds = Rect::new(Point2D::origin(), Size2D::new(width, height)).to_i32();
        let mut count = 0;
        for (y, row) in field.iter().enumerate() {
            for (x, _w) in row.iter().enumerate().filter(|&(_i, w)| !*w) {
                let p0 = Point2D::new(x, y);
                for &v in &vectors {
                    let pi1 = p0.to_i32() + v;
                    if bounds.contains(pi1) {
                        let p1 = pi1.to_usize();
                        if !field[p1.y][p1.x] {
                            let dist = dist_to_start[p0.y][p0.x].min(dist_to_start[p1.y][p1.x])
                                + dist_to_end[p1.y][p1.x].min(dist_to_end[p0.y][p0.x])
                                + v.x.unsigned_abs()  // Manhattan distance of vector
                                + v.y.unsigned_abs();
                            if total_dist - dist >= MIN_SAVE {
                                count += 1;
                            }
                        }
                    }
                }
            }
        }
        count
    }
    
    fn part2(input: String) {
        let (field, start, end) = parse(&input);
        let (dist_to_start, dist_to_end) = distances(&field, start, end);
        let total_dist = dist_to_start[end.y as usize][end.x as usize];
        println!(
            "{}",
            cheats20(&field, &dist_to_start, &dist_to_end, total_dist)
        );
    }
    
    util::aoc_main!();
    

    Also on github





  • Rust

    First part is solved by making a regex of the available towels, like ^(r|wr|bg|bwu|rb|gb|br)*$ for the example. If a design matches it, then it can be made. This didn’t work for the second part, which is done using recursion and memoization instead. Again, it was quite surprising to see such a high solution number. 32 bits were not enough (thanks, debug mode overflow detection).

    Solution
    use regex::Regex;
    use rustc_hash::FxHashMap;
    
    fn parse(input: &str) -> (Vec<&str>, Vec<&str>) {
        let (towels, designs) = input.split_once("\n\n").unwrap();
        (towels.split(", ").collect(), designs.lines().collect())
    }
    
    fn part1(input: String) {
        let (towels, designs) = parse(&input);
        let pat = format!("^({})*$", towels.join("|"));
        let re = Regex::new(&pat).unwrap();
        let count = designs.iter().filter(|d| re.is_match(d)).count();
        println!("{count}");
    }
    
    fn n_arrangements<'a>(
        design: &'a str,
        towels: &[&str],
        cache: &mut FxHashMap<&'a str, u64>,
    ) -> u64 {
        if design.is_empty() {
            return 1;
        }
        if let Some(n) = cache.get(design) {
            return *n;
        }
        let n = towels
            .iter()
            .filter(|t| design.starts_with(*t))
            .map(|t| n_arrangements(&design[t.len()..], towels, cache))
            .sum();
        cache.insert(design, n);
        n
    }
    
    fn part2(input: String) {
        let (towels, designs) = parse(&input);
        let sum: u64 = designs
            .iter()
            .map(|d| n_arrangements(d, &towels, &mut FxHashMap::default()))
            .sum();
        println!("{sum}");
    }
    
    util::aoc_main!();
    

    Also on github


  • Rust

    Naive approach running BFS after every dropped byte after 1024. Still runs in 50ms. This could be much optimized by using binary search to find the first blocked round and using A* instead of BFS, but I didn’t feel like doing more today.

    Solution
    use std::collections::VecDeque;
    
    use euclid::{default::*, vec2};
    
    fn parse(input: &str) -> Vec<Point2D<i32>> {
        input
            .lines()
            .map(|l| {
                let (x, y) = l.split_once(',').unwrap();
                Point2D::new(x.parse().unwrap(), y.parse().unwrap())
            })
            .collect()
    }
    
    const BOUNDS: Rect<i32> = Rect::new(Point2D::new(0, 0), Size2D::new(71, 71));
    const START: Point2D<i32> = Point2D::new(0, 0);
    const TARGET: Point2D<i32> = Point2D::new(70, 70);
    const N_BYTES: usize = 1024;
    const DIRS: [Vector2D<i32>; 4] = [vec2(1, 0), vec2(0, 1), vec2(-1, 0), vec2(0, -1)];
    
    fn adj(
        field: &[[bool; BOUNDS.size.width as usize]],
        v: Point2D<i32>,
    ) -> impl Iterator<Item = Point2D<i32>> + use<'_> {
        DIRS.iter()
            .map(move |&d| v + d)
            .filter(|&next| BOUNDS.contains(next) && !field[next.y as usize][next.x as usize])
    }
    
    fn find_path(field: &[[bool; BOUNDS.size.width as usize]]) -> Option<u32> {
        let mut seen = [[false; BOUNDS.size.width as usize]; BOUNDS.size.height as usize];
        let mut q = VecDeque::from([(START, 0)]);
        seen[START.y as usize][START.x as usize] = true;
        while let Some((v, dist)) = q.pop_front() {
            for w in adj(field, v) {
                if w == TARGET {
                    return Some(dist + 1);
                }
                if !seen[w.y as usize][w.x as usize] {
                    seen[w.y as usize][w.x as usize] = true;
                    q.push_back((w, dist + 1));
                }
            }
        }
        None
    }
    
    fn part1(input: String) {
        let bytes = parse(&input);
        let mut field = [[false; BOUNDS.size.width as usize]; BOUNDS.size.height as usize];
        for b in &bytes[..N_BYTES] {
            field[b.y as usize][b.x as usize] = true;
        }
        println!("{}", find_path(&field).unwrap());
    }
    
    fn part2(input: String) {
        let bytes = parse(&input);
        let mut field = [[false; BOUNDS.size.width as usize]; BOUNDS.size.height as usize];
        for (i, b) in bytes.iter().enumerate() {
            field[b.y as usize][b.x as usize] = true;
            // We already know from part 1 that below N_BYTES there is a path
            if i > N_BYTES && find_path(&field).is_none() {
                println!("{},{}", b.x, b.y);
                break;
            }
        }
    }
    
    util::aoc_main!();
    

    Also on github


  • Rust

    First part was straightforward (the divisions are actually just right shifts), second part not so much. I made some assumptions about the input program, namely that in the end register 8 is divided by 8, then an output is made, then everything starts from the beginning again (if a isn’t 0). I found that the output always depends on at most 10 bits of a, so I ran through all 10-bit numbers and grouped them by the first generated output. At that point it’s just a matter of chaining these 10-bit numbers from the correct groups so that they overlap on 7 bits. The other 3 bits are consumed each round.

    Solution
    use rustc_hash::FxHashMap;
    
    fn parse(input: &str) -> Option<Program> {
        let mut lines = input.lines();
        let a = lines.next()?.split_once(": ")?.1.parse().ok()?;
        let b = lines.next()?.split_once(": ")?.1.parse().ok()?;
        let c = lines.next()?.split_once(": ")?.1.parse().ok()?;
        lines.next()?;
        let program = lines
            .next()?
            .split_once(": ")?
            .1
            .split(',')
            .map(|s| s.parse())
            .collect::<Result<Vec<u8>, _>>()
            .ok()?;
        Some(Program {
            a,
            b,
            c,
            out: vec![],
            program,
            ip: 0,
        })
    }
    
    #[derive(Debug, Clone, Default)]
    struct Program {
        a: u64,
        b: u64,
        c: u64,
        out: Vec<u8>,
        program: Vec<u8>,
        ip: usize,
    }
    
    impl Program {
        fn run(&mut self) {
            while self.step() {}
        }
    
        // Returns true if a step was taken, false if it halted
        fn step(&mut self) -> bool {
            let Some(&[opcode, operand]) = &self.program.get(self.ip..self.ip + 2) else {
                return false;
            };
            self.ip += 2;
            match opcode {
                0 => self.adv(self.combo(operand)),
                1 => self.bxl(operand),
                2 => self.bst(self.combo(operand)),
                3 => self.jnz(operand),
                4 => self.bxc(),
                5 => self.out(self.combo(operand)),
                6 => self.bdv(self.combo(operand)),
                7 => self.cdv(self.combo(operand)),
                _ => panic!(),
            }
            true
        }
    
        fn combo(&self, operand: u8) -> u64 {
            match operand {
                0..=3 => operand as u64,
                4 => self.a,
                5 => self.b,
                6 => self.c,
                _ => unreachable!(),
            }
        }
    
        fn adv(&mut self, x: u64) {
            self.a >>= x
        }
    
        fn bxl(&mut self, x: u8) {
            self.b ^= x as u64
        }
    
        fn bst(&mut self, x: u64) {
            self.b = x % 8
        }
    
        fn jnz(&mut self, x: u8) {
            if self.a != 0 {
                self.ip = x as usize
            }
        }
    
        fn bxc(&mut self) {
            self.b ^= self.c
        }
    
        fn out(&mut self, x: u64) {
            self.out.push((x % 8) as u8)
        }
    
        fn bdv(&mut self, x: u64) {
            self.b = self.a >> x
        }
    
        fn cdv(&mut self, x: u64) {
            self.c = self.a >> x
        }
    }
    
    fn part1(input: String) {
        let mut program = parse(&input).unwrap();
        program.run();
        if let Some(e) = program.out.first() {
            print!("{e}")
        }
        for e in program.out.iter().skip(1) {
            print!(",{e}")
        }
        println!()
    }
    
    // Some assumptions on the input:
    // * There is exactly one jump instruction at the end of the program, jumping to 0
    // * Right before that, an output is generated
    // * Right before that, register a is shifted right by 3: adv(3)
    //
    // Each output depends on at most 10 bits of a (it is used with a shift of at most 7).
    // Therefore we look at all 10-bit a's and group them by the first number that is output.
    // Then we just need to combine these generators into a chain that fits together.
    fn number_generators(mut program: Program) -> [Vec<u16>; 8] {
        let mut out = [const { vec![] }; 8];
        for a in 1..(1 << 10) {
            program.a = a as u64;
            program.out.clear();
            program.ip = 0;
            program.run();
            let &output = program.out.first().unwrap();
            out[output as usize].push(a);
        }
        out
    }
    
    fn part2(input: String) {
        let mut program = parse(&input).unwrap();
        let generators = number_generators(program.clone());
    
        let output = program.program.clone();
        // a_candidates maps from 7-bit required prefixes to the lower bits of a that
        // generate the required numbers so far.
        let mut a_candidates: FxHashMap<u8, u64> = generators[output[0] as usize]
            .iter()
            .rev() // Collects the values for each prefix
            .map(|&a| ((a >> 3) as u8, a as u64 % 8))
            .collect();
        let len = output.len();
        for (i, x) in output.iter().enumerate().skip(1) {
            let mut next_candidates = FxHashMap::default();
            for (prefix, val) in generators[*x as usize]
                .iter()
                .filter(|&a| {
                    // Take only short candidates in the end to ensure that not too many numbers are generated
                    let max_bits = (len - i) * 3;
                    (*a as u64) < (1u64 << max_bits)
                })
                // Only use generators that match any required prefix
                .filter(|&a| a_candidates.contains_key(&((a % (1 << 7)) as u8)))
                .map(|&a| {
                    let prefix = (a >> 3) as u8;
                    let val = a as u64 % 8;
                    let prev = a_candidates[&((a % (1 << 7)) as u8)];
                    (prefix, (val << (i * 3)) | prev)
                })
            {
                // Only insert first (smallest) encountered value
                next_candidates.entry(prefix).or_insert(val);
            }
            a_candidates = next_candidates;
        }
        println!("{}", a_candidates[&0]);
    
        // Verify result
        program.a = a_candidates[&0];
        program.run();
        assert_eq!(program.out, program.program);
    }
    
    util::aoc_main!();
    

    Also on github



  • Rust

    Dijkstra’s algorithm. While the actual shortest path was not needed in part 1, only the distance, in part 2 the path is saved in the parent hashmap, and crucially, if we encounter two paths with the same distance, both parent nodes are saved. This ensures we end up with all shortest paths in the end.

    Solution
    use std::cmp::{Ordering, Reverse};
    
    use euclid::{default::*, vec2};
    use priority_queue::PriorityQueue;
    use rustc_hash::{FxHashMap, FxHashSet};
    
    const DIRS: [Vector2D<i32>; 4] = [vec2(1, 0), vec2(0, 1), vec2(-1, 0), vec2(0, -1)];
    
    type Node = (Point2D<i32>, u8);
    
    fn parse(input: &str) -> (Vec<Vec<bool>>, Point2D<i32>, Point2D<i32>) {
        let mut start = None;
        let mut end = None;
        let mut field = Vec::new();
        for (y, l) in input.lines().enumerate() {
            let mut row = Vec::new();
            for (x, b) in l.bytes().enumerate() {
                if b == b'S' {
                    start = Some(Point2D::new(x, y).to_i32());
                } else if b == b'E' {
                    end = Some(Point2D::new(x, y).to_i32());
                }
                row.push(b == b'#');
            }
            field.push(row);
        }
        (field, start.unwrap(), end.unwrap())
    }
    
    fn adj(field: &[Vec<bool>], (v, dir): Node) -> Vec<(Node, u32)> {
        let mut adj = Vec::with_capacity(3);
        let next = v + DIRS[dir as usize];
        if !field[next.y as usize][next.x as usize] {
            adj.push(((next, dir), 1));
        }
        adj.push(((v, (dir + 1) % 4), 1000));
        adj.push(((v, (dir + 3) % 4), 1000));
        adj
    }
    
    fn shortest_path_length(field: &[Vec<bool>], start: Node, end: Point2D<i32>) -> u32 {
        let mut dist: FxHashMap<Node, u32> = FxHashMap::default();
        dist.insert(start, 0);
        let mut pq: PriorityQueue<Node, Reverse<u32>> = PriorityQueue::new();
        pq.push(start, Reverse(0));
        while let Some((v, _)) = pq.pop() {
            for (w, weight) in adj(field, v) {
                let dist_w = dist.get(&w).copied().unwrap_or(u32::MAX);
                let new_dist = dist[&v] + weight;
                if dist_w > new_dist {
                    dist.insert(w, new_dist);
                    pq.push_increase(w, Reverse(new_dist));
                }
            }
        }
        // Shortest distance to end, regardless of final direction
        (0..4).map(|dir| dist[&(end, dir)]).min().unwrap()
    }
    
    fn part1(input: String) {
        let (field, start, end) = parse(&input);
        let distance = shortest_path_length(&field, (start, 0), end);
        println!("{distance}");
    }
    
    fn shortest_path_tiles(field: &[Vec<bool>], start: Node, end: Point2D<i32>) -> u32 {
        let mut parents: FxHashMap<Node, Vec<Node>> = FxHashMap::default();
        let mut dist: FxHashMap<Node, u32> = FxHashMap::default();
        dist.insert(start, 0);
        let mut pq: PriorityQueue<Node, Reverse<u32>> = PriorityQueue::new();
        pq.push(start, Reverse(0));
        while let Some((v, _)) = pq.pop() {
            for (w, weight) in adj(field, v) {
                let dist_w = dist.get(&w).copied().unwrap_or(u32::MAX);
                let new_dist = dist[&v] + weight;
                match dist_w.cmp(&new_dist) {
                    Ordering::Greater => {
                        parents.insert(w, vec![v]);
                        dist.insert(w, new_dist);
                        pq.push_increase(w, Reverse(new_dist));
                    }
                    // Remember both parents if distance is equal
                    Ordering::Equal => parents.get_mut(&w).unwrap().push(v),
                    Ordering::Less => {}
                }
            }
        }
        let mut path_tiles: FxHashSet<Point2D<i32>> = FxHashSet::default();
        path_tiles.insert(end);
    
        // Shortest distance to end, regardless of final direction
        let shortest_dist = (0..4).map(|dir| dist[&(end, dir)]).min().unwrap();
        for dir in 0..4 {
            if dist[&(end, dir)] == shortest_dist {
                collect_tiles(&parents, &mut path_tiles, (end, dir));
            }
        }
        path_tiles.len() as u32
    }
    
    fn collect_tiles(
        parents: &FxHashMap<Node, Vec<Node>>,
        tiles: &mut FxHashSet<Point2D<i32>>,
        cur: Node,
    ) {
        if let Some(pars) = parents.get(&cur) {
            for p in pars {
                tiles.insert(p.0);
                collect_tiles(parents, tiles, *p);
            }
        }
    }
    
    fn part2(input: String) {
        let (field, start, end) = parse(&input);
        let tiles = shortest_path_tiles(&field, (start, 0), end);
        println!("{tiles}");
    }
    
    util::aoc_main!();
    

    Also on github


  • Rust

    Part 2 was a bit tricky. Moving into a box horizontally works mostly the same as for part 1, for the vertical case I used two recursive functions. The first recurses from the left and right side for each box just to find out if the entire tree can be moved. The second function actually does the moving in a similar recursive structure, but now with the knowledge that all subtrees can actually be moved.

    Lots of moving parts, but at least it could very nicely be debugged by printing out the map from the two minimal examples after each round.

    Solution
    use euclid::{default::*, vec2};
    
    // Common type for both parts. In part 1 all boxes are BoxL.
    #[derive(Clone, Copy)]
    enum Spot {
        Empty,
        BoxL,
        BoxR,
        Wall,
    }
    
    impl From<u8> for Spot {
        fn from(value: u8) -> Self {
            match value {
                b'.' | b'@' => Spot::Empty,
                b'O' => Spot::BoxL,
                b'#' => Spot::Wall,
                other => panic!("Invalid spot: {other}"),
            }
        }
    }
    
    fn parse(input: &str) -> (Vec<Vec<Spot>>, Point2D<i32>, Vec<Vector2D<i32>>) {
        let (field_s, moves_s) = input.split_once("\n\n").unwrap();
        let mut field = Vec::new();
        let mut robot = None;
        for (y, l) in field_s.lines().enumerate() {
            let mut row = Vec::new();
            for (x, b) in l.bytes().enumerate() {
                row.push(Spot::from(b));
                if b == b'@' {
                    robot = Some(Point2D::new(x, y).to_i32())
                }
            }
            field.push(row);
        }
    
        let moves = moves_s
            .bytes()
            .filter(|b| *b != b'\n')
            .map(|b| match b {
                b'^' => vec2(0, -1),
                b'>' => vec2(1, 0),
                b'v' => vec2(0, 1),
                b'<' => vec2(-1, 0),
                other => panic!("Invalid move: {other}"),
            })
            .collect();
        (field, robot.unwrap(), moves)
    }
    
    fn gps(field: &[Vec<Spot>]) -> u32 {
        let mut sum = 0;
        for (y, row) in field.iter().enumerate() {
            for (x, s) in row.iter().enumerate() {
                if let Spot::BoxL = s {
                    sum += x + 100 * y;
                }
            }
        }
        sum as u32
    }
    
    fn part1(input: String) {
        let (mut field, mut robot, moves) = parse(&input);
        for m in moves {
            let next = robot + m;
            match field[next.y as usize][next.x as usize] {
                Spot::Empty => robot = next, // Move into space
                Spot::BoxL => {
                    let mut search = next + m;
                    let can_move = loop {
                        match field[search.y as usize][search.x as usize] {
                            Spot::BoxL => {}
                            Spot::Wall => break false,
                            Spot::Empty => break true,
                            Spot::BoxR => unreachable!(),
                        }
                        search += m;
                    };
                    if can_move {
                        robot = next;
                        field[next.y as usize][next.x as usize] = Spot::Empty;
                        field[search.y as usize][search.x as usize] = Spot::BoxL;
                    }
                }
                Spot::Wall => {} // Cannot move
                Spot::BoxR => unreachable!(),
            }
        }
        println!("{}", gps(&field));
    }
    
    // Transform part 1 field to wider part 2 field
    fn widen(field: &[Vec<Spot>]) -> Vec<Vec<Spot>> {
        field
            .iter()
            .map(|row| {
                row.iter()
                    .flat_map(|s| match s {
                        Spot::Empty => [Spot::Empty; 2],
                        Spot::Wall => [Spot::Wall; 2],
                        Spot::BoxL => [Spot::BoxL, Spot::BoxR],
                        Spot::BoxR => unreachable!(),
                    })
                    .collect()
            })
            .collect()
    }
    
    // Recursively find out whether or not the robot can move in direction `dir` from `start`.
    fn can_move_rec(field: &[Vec<Spot>], start: Point2D<i32>, dir: Vector2D<i32>) -> bool {
        let next = start + dir;
        match field[next.y as usize][next.x as usize] {
            Spot::Empty => true,
            Spot::BoxL => can_move_rec(field, next, dir) && can_move_rec(field, next + vec2(1, 0), dir),
            Spot::BoxR => can_move_rec(field, next - vec2(1, 0), dir) && can_move_rec(field, next, dir),
            Spot::Wall => false,
        }
    }
    
    // Recursively execute a move for vertical directions into boxes.
    fn do_move(field: &mut [Vec<Spot>], start: Point2D<i32>, dir: Vector2D<i32>) {
        let next = start + dir;
        match field[next.y as usize][next.x as usize] {
            Spot::Empty | Spot::Wall => {}
            Spot::BoxL => {
                do_move(field, next, dir);
                do_move(field, next + vec2(1, 0), dir);
                let move_to = next + dir;
                field[next.y as usize][next.x as usize] = Spot::Empty;
                field[next.y as usize][next.x as usize + 1] = Spot::Empty;
                field[move_to.y as usize][move_to.x as usize] = Spot::BoxL;
                field[move_to.y as usize][move_to.x as usize + 1] = Spot::BoxR;
            }
            Spot::BoxR => {
                do_move(field, next - vec2(1, 0), dir);
                do_move(field, next, dir);
                let move_to = next + dir;
                field[next.y as usize][next.x as usize - 1] = Spot::Empty;
                field[next.y as usize][next.x as usize] = Spot::Empty;
                field[move_to.y as usize][move_to.x as usize - 1] = Spot::BoxL;
                field[move_to.y as usize][move_to.x as usize] = Spot::BoxR;
            }
        }
    }
    
    fn part2(input: String) {
        let (field1, robot1, moves) = parse(&input);
        let mut field = widen(&field1);
        let mut robot = Point2D::new(robot1.x * 2, robot1.y);
        for m in moves {
            let next = robot + m;
            match field[next.y as usize][next.x as usize] {
                Spot::Empty => robot = next, // Move into space
                Spot::BoxL | Spot::BoxR if m.y == 0 => {
                    let mut search = next + m;
                    let can_move = loop {
                        match field[search.y as usize][search.x as usize] {
                            Spot::BoxL | Spot::BoxR => {}
                            Spot::Wall => break false,
                            Spot::Empty => break true,
                        }
                        search += m;
                    };
                    if can_move {
                        robot = next;
                        // Shift boxes by array remove/insert
                        field[next.y as usize].remove(search.x as usize);
                        field[next.y as usize].insert(next.x as usize, Spot::Empty);
                    }
                }
                Spot::BoxL | Spot::BoxR => {
                    if can_move_rec(&field, robot, m) {
                        do_move(&mut field, robot, m);
                        robot = next;
                    }
                }
                Spot::Wall => {} // Cannot move
            }
        }
        println!("{}", gps(&field));
    }
    
    util::aoc_main!();
    

    Also on github



  • Rust

    Part 2 was very surprising in that it had a very vague requirement: “Find christmas tree!”. But my idea of finding the first round where no robots overlap turned out to just work when printing the map, so that was nice. I’m glad I did not instead start looking for symmetric patterns, because the christmas tree map is not symmetric at all.

    Solution
    use euclid::default::*;
    use regex::Regex;
    
    fn parse(input: &str) -> Vec<(Point2D<i32>, Vector2D<i32>)> {
        let re = Regex::new(r"p=(\d+),(\d+) v=(-?\d+),(-?\d+)").unwrap();
        re.captures_iter(input)
            .map(|cap| {
                let (_, [p0, p1, v0, v1]) = cap.extract();
                (
                    Point2D::new(p0.parse().unwrap(), p1.parse().unwrap()),
                    Vector2D::new(v0.parse().unwrap(), v1.parse().unwrap()),
                )
            })
            .collect()
    }
    
    const ROOM: Size2D<i32> = Size2D::new(101, 103);
    const TIME: i32 = 100;
    
    fn part1(input: String) {
        let robots = parse(&input);
        let new_pos: Vec<Point2D<i32>> = robots.iter()
            .map(|&(p, v)| (p + v * TIME).rem_euclid(&ROOM))
            .collect();
    
        assert_eq!(ROOM.width % 2, 1);
        assert_eq!(ROOM.height % 2, 1);
        let mid_x = ROOM.width / 2;
        let mid_y = ROOM.height / 2;
        
        let mut q = [0u32; 4];
        for p in new_pos {
            use std::cmp::Ordering::*;
            match (p.x.cmp(&mid_x), p.y.cmp(&mid_y)) {
                (Less, Less) => q[0] += 1,
                (Greater, Less) => q[1] += 1,
                (Less, Greater) => q[2] += 1,
                (Greater, Greater) => q[3] += 1,
                _ => {}
            };
        }
        let prod = q[0] * q[1] * q[2] * q[3];
        println!("{prod}");
    }
    
    fn print_map(map: &[Vec<bool>]) {
        for row in map {
            for p in row {
                if *p { print!("#") } else { print!(".") }
            }
            println!();
        }
        println!();
    }
    
    
    fn part2(input: String) {
        let mut robots = parse(&input);
        let mut map = vec![vec![false; ROOM.width as usize]; ROOM.height as usize];
        for i in 1.. {
            let mut overlap = false;
            for (p, v) in &mut robots {
                *p = (*p + *v).rem_euclid(&ROOM);
                if map[p.y as usize][p.x as usize] {
                    // Found two robots on the same spot,
                    // which is used as a heuristic for detecting the easter egg.
                    overlap = true;
                } else {
                    map[p.y as usize][p.x as usize] = true;
                }
            }
            if !overlap {
                print_map(&map);
                println!("Round: {i}");
                break;
            }
            for row in &mut map {
                row.fill(false);
            }
        }
    }
    
    util::aoc_main!();
    

    Also on github


  • Rust

    This problem is basically a linear system, which can be solved by inverting the 2x2 matrix of button distances. I put some more detail in the comments.

    Solution
    use std::sync::LazyLock;
    
    use regex::Regex;
    
    #[derive(Debug)]
    struct Machine {
        a: (i64, i64),
        b: (i64, i64),
        prize: (i64, i64),
    }
    
    impl Machine {
        fn tokens_100(&self) -> i64 {
            for b in 0..=100 {
                for a in 0..=100 {
                    let pos = (self.a.0 * a + self.b.0 * b, self.a.1 * a + self.b.1 * b);
                    if pos == self.prize {
                        return b + 3 * a;
                    }
                }
            }
            0
        }
    
        fn tokens_inv(&self) -> i64 {
            // If [ab] is the matrix containing our two button vectors: [ a.0 b.0 ]
            //                                                          [ a.1 b.1 ]
            // then prize = [ab] * x, where x holds the number of required button presses
            // for a and b, (na, nb).
            // By inverting [ab] we get
            //
            // x = [ab]⁻¹ * prize
            let det = (self.a.0 * self.b.1) - (self.a.1 * self.b.0);
            if det == 0 {
                panic!("Irregular matrix");
            }
            let det = det as f64;
            // The matrix [ a b ] is the inverse of [ a.0 b.0 ] .
            //            [ c d ]                   [ a.1 b.1 ]
            let a = self.b.1 as f64 / det;
            let b = -self.b.0 as f64 / det;
            let c = -self.a.1 as f64 / det;
            let d = self.a.0 as f64 / det;
            // Multiply [ab] * prize to get the result
            let na = self.prize.0 as f64 * a + self.prize.1 as f64 * b;
            let nb = self.prize.0 as f64 * c + self.prize.1 as f64 * d;
    
            // Only integer solutions are valid, verify rounded results:
            let ina = na.round() as i64;
            let inb = nb.round() as i64;
            let pos = (
                self.a.0 * ina + self.b.0 * inb,
                self.a.1 * ina + self.b.1 * inb,
            );
            if pos == self.prize {
                inb + 3 * ina
            } else {
                0
            }
        }
    
        fn translate(&self, tr: i64) -> Self {
            let prize = (self.prize.0 + tr, self.prize.1 + tr);
            Machine { prize, ..*self }
        }
    }
    
    impl From<&str> for Machine {
        fn from(s: &str) -> Self {
            static RE: LazyLock<(Regex, Regex)> = LazyLock::new(|| {
                (
                    Regex::new(r"Button [AB]: X\+(\d+), Y\+(\d+)").unwrap(),
                    Regex::new(r"Prize: X=(\d+), Y=(\d+)").unwrap(),
                )
            });
            let (re_btn, re_prize) = &*RE;
            let mut caps = re_btn.captures_iter(s);
            let (_, [a0, a1]) = caps.next().unwrap().extract();
            let a = (a0.parse().unwrap(), a1.parse().unwrap());
            let (_, [b0, b1]) = caps.next().unwrap().extract();
            let b = (b0.parse().unwrap(), b1.parse().unwrap());
            let (_, [p0, p1]) = re_prize.captures(s).unwrap().extract();
            let prize = (p0.parse().unwrap(), p1.parse().unwrap());
            Machine { a, b, prize }
        }
    }
    
    fn parse(input: String) -> Vec<Machine> {
        input.split("\n\n").map(Into::into).collect()
    }
    
    fn part1(input: String) {
        let machines = parse(input);
        let sum = machines.iter().map(|m| m.tokens_100()).sum::<i64>();
        println!("{sum}");
    }
    
    const TRANSLATION: i64 = 10000000000000;
    
    fn part2(input: String) {
        let machines = parse(input);
        let sum = machines
            .iter()
            .map(|m| m.translate(TRANSLATION).tokens_inv())
            .sum::<i64>();
        println!("{sum}");
    }
    
    util::aoc_main!();
    

    Also on github