Table of Contents

mr-kaffee

73745454?v=4

mr-kaffee
Peter Wieland
Github: mr-kaffee, Strava: Peter Wieland

About me

I am a regular SW coder. Each year in december, I code AoC puzzle solutions. This year, for the 3rd year in a row, I’ll go with Rust.

In my professional career, I used to do lots of Java and MATLAB coding, yet, this feels like very long time ago.

If I do not code or work, I enjoy cycling on or off roads, with or without electric support.

Day 00: rust

Day 00: Hello World!

It’ll be the 3rd year of Rust solutions for the 2022 edition of Advent of Code

I created a little solution infrastructure (which resides in the aoc subfolder of day00/rust/mr-kaffee), that I want to use in my solutions.

My day00 solution can be used to run all or some of the solutions (cargo run --release -- run -y 2022 -d 1..=25 or cargo run --release -- run -y 2022 -d 1,3,7 or simply cargo run --release to run everything)

Other than that, my challenges are again to create solutions that perform well and that do not use external dependencies. The day00 solution is an exception to the latter rule. It uses itertools, clap, regex, and lazy_static.

General structure

My solutions will be implemented in a src/lib.rs file and generally have the structure detailed below. This is most probably a bit of an overhead, but fun to write and a learning opportunity for me.

The Puzzle definition

This is a function returning a mr_kaffee_aoc::Puzzle struct which defines metadata, input data, references to solver functions and expected solutions.

pub fn puzzle() -> Puzzle<'static, PuzzleData, usize, usize, usize, usize> {
    Puzzle {
        year: 2022,
        day: 0,
        input: include_str!("../input.txt"),
        star1: Some(Star {
            name: "Hello World example",
            f: &star_1,
            exp: Some(0),
        }),
        star2: None,
    }
}
Data Structures

This includes the code to parse the input data

pub struct PuzzleData {
    input: &'static str,
}

impl From<&'static str> for PuzzleData {
    fn from(input: &'static str) -> Self {
        Self { input }
    }
}
The solver functions

The main solver functions plus potentially helper functions

pub fn star_1(data: &PuzzleData) -> usize {
    println!("{}", data.input);
    0
}
Tests

Tests, in particular code to execute the test cases typically defined in the puzzles

#[cfg(test)]
mod tests {
    use super::*;
    use mr_kaffee_aoc::GenericPuzzle;

    #[test]
    pub fn test_something() {
        let puzzle = puzzle();
        assert!(puzzle.solve_handle_err());
    }
}

Day 01: rust

Day 1: Calorie Counting

Rust solution to AoC|2022|1.

Nothing very interesting today.

Input

Parse everything in a vec of sums (initially I used a vec of vecs; since individual elements are never needed sums are enough)

pub mod input {
    use std::num::ParseIntError;

    #[derive(Debug)]
    pub struct PuzzleData {
        pub calories: Vec<usize>,
    }

    impl TryFrom<&'static str> for PuzzleData {
        type Error = ParseIntError;

        /// parse the puzzle input
        fn try_from(s: &'static str) -> Result<Self, Self::Error> {
            s.split("\n\n")
                .map(|elf| elf.lines().map(|l| l.parse::<usize>()).sum())
                .collect::<Result<Vec<_>, _>>()
                .map(|calories| Self { calories })
        }
    }
}
Star 1

Just find the biggest sum (I use fold instead of max to not handle the Option::None case)

pub fn star_1(data: &PuzzleData) -> usize {
    data.calories.iter().fold(0, |mx, &cal| mx.max(cal))
}
Star 2

Sum the three biggest sums. First solution used Vec::sort. New solution does not use any sorting and should be O(n).

pub fn star_2(data: &PuzzleData) -> usize {
    data.calories
        .iter()
        .fold([0; 3], |mut mx, &cal| {
            if cal > mx[0] {
                mx[2] = mx[1];
                mx[1] = mx[0];
                mx[0] = cal;
            } else if cal > mx[1] {
                mx[2] = mx[1];
                mx[1] = cal;
            } else if cal > mx[2] {
                mx[2] = cal;
            }
            mx
        })
        .iter()
        .sum()
}
Tests

No tests today. It was too simple.

Today I learned

Not so much. My template works.

Iterating/refactoring the solution, I learned that std::iter::Sum has an implementation impl<T, U, E> Sum<Result<U, E>> for Result<T, E>.

Day 02: rust

Day 2: Rock Paper Scissors

Rust solution to AoC|2022|2.

Input

I have two enums to parse the first column (A, B, C to RockPaperScissors) and the second column (X, Y, Z to XYZ).

It would have been much simpler to parse both into 0, 1, 2 and than use some simple formulas later on (which I did as an alternative, see below), but my solution fails gracefully if something unexpected comes in the input ;)

pub mod input {

    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub enum RockPaperScissors {
        Rock,
        Paper,
        Scissors,
    }

    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub enum XYZ {
        X,
        Y,
        Z,
    }

    #[derive(Debug)]
    pub struct PuzzleData {
        pub strategy: Vec<(RockPaperScissors, XYZ)>,
    }

    impl TryFrom<&'static str> for PuzzleData {
        type Error = String;

        /// parse the puzzle input
        fn try_from(s: &'static str) -> Result<Self, Self::Error> {
            s.lines()
                .map(|l| {
                    l.split_once(' ')
                        .ok_or_else(|| format!("Could not parse line '{l}'"))
                        .and_then(|(a, b)| {
                            match a {
                                "A" => Ok(RockPaperScissors::Rock),
                                "B" => Ok(RockPaperScissors::Paper),
                                "C" => Ok(RockPaperScissors::Scissors),
                                _ => Err(format!("Expected one of A, B, C, found '{a}'")),
                            }
                            .and_then(|a| match b {
                                "X" => Ok((a, XYZ::X)),
                                "Y" => Ok((a, XYZ::Y)),
                                "Z" => Ok((a, XYZ::Z)),
                                _ => Err(format!("Expected one of X, Y, Z, found '{b}'")),
                            })
                        })
                })
                .collect::<Result<Vec<_>, _>>()
                .map(|strategy| Self { strategy })
        }
    }
}
Star 1

For star 1 I directly convert `XYZ`s to `RockPaperScissors’ using

    fn to_rock_paper_scissors(&self) -> RockPaperScissors {
        match self {
            XYZ::X => RockPaperScissors::Rock,
            XYZ::Y => RockPaperScissors::Paper,
            XYZ::Z => RockPaperScissors::Scissors,
        }
    }

The scores are calculated with

    fn result(&self, other: &Self) -> usize {
        match (self, other) {
            (RockPaperScissors::Rock, RockPaperScissors::Rock) => 1 + 3,
            (RockPaperScissors::Rock, RockPaperScissors::Paper) => 1 + 0,
            (RockPaperScissors::Rock, RockPaperScissors::Scissors) => 1 + 6,
            (RockPaperScissors::Paper, RockPaperScissors::Rock) => 2 + 6,
            (RockPaperScissors::Paper, RockPaperScissors::Paper) => 2 + 3,
            (RockPaperScissors::Paper, RockPaperScissors::Scissors) => 2 + 0,
            (RockPaperScissors::Scissors, RockPaperScissors::Rock) => 3 + 0,
            (RockPaperScissors::Scissors, RockPaperScissors::Paper) => 3 + 6,
            (RockPaperScissors::Scissors, RockPaperScissors::Scissors) => 3 + 3,
        }
    }

And the solution is

pub fn star_1(data: &PuzzleData) -> usize {
    data.strategy
        .iter()
        .map(|(a, b)| b.to_rock_paper_scissors().result(a))
        .sum()
}
Star 2

I was expecting an optimization for the second part of the kind, "Figure out what X, Y, Z need to be so you end up with the highest score possible" or "…​ so that you end up with the lowest score possible that with more than 50% wins". Maybe that would have been too much for day 2.

The scores are still calculated in the same way but the conversion is now done using

    fn for_result(&self, opponent: &RockPaperScissors) -> RockPaperScissors {
        match (&self, opponent) {
            (XYZ::X, RockPaperScissors::Rock) => RockPaperScissors::Scissors,
            (XYZ::X, RockPaperScissors::Paper) => RockPaperScissors::Rock,
            (XYZ::X, RockPaperScissors::Scissors) => RockPaperScissors::Paper,
            (XYZ::Y, _) => *opponent,
            (XYZ::Z, RockPaperScissors::Rock) => RockPaperScissors::Paper,
            (XYZ::Z, RockPaperScissors::Paper) => RockPaperScissors::Scissors,
            (XYZ::Z, RockPaperScissors::Scissors) => RockPaperScissors::Rock,
        }
    }

The solution is

pub fn star_2(data: &PuzzleData) -> usize {
    data.strategy
        .iter()
        .map(|(a, b)| b.for_result(a).result(a))
        .sum()
}
Tests

I made a mistake in my scoring function in the first place. So I wrote tests this time to debug this…​

#[cfg(test)]
mod tests {
    use super::*;

    const CONTENT: &str = r#"A Y
B X
C Z"#;

    #[test]
    pub fn test_star_1() {
        let data = PuzzleData::try_from(CONTENT).unwrap();
        assert_eq!(15, star_1(&data));
    }

    #[test]
    pub fn test_star_2() {
        let data = PuzzleData::try_from(CONTENT).unwrap();
        assert_eq!(12, star_2(&data));
    }
}
Today I learned

I think it was the first time I used Result::and_then in a (maybe) meaningful way.

Alternative based on direct calculations

I could not stop myself from implementing an alternative solution using direct calculations. Run the alternative solution with cargo run --release --features modulo

    pub fn star_1(data: &PuzzleData) -> usize {
        // 0 rock
        // 1 paper
        // 2 scissor
        // (rock - paper + 1) % 3 = 0
        // (rock - rock + 1) % 3 = 1
        // (rock - scissor + 1) % 3 = 2
        data.strategy
            .iter()
            .map(|(a, b)| ((b + 4 - a) % 3) * 3 + (b + 1))
            .sum()
    }

    pub fn star_2(data: &PuzzleData) -> usize {
        // 0 rock, 1 paper, 2 scissor
        // 0 loose, 1 draw, 2 win
        // to loose, subtract 1 (% 3), to win add 1 (% 3)
        // play (a + b - 1) % 3 -> add this in formula for first star
        data.strategy
            .iter()
            .map(|(a, b)| b * 3 + (a + b + 2) % 3 + 1)
            .sum()
    }

I also implemented a parse function which does not check any inputs and will probably result in panics if something unexpected is in the input (to parse without error handling run cargo run --release --features unchecked_parse; this will automatically use the direct calculation variant)

    impl TryFrom<&'static str> for PuzzleData {
        type Error = String;

        /// parse the puzzle input
        fn try_from(s: &'static str) -> Result<Self, Self::Error> {
            if cfg!(feature = "unchecked_parse") {
                Ok(Self {
                    strategy: s
                        .lines()
                        .map(str::as_bytes)
                        .map(|bytes| ((bytes[0] - b'A') as usize, (bytes[2] - b'X') as usize))
                        .collect(),
                })
            } else {
                s.lines()
                    .map(|l| {
                        l.split_once(' ')
                            .ok_or_else(|| format!("Could not parse line '{l}'"))
                            .and_then(|(a, b)| {
                                match a {
                                    "A" => Ok(0),
                                    "B" => Ok(1),
                                    "C" => Ok(2),
                                    _ => Err(format!("Expected one of A, B, C, found '{a}'")),
                                }
                                .and_then(|a| match b {
                                    "X" => Ok((a, 0)),
                                    "Y" => Ok((a, 1)),
                                    "Z" => Ok((a, 2)),
                                    _ => Err(format!("Expected one of X, Y, Z, found '{b}'")),
                                })
                            })
                    })
                    .collect::<Result<Vec<_>, _>>()
                    .map(|strategy| Self { strategy })
            }
        }
    }

Interestingly, not doing any error handling in parsing the input does not lead to any measurable speed-up. Maybe this is because the overall solution time is so small, that the differences are not distinguishable from noise?

Day 03: rust

Day 3: Rucksack Reorganization

Rust solution to AoC|2022|3.

Input

I parse the input directly into a vec of vecs of bytes, each representing the priority of the items contained in the rucksacks.

I was tempted to think about using sets for quicker contains operations, but given the size of the problem, this is most likely not worth it.

pub mod input {
    #[derive(Debug)]
    pub struct PuzzleData {
        pub rucksacks: Vec<Vec<u8>>,
    }

    impl TryFrom<&'static str> for PuzzleData {
        type Error = &'static str;

        /// parse the puzzle input
        fn try_from(s: &'static str) -> Result<Self, Self::Error> {
            s.lines()
                .map(|l| {
                    l.as_bytes()
                        .iter()
                        .map(|&b| match b {
                            b'a'..=b'z' => Ok(b - b'a' + 1),
                            b'A'..=b'Z' => Ok(b - b'A' + 27),
                            _ => Err("Unexpected bytes in input"),
                        })
                        .collect::<Result<Vec<_>, _>>()
                })
                .collect::<Result<Vec<_>, _>>()
                .map(|rucksacks| Self { rucksacks })
        }
    }
}
Star 1

Star 1 is about finding the item in the first compartment (first half) of the rucksack, which is also contained in the secod half.

Some simplifications work because / if the input is correct.

  • I assume that a common item always exists and therefore do not limit the search to the first half (it will stop once an item is found, which will be in the first half)

  • The find function returns an Option. If there were any None values, they would be simply discarded using filter_map (actually, for part 1, there can be no None values, because search is not stopped in the first half, so the first element of the second half would be found, if there is no common element between first and second half. so a simple unwrap would work as well).

  • The chunks_exact function makes sure that every chunk has exactly three elements. If the overall number of rucksacks was not a multiple of three, the remaining rucksacks would simply be discarded.

I use a fold instead of sum to do type conversion on the fly (u8 would not be big enough to hold the sum).

pub fn star_1(data: &PuzzleData) -> usize {
    data.rucksacks
        .iter()
        .filter_map(|rucksack| {
            rucksack
                .iter()
                .find(|item| rucksack.as_slice()[rucksack.len() / 2..].contains(item))
        })
        .fold(0usize, |sum, item| sum + *item as usize)
}
Star 2

Star 2 is a simple modification, where we look for items that are common for groups of three consecutive rucksacks

pub fn star_2(data: &PuzzleData) -> usize {
    data.rucksacks
        .chunks_exact(3)
        .filter_map(|group| {
            group[0]
                .iter()
                .find(|item| group[1].contains(item) && group[2].contains(item))
        })
        .fold(0usize, |sum, item| sum + *item as usize)
}
Tests

Tests use the example given in the puzzle.

#[cfg(test)]
mod tests {
    use super::*;

    const CONTENT: &str = r#"vJrwpWtwJgWrhcsFMMfFFhFp
jqHRNqRjqzjGDLGLrsFMfFZSrLrFZsSL
PmmdzqPrVvPwwTWBwg
wMqvLMZHhHMvwLHjbvcjnnSBnvTQFn
ttgJtRGJQctTZtZT
CrZsJsPPZsGzwwsLwLmpwMDw"#;

    #[test]
    pub fn test_star_1() {
        let data = PuzzleData::try_from(CONTENT).unwrap();
        assert_eq!(157, star_1(&data));
    }

    #[test]
    pub fn test_star_2() {
        let data = PuzzleData::try_from(CONTENT).unwrap();
        assert_eq!(70, star_2(&data));
    }
}
Today I learned

... that sometimes it is as simple as it appears at first view.

Day 04: rust

Day 4: Camp Cleanup

Rust solution to AoC|2022|4.

Input

Today, input parsing was the biggest challenge. Mainly because I decided to not use unwrap but try some proper error handling and to avoid intermediate collect calls. Until I figured out that the code is much simplified if I create a separate function to parse a single line (because ? can be used in that function but not in a clojure within an iterator’s map function), I had to use a lot of and_then, map, map_error, …​

So here is my parsing functions (admittedly not how it looked like when I submitted my results)

pub mod input {
    use mr_kaffee_aoc::err::PuzzleError;

    #[derive(Debug)]
    pub struct PuzzleData {
        pub range_pairs: Vec<((usize, usize), (usize, usize))>,
    }

    fn parse_pair(line: &str) -> Result<((usize, usize), (usize, usize)), PuzzleError> {
        let mut iter = line.split(|c: char| c == '-' || c == ',');
        Ok((
            (
                iter.next()
                    .ok_or_else(|| format!("Missing start 1 '{line}'"))?
                    .parse()?,
                iter.next()
                    .ok_or_else(|| format!("Missing end 1 in '{line}'"))?
                    .parse()?,
            ),
            (
                iter.next()
                    .ok_or_else(|| format!("Missing start 2 in '{line}'"))?
                    .parse()?,
                iter.next()
                    .ok_or_else(|| format!("Missing end 2 in '{line}'"))?
                    .parse()?,
            ),
        ))
    }

    impl TryFrom<&'static str> for PuzzleData {
        type Error = PuzzleError;

        /// parse the puzzle input
        fn try_from(s: &'static str) -> Result<Self, Self::Error> {
            s.lines()
                .map(parse_pair)
                .collect::<Result<Vec<_>, _>>()
                .map(|range_pairs| Self { range_pairs })
        }
    }
}
Star 1

The actual solution is simple for part 1 …​

pub fn star_1(data: &PuzzleData) -> usize {
    // count how often one range is contained in the other
    data.range_pairs
        .iter()
        .filter(|((start1, end1), (start2, end2))| {
            (start1 <= start2 && end2 <= end1) || (start2 <= start1 && end1 <= end2)
        })
        .count()
}
Star 2

... as well as for part 2 (I was afraid to be asked to look for overlaps across pairs in part 2 …​)

pub fn star_2(data: &PuzzleData) -> usize {
    // count how often ranges overlap, i.e., start of one range is contained in the other range
    data.range_pairs
        .iter()
        .filter(|((start1, end1), (start2, end2))| {
            (start1 <= start2 && start2 <= end1) || (start2 <= start1 && start1 <= end2)
        })
        .count()
}
Tests

Today I even did test-driven development in the sense that I did not write any functional code before I had a failing test case ;)

#[cfg(test)]
mod tests {
    use super::*;
    use mr_kaffee_aoc::err::PuzzleError;

    const CONTENT: &str = r#"2-4,6-8
2-3,4-5
5-7,7-9
2-8,3-7
6-6,4-6
2-6,4-8"#;

    #[test]
    pub fn test_star_1() -> Result<(), PuzzleError> {
        let data = PuzzleData::try_from(CONTENT)?;
        assert_eq!(2, star_1(&data));
        Ok(())
    }

    #[test]
    pub fn test_star_2() -> Result<(), PuzzleError> {
        let data = PuzzleData::try_from(CONTENT)?;
        assert_eq!(4, star_2(&data));
        Ok(())
    }
}
Today I learned

and_then - map - and_then - map - map_err …​ is all not needed if some parsing functionality for one line is moved to a separate function where the ? shortcut operator can be used for error propagation and conversion

Day 05: rust

Day 5: Supply Stacks

Rust solution to AoC|2022|5.

Today, there where two main challenges for me:

  1. Parsing the input

  2. Rust’s mutability & borrowing concept for part 2

In addition, it turns out quite complicated to handle possible errors (all kind of invalid moves, empty stacks at end of moves, …​). All this effort for code that is never used, because the puzzle inputs are well-formed. I think I will stop this excercise and use plain unwrap again for subsequent days.

Input

Today, it was not just "process input line by line", but in the end almost …​

First, I split the input in one part describing the stacks and a second part describing the moves. Parsing the moves in tuples (n, from, to) is easy. I make the from and to parts zero-based on the fly.

Parsing the stacks of crates is a bit more tricky. I did not (could not) use a simple iterator / collect scheme but allocated the stacks upfront and than process line by line starting at the last and pushing elements on the stack.

pub mod input {
    use mr_kaffee_aoc::err::PuzzleError;

    #[derive(Debug, PartialEq, Eq)]
    pub struct PuzzleData {
        stacks: Vec<Vec<char>>,
        moves: Vec<(usize, usize, usize)>,
    }

    fn parse_move(line: &str, len: usize) -> Result<(usize, usize, usize), PuzzleError> {
        let mut parts = line.split(" ");

        parts.next(); // skip "move"
        let n = parts
            .next()
            .ok_or_else(|| format!("Missing number in move '{line}'"))?
            .parse::<usize>()?;
        parts.next(); // skip "from"
        let from = parts
            .next()
            .ok_or_else(|| format!("Missing from in move '{line}'"))?
            .parse::<usize>()?
            - 1;
        parts.next(); // skip "to"
        let to = parts
            .next()
            .ok_or_else(|| format!("Missing to in move '{line}'"))?
            .parse::<usize>()?
            - 1;

        if from >= len || to >= len {
            Err(format!("Invalid move: '{line}', <from>, <to> <= {len} required.").into())
        } else if from == to {
            Err(format!("Invalid move: '{line}', <from> != <to> required.").into())
        } else {
            Ok((n, from, to))
        }
    }

    fn parse_crate_layer(stacks: &mut Vec<Vec<char>>, line: &str) {
        for (k, item) in line
            .chars()
            .skip(1)
            .step_by(4)
            .enumerate()
            .filter(|(_, item)| *item != ' ')
        {
            while k >= stacks.len() {
                stacks.push(Vec::new());
            }
            stacks[k].push(item);
        }
    }

    impl TryFrom<&'static str> for PuzzleData {
        type Error = PuzzleError;

        /// parse the puzzle input
        fn try_from(s: &'static str) -> Result<Self, Self::Error> {
            let (stacks_part, moves_part) = s
                .split_once("\n\n")
                .ok_or("Could not separate crates from moves")?;

            let mut stacks: Vec<Vec<char>> = vec![];
            for line in stacks_part.lines().rev().skip(1) {
                parse_crate_layer(&mut stacks, line);
            }

            let moves = moves_part
                .lines()
                .map(|line| parse_move(line, stacks.len()))
                .collect::<Result<Vec<_>, _>>()?;

            Ok(PuzzleData { stacks, moves })
        }
    }

    impl PuzzleData {
        /// get moves
        pub fn moves(&self) -> &[(usize, usize, usize)] {
            &self.moves
        }

        /// get cloned crates
        pub fn stacks(&self) -> Vec<Vec<char>> {
            self.stacks.clone()
        }
    }
}
Star 1

This is straight forward. Just process move by move, and pop from one stack / push to the other stack the correct number of times.

fn msg(stacks: &[Vec<char>]) -> Result<String, PuzzleError> {
    stacks
        .iter()
        .map(|c| {
            c.last().ok_or_else(|| {
                PuzzleError::from(format!(
                    "Can't construct message. Empty stack in {stacks:?}"
                ))
            })
        })
        .collect()
}

pub fn star_1(data: &PuzzleData) -> Result<String, PuzzleError> {
    let mut stacks = data.stacks();
    for (n, from, to) in data.moves() {
        for _ in 0..*n {
            let item = stacks[*from]
                .pop()
                .ok_or_else(|| format!("Tried to pop from empty stack {from}, stacks: {stacks:?}"))?;
            stacks[*to].push(item);
        }
    }

    msg(&stacks)
}
Star 2

This is slightly more tricky. Now we have to pop a complete pack of items and push that on top of another stack preserving the order.

The challenging part of it is that Rust does not allow mutable references to two items in a vec at the same time. My first solution used intermediate storage. My current solution uses slice::split_at_mut to circumvent this (speed-up by a factor 3 to 4 for part 2 compared to intermediate storage). The code gets a bit complicated though — I extracted the complicated part to a function mut_references.

fn mut_references<T>(v: &mut Vec<T>, idx1: usize, idx2: usize) -> (&mut T, &mut T) {
    if idx1 > idx2 {
        let (left, right) = v.split_at_mut(idx1);
        (&mut right[0], &mut left[idx2])
    } else {
        let (left, right) = v.split_at_mut(idx2);
        (&mut left[idx1], &mut right[0])
    }
}

pub fn star_2(data: &PuzzleData) -> Result<String, PuzzleError> {
    let mut stacks = data.stacks();
    for (n, from, to) in data.moves() {
        // I need a mutable reference to the from and the to part at the same time
        // to avoid creating intermediate storage
        let (source, dest) = mut_references(&mut stacks, *from, *to);

        let len = source.len();
        if *n > len {
            return Err(format!(
                "Trying to pop {n} elements from stack {from} containing {len}, stacks: {stacks:?}"
            )
            .into());
        }
        for item in source.drain(len - n..) {
            dest.push(item);
        }
    }

    msg(&stacks)
}
Tests

And there are some tests.

Since parsing was not totally obvious, I did an additional test for this part …​

#[cfg(test)]
mod tests {
    use super::*;

    const CONTENT: &str = r#"    [D]
[N] [C]
[Z] [M] [P]
 1   2   3

move 1 from 2 to 1
move 3 from 1 to 3
move 2 from 2 to 1
move 1 from 1 to 2"#;

    #[test]
    pub fn test_parse() {
        let data = PuzzleData::try_from(CONTENT).unwrap();
        assert_eq!(
            vec![vec!['Z', 'N'], vec!['M', 'C', 'D'], vec!['P']],
            data.stacks()
        );
        assert_eq!(&[(1, 1, 0), (3, 0, 2), (2, 1, 0), (1, 0, 1)], data.moves());
    }

    #[test]
    pub fn test_star_1() {
        let data = PuzzleData::try_from(CONTENT).unwrap();
        assert_eq!("CMZ", star_1(&data).unwrap());
    }

    #[test]
    pub fn test_star_2() {
        let data = PuzzleData::try_from(CONTENT).unwrap();
        assert_eq!("MCD", star_2(&data).unwrap());
    }
}
Today I learned

How to mutably access two elements of one vec in Rust.

Day 06: rust

Day 6: Tuning Trouble

Rust solution to AoC|2022|6.

Today I totally screwed up for the second star for no specific reason :(

Too little sleep probably.

Input

I parse the input directly into a slice of bytes. That should be very cheap…​

pub mod input {
    use std::convert::Infallible;

    #[derive(Debug)]
    pub struct PuzzleData {
        pub stream: &'static [u8],
    }

    impl TryFrom<&'static str> for PuzzleData {
        type Error = Infallible;

        /// parse the puzzle input
        fn try_from(s: &'static str) -> Result<Self, Self::Error> {
            Ok(PuzzleData {
                stream: s.trim().as_bytes(),
            })
        }
    }
}
Star 1

Hand-crafted solution

pub fn star_1(data: &PuzzleData) -> usize {
    let (k, _) = data
        .stream
        .windows(4)
        .enumerate()
        .find(|(_, s)| match s {
            &[a, b, c, d] => a != b && a != c && a != d && b != c && b != d && c != d,
            _ => unreachable!(),
        })
        .unwrap();

    k + 4
}
Star 2

Solution with a bit of iterators.

It feels bad to search the same thing again and again. If a duplicate is found in a window, the search could skip everything until the character just after the first occurance of the duplicate. I implemented this as an alternative (see below)

pub fn star_2(data: &PuzzleData) -> usize {
    let (k, _) = data
        .stream
        .windows(14)
        .enumerate()
        .find(|(_, w)| {
            w.iter()
                .enumerate()
                .skip(1)
                .all(|(p, c1)| w.iter().take(p).all(|c2| c1 != c2))
        })
        .unwrap();
    k + 14
}
Alternative

As an alternative, I implemented a generic solution which skips parts of the search already covered. Interestingly, this solution tends to be slightly slower (the difference is close to measurement noise in my not very professional time measurements)

pub fn find_distinct(stream: &[u8], n: usize) -> usize {
    let mut k = 0;
    while k < stream.len() - n {
        match (k + 1..k + n).find(|p| stream[*p..k + n].contains(&stream[*p - 1])) {
            Some(q) => k = q,
            None => return k + n,
        }
    }

    panic!("No solution.");
}
Tests

Tests for all variants

#[cfg(test)]
mod tests {
    use super::*;

    const CONTENT: &str = r#"mjqjpqmgbljsphdztnvjfqwrcgsmlb"#;

    #[test]
    pub fn test_star_1() {
        let data = PuzzleData::try_from(CONTENT).unwrap();
        assert_eq!(7, star_1(&data));
    }

    #[test]
    pub fn test_star_2() {
        let data = PuzzleData::try_from(CONTENT).unwrap();
        assert_eq!(19, star_2(&data));
    }

    #[test]
    pub fn test_find_distinct() {
        let data = PuzzleData::try_from(CONTENT).unwrap();
        assert_eq!(7, find_distinct(data.stream, 4));
        assert_eq!(19, find_distinct(data.stream, 14));
    }
}
Today I learned

Take a second cup of coffee before solving the puzzle, don’t forget to wear glasses, and

  • it is cool to not allocate any intermediate storage but work directly on byte slices.

  • it is possible to destructure slices with match

Day 07: rust

Day 7: No Space Left On Device

Rust solution to AoC|2022|7.

When reading the puzzle today, I thought it would get complicated. Finally, it was quite straight-forward.

Input

The description asks for recursion / recursive data structures, which I find a pain in Rust. My solution is to create a vec of Directory elements. Each directory contains references to children and the parent directory as index to this vec.

The directories are created by processing the input line by line. When an ls command yields a directory, a new Directory element is added, its index is added to the children to of the current directory and its parent is set to the current directorie’s index.

File sizes are directly summed up (initially, child files were stored in a vec, but once part 2 was unveiled, it was clear that the sum is the only thing needed)

pub mod input {
    use std::{collections::HashMap, convert::Infallible};

    #[derive(Debug)]
    pub struct Directory {
        parent: Option<usize>,
        children: HashMap<&'static str, usize>,
        size: usize,
    }

    impl Directory {
        fn new(parent: Option<usize>) -> Self {
            Self {
                parent,
                children: HashMap::new(),
                size: 0,
            }
        }

        pub fn total_size(&self, dirs: &[Directory]) -> usize {
            self.size
                + self
                    .children
                    .values()
                    .map(|idx| dirs[*idx].total_size(dirs))
                    .sum::<usize>()
        }
    }

    #[derive(Debug)]
    pub struct PuzzleData {
        dirs: Vec<Directory>,
    }

    impl TryFrom<&'static str> for PuzzleData {
        type Error = Infallible;

        /// parse the puzzle input
        fn try_from(s: &'static str) -> Result<Self, Self::Error> {
            let mut dirs = vec![Directory::new(None)];

            let mut current = 0;
            for line in s.lines() {
                match line {
                    "$ cd /" => current = 0,
                    "$ cd .." => current = dirs[current].parent.unwrap(),
                    "$ ls" => (),
                    _ if line.starts_with("$ cd ") => current = dirs[current].children[&line[5..]],
                    _ if line.starts_with("dir ") => {
                        let dir = dirs.len();
                        dirs.push(Directory::new(Some(current)));
                        dirs[current].children.insert(&line[4..], dir);
                    }
                    _ => {
                        dirs[current].size +=
                            line[..line.find(' ').unwrap()].parse::<usize>().unwrap();
                    }
                }
            }

            Ok(Self { dirs })
        }
    }

    impl PuzzleData {
        /// immutable access to directories as slice
        pub fn dirs(&self) -> &[Directory] {
            &self.dirs
        }
    }
}
Star 1

Simple iter - filter - fold

pub fn star_1(data: &PuzzleData) -> usize {
    data.dirs()
        .iter()
        .map(|d| d.total_size(data.dirs()))
        .filter(|&s| s <= 100_000)
        .sum()
}
Star 2

Another simple iter - filter - fold

pub fn star_2(data: &PuzzleData) -> usize {
    let required = data.dirs()[0].total_size(data.dirs()) - 40_000_000;

    data.dirs()
        .iter()
        .map(|d| d.total_size(data.dirs()))
        .filter(|&s| s >= required)
        .fold(usize::MAX, |mn, s| mn.min(s))
}
Tests

The standard tests.

#[cfg(test)]
mod tests {
    use super::*;

    const CONTENT: &str = r#"$ cd /
$ ls
dir a
14848514 b.txt
8504156 c.dat
dir d
$ cd a
$ ls
dir e
29116 f
2557 g
62596 h.lst
$ cd e
$ ls
584 i
$ cd ..
$ cd ..
$ cd d
$ ls
4060174 j
8033020 d.log
5626152 d.ext
7214296 k"#;

    #[test]
    pub fn test_star_1() {
        let data = PuzzleData::try_from(CONTENT).unwrap();
        assert_eq!(95_437, star_1(&data))
    }

    #[test]
    pub fn test_star_2() {
        let data = PuzzleData::try_from(CONTENT).unwrap();
        assert_eq!(24_933_642, star_2(&data))
    }
}

Day 08: rust

Day 8: Treetop Tree House

Rust solution to AoC|2022|8.

I took a moment to put my brain in the condition to think in the 2D grid…​

Input

I directly use the bytes from the input. I scan for the first occurrence of a line break. Its index is the width of the grid. The PuzzleData struct’s implementation has an additional field for the height of the grid.

pub mod input {
    #[derive(Debug)]
    pub struct PuzzleData {
        pub trees: &'static [u8],
        pub w: usize,
        pub h: usize,
    }

    impl From<&'static str> for PuzzleData {
        /// parse the puzzle input
        fn from(s: &'static str) -> Self {
            let trees = s.as_bytes();
            let w = s.find('\n').unwrap();
            let h = trees.len() / (w + 1);
            Self { trees, w, h }
        }
    }
}
Star 1

The function is_visible verifies in all four directions (left, right, top, bottom), that all trees up to the boundary are smaller. It stops after one such direction is found.

    pub fn is_visible(&self, x: usize, y: usize) -> bool {
        let h = self.trees[x + (self.w + 1) * y];

        let fx = |x_: usize| self.trees[x_ + (self.w + 1) * y] < h;
        let fy = |y_: usize| self.trees[x + (self.w + 1) * y_] < h;

        (0..x).all(fx) || (x + 1..self.w).all(fx) || (0..y).all(fy) || (y + 1..self.h).all(fy)
    }

Then I count in the star_1 function, how many trees are visible. For a tree on the boundary, is_visible always returns true, so no need to handle the boundary separately.

pub fn star_1(data: &PuzzleData) -> usize {
    (0..data.w)
        .map(|x| (0..data.h).filter(|&y| data.is_visible(x, y)).count())
        .sum::<usize>()
}
Star 2

Similar to star 1. This time, I use the function scenic_score to calculate the scenic score of each tree and the function star_2 to find the maximum.

In the scenic score calculations, different to the visibility check, the traversal order matters. For left and top directions, the direction is reversed.

    pub fn scenic_score(&self, x: usize, y: usize) -> usize {
        let h = self.trees[x + (self.w + 1) * y];

        let fx = |&x_: &usize| self.trees[x_ + (self.w + 1) * y] >= h;
        let fy = |&y_: &usize| self.trees[x + (self.w + 1) * y_] >= h;

        (x - (0..x).rev().find(fx).unwrap_or(0))
            * ((x + 1..self.w).find(fx).unwrap_or(self.w - 1) - x)
            * (y - (0..y).rev().find(fy).unwrap_or(0))
            * ((y + 1..self.h).find(fy).unwrap_or(self.h - 1) - y)
    }
pub fn star_2(data: &PuzzleData) -> usize {
    (0..data.w)
        .map(|x| (0..data.h).map(|y| data.scenic_score(x, y)).max().unwrap())
        .max()
        .unwrap()
}
Tests

Tests for star_1, scenic_score and star_2 functions based on example defined in puzzle description.

#[cfg(test)]
mod tests {
    use super::*;

    const CONTENT: &str = r#"30373
25512
65332
33549
35390
"#;

    #[test]
    pub fn test_star_1() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(21, star_1(&data));
    }

    #[test]
    pub fn test_scenic_score() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(4, data.scenic_score(2, 1));
    }

    #[test]
    pub fn test_star_2() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(8, star_2(&data));
    }
}

Day 09: rust

Day 9: Rope Bridge

Rust solution to AoC|2022|9.

Knots are moving around, following each other. The difficulty is to figure out where you have your off by one errors or similar. In the end, I had a print function showing current configurations to debug my code (because it actually looks nice, you may want to have a look at the output with cargo test test_star_2 -- --nocapture):

/// Print knots for debugging purposes
///
/// Knots are printed by consecutive letters `a`, `b`, ... starting with the head at `a`
///
/// If a knot sits on a spot which is already seen, it is represented by a capital letter `A`, `B`, ...
///
/// The start is indicated by `$` if no knot is located at the start
///
/// Spots that have been seen and where currently no knot is located are shown as `#`
///
/// If several knots sit on top of each other, the first one in the chain is shown.
pub fn print(
    rx: RangeInclusive<isize>,
    ry: RangeInclusive<isize>,
    knots: &[(isize, isize)],
    ((dx, dy), s): ((isize, isize), usize),
    seen: &HashSet<(isize, isize)>,
) {
    println!("\nAfter moving {s} times by ({dx}, {dy}):");
    for y in ry {
        for x in rx.clone() {
            let seen = seen.contains(&(x, y));
            match knots
                .iter()
                .enumerate()
                .find(|(_, (xn, yn))| *xn == x && *yn == y)
            {
                Some((v, _)) if seen => print!("{}", (b'A' + v as u8) as char),
                Some((v, _)) => print!("{}", (b'a' + v as u8) as char),
                None if x == 0 && y == 0 => print!("$"),
                _ if seen => print!("#"),
                _ => print!("\u{00b7}"),
            }
        }
        println!();
    }
}
Input

I parse the input in a vec of tuples ((dx, dy), s) where dx and dy are the horizontal / vertical unit step changes and s is the number of steps.

Not allocating extra memory for today’s input parsing is not for me ;)

pub mod input {
    #[derive(Debug)]
    pub struct PuzzleData {
        moves: Vec<((isize, isize), usize)>,
    }

    impl From<&str> for PuzzleData {
        /// parse the puzzle input
        fn from(s: &str) -> Self {
            Self {
                moves: s
                    .lines()
                    .map(|l| match (l.as_bytes()[0], l[2..].parse().unwrap()) {
                        (b'U', s) => ((0, -1), s),
                        (b'D', s) => ((0, 1), s),
                        (b'L', s) => ((-1, 0), s),
                        (b'R', s) => ((1, 0), s),
                        _ => panic!("No valid move: {l}"),
                    })
                    .collect(),
            }
        }
    }

    impl PuzzleData {
        pub fn moves(&self) -> &[((isize, isize), usize)] {
            &self.moves
        }
    }
}
Solution

After a little bit of refactoring, one solution works for both parts today. The solve function is called with the total number of knots (including head) as a parameter.

In each step, it updates the head and then all subsequent knots ony by one.

The debug function parameter is a clojure that is called after each move for debugging purposes. In the actual solution, an empty function is used. In the test cases, the print function is called.

pub fn solve<F>(data: &PuzzleData, n: usize, debug: F) -> usize
where
    F: Fn(&[(isize, isize)], ((isize, isize), usize), &HashSet<(isize, isize)>) -> (),
{
    let mut seen: HashSet<(isize, isize)> = HashSet::from([(0, 0)]);
    let mut knots: Vec<(isize, isize)> = vec![(0, 0); n];

    for ((dx, dy), s) in data.moves() {
        for _ in 0..*s {
            knots[0].0 += *dx;
            knots[0].1 += *dy;

            for k in 1..n {
                let dx = knots[k].0 - knots[k - 1].0;
                let dy = knots[k].1 - knots[k - 1].1;

                knots[k].0 += (dx < -1 || (dx == -1 && dy.abs() > 1)) as isize
                    - (dx > 1 || (dx == 1 && dy.abs() > 1)) as isize;

                knots[k].1 += (dy < -1 || (dy == -1 && dx.abs() > 1)) as isize
                    - (dy > 1 || (dy == 1 && dx.abs() > 1)) as isize;
            }

            seen.insert(knots[n - 1]);
        }

        debug(&knots, ((*dx, *dy), *s), &seen);
    }

    seen.len()
}
Tests

The usual tests:

#[cfg(test)]
mod tests {
    use super::*;

    const CONTENT: &str = r#"R 4
U 4
L 3
D 1
R 4
D 1
L 5
R 2
"#;

    const CONTENT_2: &str = r#"R 5
U 8
L 8
D 3
R 17
D 10
L 25
U 20
"#;

    #[test]
    pub fn test_try_from() {
        let data = PuzzleData::from(CONTENT);
        println!("{data:?}");
    }

    #[test]
    pub fn test_star_1() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(
            13,
            solve(&data, 2, |knots, mv, seen| print(
                0..=5,
                -5..=0,
                knots,
                mv,
                seen
            ))
        );
    }

    #[test]
    pub fn test_star_2() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(1, solve(&data, 10, |_, _, _| ()));

        let data = PuzzleData::from(CONTENT_2);
        assert_eq!(
            36,
            solve(&data, 10, |knots, mv, seen| print(
                -11..=14,
                -15..=5,
                knots,
                mv,
                seen
            ))
        );
    }
}
Today I learned

That sometimes it is helpful to write as _ to give the rust compiler a hint to perform type coercion. Although in the end, I did not use it in my solution.

And, today for the first time, I used my scripted solution to submit results. Not because it is faster (who cares about a few seconds) but because it is fun.

Day 10: rust

Day 10: Cathode-Ray Tube

Rust solution to AoC|2022|10.

Today is about a simple CPU which is used to control a simple display (a cathode-ray tube)

Input

Parsing the input is straight forward. Each line represents either an addx operation or a noop operation

pub mod input {
    #[derive(Debug, PartialEq, Eq, Clone, Copy)]
    pub enum Instr {
        NoOp,
        AddX(isize),
    }

    #[derive(Debug)]
    pub struct PuzzleData {
        instructions: Vec<Instr>,
    }

    impl From<&'static str> for PuzzleData {
        /// parse the puzzle input
        fn from(s: &'static str) -> Self {
            Self {
                instructions: s
                    .lines()
                    .map(|l| match l {
                        "noop" => Instr::NoOp,
                        _ => Instr::AddX(l[5..].parse().unwrap()),
                    })
                    .collect(),
            }
        }
    }

    impl PuzzleData {
        pub fn instructions(&self) -> &[Instr] {
            &&self.instructions
        }
    }
}
Star 1

I created a small model of the CPU:

pub struct Cpu {
    instructions: Vec<Instr>,
    pointer: usize,
    cycle: usize,
    timer: usize,
    x: isize,
}

impl Cpu {
    pub fn init(instructions: &[Instr]) -> Self {
        let mut cpu = Self {
            instructions: Vec::from(instructions),
            pointer: 0,
            cycle: 0,
            timer: 0,
            x: 1,
        };

        // set initial timer if required
        if let Instr::AddX(_) = cpu.instructions[0] {
            cpu.timer = 1;
        }

        cpu
    }

    pub fn step(&mut self) {
        // should not be called if no more instructions
        assert!(self.pointer < self.instructions.len(), "Cpu halted");

        if self.timer > 0 {
            // only decrement timer
            self.timer -= 1;
        } else {
            // apply increment
            if let Instr::AddX(inc) = self.instructions[self.pointer] {
                self.x += inc;
            }

            // increment pointer
            self.pointer += 1;

            // set timer if required
            if self.pointer < self.instructions.len() {
                if let Instr::AddX(_) = self.instructions[self.pointer] {
                    self.timer = 1;
                }
            }
        }

        // increment cycle counter
        self.cycle += 1;
    }
}

The most difficult part was to read the puzzle description carefully. Specifically the part were it says to calculate the signal strength during and not after the 20th, 60th, …​ cycle.

The rest was just applying the instructions giving in the puzzle.

pub fn star_1(data: &PuzzleData) -> isize {
    let mut cpu = Cpu::init(data.instructions());

    // we first look during 20th cycle, i.e., after 19th cycle
    for _ in 0..19 {
        cpu.step();
    }
    let mut v = (cpu.cycle as isize + 1) * cpu.x;

    // 5 blocks of 40 cycles
    for _ in 0..5 {
        for _ in 0..40 {
            cpu.step();
        }
        v += (cpu.cycle as isize + 1) * cpu.x;
    }

    v
}
Star 2

Again, reading carefully is important. I did not get the meaning of

In this system, there is no such thing as "vertical position"

After having that fixed, it was again straight forward. It somehow feels incorrect to manually read the "LCD" to produce the result to enter at the website, hence I created a little helper which recognizes the letters. Since there is nothing to recognize for the sample data, I put the recognition in a separate function to still be able to run my code on the examples.

pub fn solve_2(data: &PuzzleData) -> [u8; 240] {
    const W: usize = 40;
    const H: usize = 6;

    // initialize lcd with new lines, W + 1 to keep new lines at the and
    let mut lcd = [b'.'; W * H];

    let mut cpu = Cpu::init(data.instructions());
    for row in 0..H {
        for col in 0..W {
            lcd[col as usize + W * row] = if (cpu.x - 1..=cpu.x + 1).contains(&(col as _)) {
                LIT as u8
            } else {
                DARK as u8
            };
            cpu.step();
        }
    }

    lcd
}

pub fn star_2(data: &PuzzleData) -> String {
    solve_2(data).decode(0).unwrap()
}
Tests

A bit of tests, as usual …​

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    pub fn test_try_from() {
        let data = PuzzleData::from(CONTENT);
        println!("{data:?}");
    }

    #[test]
    pub fn test_star_1() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(13_140, star_1(&data));
    }

    #[test]
    pub fn test_solve_2() {
        let data = PuzzleData::from(CONTENT);
        let b = solve_2(&data);
        let mut s = String::with_capacity(41 * 6);
        for row in 0..6 {
            for col in 0..40 {
                s.push(b[col + 40 * row] as _);
            }
            s.push('\n');
        }
        assert_eq!(EXP_2, s);
    }

    #[test]
    pub fn test_simple() {
        let data = PuzzleData::from(CONTENT_SIMPLE);
        let mut cpu = Cpu::init(data.instructions());
        let r = (0..5)
            .map(|_| {
                cpu.step();
                cpu.x
            })
            .collect::<Vec<_>>();
        let exp: &[isize] = &[1, 1, 4, 4, -1];
        assert_eq!(exp, r);
    }

    const CONTENT_SIMPLE: &str = r#"noop
addx 3
addx -5"#;

    const EXP_2: &str = r"##..##..##..##..##..##..##..##..##..##..
###...###...###...###...###...###...###.
####....####....####....####....####....
#####.....#####.....#####.....#####.....
######......######......######......####
#######.......#######.......#######.....
";

    const CONTENT: &str = r#"addx 15
addx -11
addx 6
addx -3
addx 5
addx -1
addx -8
addx 13
addx 4
noop
addx -1
addx 5
addx -1
addx 5
addx -1
addx 5
addx -1
addx 5
addx -1
addx -35
addx 1
addx 24
addx -19
addx 1
addx 16
addx -11
noop
noop
addx 21
addx -15
noop
noop
addx -3
addx 9
addx 1
addx -3
addx 8
addx 1
addx 5
noop
noop
noop
noop
noop
addx -36
noop
addx 1
addx 7
noop
noop
noop
addx 2
addx 6
noop
noop
noop
noop
noop
addx 1
noop
noop
addx 7
addx 1
noop
addx -13
addx 13
addx 7
noop
addx 1
addx -33
noop
noop
noop
addx 2
noop
noop
noop
addx 8
noop
addx -1
addx 2
addx 1
noop
addx 17
addx -9
addx 1
addx 1
addx -3
addx 11
noop
noop
addx 1
noop
addx 1
noop
noop
addx -13
addx -19
addx 1
addx 3
addx 26
addx -30
addx 12
addx -1
addx 3
addx 1
noop
noop
noop
addx -9
addx 18
addx 1
addx 2
noop
noop
addx 9
noop
noop
noop
addx -1
addx 2
addx -37
addx 1
addx 3
noop
addx 15
addx -21
addx 22
addx -6
addx 1
noop
addx 2
addx 1
noop
addx -10
noop
noop
addx 20
addx 1
addx 2
addx 2
addx -6
addx -11
noop
noop
noop
"#;
}

Day 11: rust

Day 11: Monkey in the Middle

Rust solution to AoC|2022|11.

It was quite tedious today for me to create the solution.

I liked the twist in part 2. Since each monkey does a modulo test before passing on the item, we can safely do a modulo with the product of all tests of all monkeys to keep the numbers small. Still took me a while to complete part two because I just replaced the / 3 from the first part with / mod in the second part which is obviously not the same as % mod. And I have to confess that my first attempt was to use 128bit numbers.

Input

Parsing was a pain today. I almost regret my decision to not use external dependencies (and thus not use Regex) …​

pub mod input {
    #[derive(Debug, PartialEq, Eq, Clone, Copy)]
    pub enum Operation {
        Plus(usize),
        Times(usize),
        Square,
        Double,
    }

    impl Operation {
        pub fn apply(&self, item: usize) -> usize {
            match self {
                Operation::Plus(v) => item + v,
                Operation::Times(v) => item * v,
                Operation::Square => item * item,
                Operation::Double => item + item,
            }
        }
    }

    #[derive(Debug, PartialEq, Eq, Clone)]
    pub struct Monkey {
        pub worries: Vec<usize>,
        pub upd: Operation,
        pub test: usize,
        pub if_true: usize,
        pub if_false: usize,
    }

    impl From<&'static str> for Monkey {
        fn from(monkey: &'static str) -> Self {
            let words = monkey.split_ascii_whitespace();

            let mut words = words.skip(4); // Monkey <id>: Starting items:

            let mut worries = Vec::new();
            let mut word = words.next().unwrap();
            while word != "Operation:" {
                worries.push(word.trim_end_matches(',').parse().unwrap());
                word = words.next().unwrap();
            }

            let mut words = words.skip(3); // new = old

            let upd = match (words.next().unwrap(), words.next().unwrap()) {
                ("*", "old") => Operation::Square,
                ("+", "old") => Operation::Double,
                ("*", v) => Operation::Times(v.parse().unwrap()),
                ("+", v) => Operation::Plus(v.parse().unwrap()),
                _ => unreachable!(),
            };

            let mut words = words.skip(3); // Test: divisible by

            let test = words.next().unwrap().parse().unwrap();

            let mut words = words.skip(5); // If true: throw to monkey

            let if_true = words.next().unwrap().parse().unwrap();

            let mut words = words.skip(5); // If false: throw to monkey

            let if_false = words.next().unwrap().parse().unwrap();

            Self {
                worries,
                upd,
                test,
                if_true,
                if_false,
            }
        }
    }

    #[derive(Debug)]
    pub struct PuzzleData {
        monkeys: Vec<Monkey>,
    }

    impl From<&'static str> for PuzzleData {
        /// parse the puzzle input
        fn from(s: &'static str) -> Self {
            Self {
                monkeys: s.split("\n\n").map(Monkey::from).collect(),
            }
        }
    }

    impl PuzzleData {
        pub fn monkeys(&self) -> &[Monkey] {
            &self.monkeys
        }
    }
}
Solution

Here is my solution for both parts. The solve function is called with div = 3 for the first part and div = 1 for the second.

pub fn round(monkeys: &mut Vec<Monkey>, counts: &mut Vec<usize>, div: usize, m: usize) {
    for id in 0..monkeys.len() {
        counts[id] += monkeys[id].worries.len();
        for k in 0..monkeys[id].worries.len() {
            let worry = (monkeys[id].upd.apply(monkeys[id].worries[k]) / div) % m;
            let target = if worry % monkeys[id].test == 0 {
                monkeys[id].if_true
            } else {
                monkeys[id].if_false
            };
            monkeys[target].worries.push(worry);
        }
        monkeys[id].worries.clear();
    }
}

pub fn solve(data: &PuzzleData, div: usize, rounds: usize) -> usize {
    let mut monkeys = Vec::from(data.monkeys());
    let mut counts = vec![0; monkeys.len()];
    let m = monkeys.iter().map(|monkey| monkey.test).product();

    for _ in 0..rounds {
        round(&mut monkeys, &mut counts, div, m);
    }

    counts.sort_unstable();

    counts.pop().unwrap() as usize * counts.pop().unwrap() as usize
}
Tests
  1. and the tests

#[cfg(test)]
mod tests {
    use super::*;

    const CONTENT: &str = r#"Monkey 0:
Starting items: 79, 98
Operation: new = old * 19
Test: divisible by 23
  If true: throw to monkey 2
  If false: throw to monkey 3

Monkey 1:
Starting items: 54, 65, 75, 74
Operation: new = old + 6
Test: divisible by 19
  If true: throw to monkey 2
  If false: throw to monkey 0

Monkey 2:
Starting items: 79, 60, 97
Operation: new = old * old
Test: divisible by 13
  If true: throw to monkey 1
  If false: throw to monkey 3

Monkey 3:
Starting items: 74
Operation: new = old + 3
Test: divisible by 17
  If true: throw to monkey 0
  If false: throw to monkey 1
"#;

    #[test]
    pub fn test_from() {
        let data = PuzzleData::from(CONTENT);
        println!("{data:?}");
    }

    #[test]
    pub fn test_round() {
        let data = PuzzleData::from(CONTENT);
        let mut monkeys = Vec::from(data.monkeys());
        let mut counts = vec![0; monkeys.len()];
        round(&mut monkeys, &mut counts, 3, usize::MAX);
        assert_eq!(vec![20, 23, 27, 26], monkeys[0].worries);
        assert_eq!(vec![2080, 25, 167, 207, 401, 1046], monkeys[1].worries);
        assert!(monkeys[2].worries.is_empty());
        assert!(monkeys[3].worries.is_empty());
    }

    #[test]
    pub fn test_star_1() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(10605, solve(&data, 3, 20));
    }

    #[test]
    pub fn test_star_2() {
        let data = PuzzleData::from(CONTENT);

        assert_eq!(4 * 6, solve(&data, 1, 1));
        assert_eq!(99 * 103, solve(&data, 1, 20));

        assert_eq!(2_713_310_158, solve(&data, 1, 10_000));
    }
}

Day 12: rust

Day 12: Hill Climbing Algorithm

Rust solution to AoC|2022|12.

The first path finding challenge in 2022’s AoC edition ;)

Input

I parse the input into a grid of bytes stored in a vec and additionally determine the starting position, the target position and the width of the grid.

pub mod input {
    #[derive(Debug)]
    pub struct PuzzleData {
        pub grid: Vec<u8>,
        pub width: usize,
        pub start: usize,
        pub target: usize,
    }

    impl<'a> From<&'a str> for PuzzleData {
        /// parse the puzzle input
        fn from(s: &'a str) -> Self {
            let width = s.find('\n').unwrap();

            let mut grid: Vec<_> = s
                .as_bytes()
                .iter()
                .cloned()
                .filter(|b| *b != b'\n')
                .collect();
            let start = grid.iter().position(|&b| b == b'S').unwrap();
            let target = grid.iter().position(|&b| b == b'E').unwrap();

            grid[start] = b'a';
            grid[target] = b'z';

            Self {
                grid,
                width,
                start,
                target,
            }
        }
    }
}
Star 1

The shortest path is found by a breadth first traversal.

The function shortest_path returns an option with a None value in case no path is found. My initial version just panicked in that case which turned out to not be good enough for part 2.

Today, I made the baddest mistake ever: I put the wrong number in my test (32 instead of 31) and was trying to figure out for quite some while where I inserted an off-by-one error in my code until I figured that my expected value was wrong.

pub fn shortest_path(data: &PuzzleData, start: usize) -> Option<usize> {
    let mut queue = VecDeque::new();
    queue.push_back((0, start));

    let mut seen = vec![false; data.grid.len()];
    seen[start] = true;

    let height = data.grid.len() / data.width;

    while let Some((steps, pos)) = queue.pop_front() {
        if pos == data.target {
            return Some(steps);
        }

        let x = pos % data.width;
        let y = pos / data.width;

        for (chk, nxt) in [
            (x > 0, pos as isize - 1),
            (x < data.width - 1, pos as isize + 1),
            (y > 0, pos as isize - data.width as isize),
            (y < height - 1, pos as isize + data.width as isize),
        ] {
            if chk && !seen[nxt as usize] && data.grid[nxt as usize] <= data.grid[pos] + 1 {
                queue.push_back((steps + 1, nxt as _));
                seen[nxt as usize] = true;
            }
        }
    }

    None
}

pub fn star_1(data: &PuzzleData) -> usize {
    shortest_path(data, data.start).unwrap()
}
Star 2

Just perform shortest path calculation for all possible starting positions (that was my solution to submit the answer)

pub fn star_2_original(data: &PuzzleData) -> usize {
    (0..data.grid.len())
        .filter(|&k| data.grid[k] == b'a')
        .filter_map(|start| shortest_path(data, start))
        .min()
        .unwrap()
}

Then I realized it is much simpler if I just reverse the search direction and look for the shortest path from the target to any 'a'. So I did that to have another sub 1ms solution.

pub fn shortest_path_2<F, G>(data: &PuzzleData, start: usize, reached: F, check: G) -> Option<usize>
where
    F: Fn(usize) -> bool,
    G: Fn(u8, u8) -> bool,
{
    let mut queue = VecDeque::new();
    queue.push_back((0, start));

    let mut seen = vec![false; data.grid.len()];
    seen[start] = true;

    let height = data.grid.len() / data.width;

    while let Some((steps, pos)) = queue.pop_front() {
        if reached(pos) {
            return Some(steps);
        }

        let x = pos % data.width;
        let y = pos / data.width;

        for (chk, nxt) in [
            (x > 0, pos as isize - 1),
            (x < data.width - 1, pos as isize + 1),
            (y > 0, pos as isize - data.width as isize),
            (y < height - 1, pos as isize + data.width as isize),
        ] {
            if chk && !seen[nxt as usize] && check(data.grid[pos], data.grid[nxt as usize]) {
                queue.push_back((steps + 1, nxt as _));
                seen[nxt as usize] = true;
            }
        }
    }

    None
}

pub fn star_2(data: &PuzzleData) -> usize {
    shortest_path_2(
        data,
        data.target,
        |pos| data.grid[pos] == b'a',
        |f, t| f <= t + 1,
    )
    .unwrap()
}

The shortest_path_2 function is generic to also work for part 1, but I did not change it in my code. Here is how to call it:

shortest_path_2(data, data.start, |pos| pos == data.target, |f, t| t <= f + 1).unwrap()
Tests

And the mandatory tests. But be careful, if a test of the type expected == actual fails, there are two possible reasons: actual can be wrong or expected can be wrong.

#[cfg(test)]
mod tests {
    use super::*;

    const CONTENT: &str = r#"Sabqponm
abcryxxl
accszExk
acctuvwj
abdefghi
"#;

    #[test]
    pub fn test_from() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(8, data.width);
        assert_eq!(0, data.start);
        assert_eq!(21, data.target);
        println!("{data:?}");
    }

    #[test]
    pub fn test_star_1() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(31, star_1(&data));
    }

    #[test]
    pub fn test_star_2() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(29, star_2(&data));
    }
}

Day 13: rust

Day 13: Distress Signal

Rust solution to AoC|2022|13.

My initial solution was based on "If the input looks like a tree structure, parse it into a tree structure!"

Input
    pub mod input {
        use super::node::Node;

        pub struct PuzzleData {
            pub nodes: Vec<Node>,
        }

        impl<'a> From<&'a str> for PuzzleData {
            fn from(s: &'a str) -> Self {
                Self {
                    nodes: s
                        .lines()
                        .filter(|l| !l.is_empty())
                        .map(|l| Node::from(l))
                        .collect(),
                }
            }
        }
    }
Solution

The whole work for the solution is in the recursive Node enum modeling the trees represented by the puzzle input with its implementation. Namely recursive parsing in the parse function and comparison in the Ord trait implementation.

    pub mod node {
        //! Tree structure for recursive lists from puzzle
        use std::cmp::Ordering;

        #[derive(Debug, Eq, PartialEq, Clone)]
        pub enum Node {
            List(Box<[Node]>),
            Value(usize),
        }

        impl std::fmt::Display for Node {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                match self {
                    Node::List(list) => {
                        '['.fmt(f)?;
                        for n in list.iter().take(1) {
                            n.fmt(f)?;
                        }
                        for n in list.iter().skip(1) {
                            ','.fmt(f)?;
                            n.fmt(f)?;
                        }
                        ']'.fmt(f)?;
                    }
                    Node::Value(value) => value.fmt(f)?,
                }

                Ok(())
            }
        }

        impl<T> From<T> for Node
        where
            T: AsRef<[u8]>,
        {
            fn from(s: T) -> Self {
                Self::parse(s.as_ref(), 0).0
            }
        }

        impl Ord for Node {
            fn cmp(&self, other: &Self) -> Ordering {
                match (self, other) {
                    (Node::List(lhs), Node::List(rhs)) => {
                        for k in 0..lhs.len().min(rhs.len()) {
                            let o = lhs[k].cmp(&rhs[k]);
                            if o != Ordering::Equal {
                                return o;
                            }
                        }
                        lhs.len().cmp(&rhs.len())
                    }
                    (Node::Value(lhs), Node::Value(rhs)) => lhs.cmp(rhs),
                    (_, Node::Value(rhs)) => self.cmp(&Node::List(vec![Node::Value(*rhs)].into())),
                    (Node::Value(lhs), _) => Node::List(vec![Node::Value(*lhs)].into()).cmp(other),
                }
            }
        }

        impl PartialOrd for Node {
            fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
                Some(self.cmp(other))
            }
        }

        impl Node {
            fn parse(s: &[u8], pos: usize) -> (Self, usize) {
                if s[pos] == b'[' && s[pos + 1] == b']' {
                    // handle empty list separately
                    (Self::List(vec![].into()), 2)
                } else if s[pos] == b'[' {
                    let mut v = Vec::new();
                    let mut len = 1;
                    loop {
                        let (n, l) = Self::parse(s, pos + len);
                        v.push(n);
                        len += l + 1;
                        if s[pos + len - 1] == b']' {
                            break;
                        }
                    }
                    (Self::List(v.into()), len)
                } else {
                    let mut v = 0;
                    let mut len = 0;
                    while pos + len < s.len() && s[pos + len] >= b'0' && s[pos + len] <= b'9' {
                        v = v * 10 + (s[pos + len] - b'0') as usize;
                        len += 1;
                    }
                    (Self::Value(v), len)
                }
            }
        }
    }
Star 1

Do the pairwise comparisons.

    pub fn star_1(data: &PuzzleData) -> usize {
        data.nodes
            .iter()
            .step_by(2)
            .zip(data.nodes.iter().skip(1).step_by(2))
            .enumerate()
            .filter(|(_, (a, b))| a.cmp(b) != Ordering::Greater)
            .fold(0, |s, (k, _)| s + k + 1)
    }
Star 2

Find the position of the divider packets after sorting.

    pub fn star_2(data: &PuzzleData) -> usize {
        let mut v = data.nodes.clone();
        v.push(Node::from("[[2]]"));
        v.push(Node::from("[[6]]"));
        v.sort_unstable();

        let a = v.iter().position(|n| n == &Node::from("[[2]]")).unwrap();
        let b = v.iter().position(|n| n == &Node::from("[[6]]")).unwrap();

        (a + 1) * (b + 1)
    }
Tests
    #[cfg(test)]
    mod tests {
        use super::*;
        use crate::tree::node::Node;

        #[test]
        pub fn test_parse() {
            let s = "[1,[2,[3,[4,[5,6,7]]]],8,9]";
            let n = Node::from(s);
            println!("{n:?}");
            assert_eq!(s, n.to_string());
        }

        #[test]
        pub fn test_cmp() {
            let nodes = PuzzleData::from(crate::tests::CONTENT).nodes;
            let cmp = nodes
                .iter()
                .step_by(2)
                .zip(nodes.iter().skip(1).step_by(2))
                .map(|(a, b)| a.cmp(b))
                .collect::<Vec<_>>();
            println!("{cmp:?}");
            assert_eq!(
                vec![
                    Ordering::Less,
                    Ordering::Less,
                    Ordering::Greater,
                    Ordering::Less,
                    Ordering::Greater,
                    Ordering::Less,
                    Ordering::Greater,
                    Ordering::Greater
                ],
                cmp
            );
        }

        #[test]
        pub fn test_star_1() {
            assert_eq!(13, star_1(&PuzzleData::from(crate::tests::CONTENT)))
        }

        #[test]
        pub fn test_star_2() {
            assert_eq!(140, star_2(&PuzzleData::from(crate::tests::CONTENT)))
        }
    }
Alternative without using heap

Later on, I thought it should be possible to implement a solution that does not require any heap allocations by directly iterating on the input data. I added this in a variant of my solution in mod iter. You can run this variant using cargo run --release --features no-heap. Interestingly it is not really performing any better than the original solution. The advantage for the second part is probably mainly caused by avoiding to sort.

pub mod iter {
    use self::node::List;
    use self::{input::PuzzleData, node::Node};
    use mr_kaffee_aoc::{Puzzle, Star};
    use std::cmp::Ordering;

    /// the puzzle
    pub fn puzzle() -> Puzzle<'static, PuzzleData<'static>, usize, usize, usize, usize> {
        Puzzle {
            year: 2022,
            day: 13,
            input: include_str!("../input.txt"),
            star1: Some(Star {
                name: "Star 1",
                f: &star_1,
                exp: Some(5_675),
            }),
            star2: Some(Star {
                name: "Star 2",
                f: &star_2,
                exp: Some(20_383),
            }),
        }
    }

    pub mod input {
        pub struct PuzzleData<'a> {
            pub input: &'a str,
        }

        impl<'a> From<&'a str> for PuzzleData<'a> {
            fn from(input: &'a str) -> Self {
                Self { input }
            }
        }
    }

    pub mod node {
        use std::{cmp::Ordering, iter::once};

        #[derive(Debug, Clone)]
        pub struct List<'a> {
            data: &'a [u8],
            pos: usize,
        }

        #[derive(Debug, Clone)]
        pub enum Node<'a> {
            List(List<'a>),
            Value(usize),
        }

        impl<'a> std::fmt::Display for Node<'a> {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                match self {
                    Node::List(list) => list.fmt(f),
                    Node::Value(value) => value.fmt(f),
                }
            }
        }

        impl<'a> From<&'a str> for Node<'a> {
            fn from(s: &'a str) -> Self {
                if s.starts_with('[') {
                    Self::List(List::from(s))
                } else {
                    Self::Value(s.parse().unwrap())
                }
            }
        }

        impl<'a> std::fmt::Display for List<'a> {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                let mut level = 1;
                '['.fmt(f)?;
                for &b in &self.data[self.pos..] {
                    level = match b {
                        b'[' => level + 1,
                        b']' => level - 1,
                        _ => level,
                    };
                    (b as char).fmt(f)?;
                    if level == 0 {
                        break;
                    }
                }
                Ok(())
            }
        }

        impl<'a> From<&'a [u8]> for List<'a> {
            fn from(s: &'a [u8]) -> Self {
                let data = s.as_ref();
                let pos = if data[0] == b'[' { 1 } else { 0 };
                Self { data, pos }
            }
        }

        impl<'a> From<&'a str> for List<'a> {
            fn from(s: &'a str) -> Self {
                Self::from(s.as_bytes())
            }
        }

        impl<'a> Iterator for List<'a> {
            type Item = Node<'a>;

            fn next(&mut self) -> Option<Self::Item> {
                if self.pos >= self.data.len() || self.data[self.pos] == b']' {
                    // exhausted
                    None
                } else if self.data[self.pos] == b'[' {
                    // parse list
                    let nxt = Self::Item::List(List::from(&self.data[self.pos..]));

                    // advance pos to after list
                    self.pos += 2 + self.data[self.pos + 1..]
                        .iter()
                        .scan(1usize, |level, b| {
                            match b {
                                b'[' => *level += 1,
                                b']' => *level -= 1,
                                _ => (),
                            };
                            Some(*level)
                        })
                        .position(|level| level == 0)
                        .unwrap();

                    // skip ',' if applicable
                    if self.pos < self.data.len() && self.data[self.pos] == b',' {
                        self.pos += 1;
                    }

                    // return list
                    Some(nxt)
                } else {
                    // parse value
                    let mut v = 0;
                    while (b'0'..=b'9').contains(&self.data[self.pos]) {
                        // parse digit
                        v = 10 * v + (self.data[self.pos] - b'0') as usize;
                        self.pos += 1;
                    }

                    // skip ',' if applicable
                    if self.pos < self.data.len() && self.data[self.pos] == b',' {
                        self.pos += 1;
                    }

                    // return value
                    Some(Self::Item::Value(v))
                }
            }
        }

        impl<'a> Ord for Node<'a> {
            fn cmp(&self, other: &Self) -> std::cmp::Ordering {
                match (self, other) {
                    (Node::Value(lhs), Node::Value(rhs)) => lhs.cmp(rhs),
                    (Node::List(lhs), Node::List(rhs)) => lhs.clone().cmp(rhs.clone()),
                    (Node::List(lhs), rhs) => lhs.clone().cmp(once(rhs.clone())),
                    (lhs, Node::List(rhs)) => once(lhs.clone()).cmp(rhs.clone()),
                }
            }
        }

        impl<'a> PartialOrd for Node<'a> {
            fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
                Some(self.cmp(other))
            }
        }

        impl<'a> PartialEq for Node<'a> {
            fn eq(&self, other: &Self) -> bool {
                self.cmp(other) == Ordering::Equal
            }
        }

        impl<'a> Eq for Node<'a> {}
    }

    pub fn star_1(data: &PuzzleData) -> usize {
        let mut lines = data.input.lines().filter(|l| !l.is_empty());
        let mut idx = 0;
        let mut result = 0;
        while let (Some(a), Some(b)) = (lines.next(), lines.next()) {
            idx += 1;
            if Node::from(a).cmp(&Node::from(b)) != Ordering::Greater {
                result += idx;
            }
        }

        result
    }

    pub fn star_2(data: &PuzzleData) -> usize {
        let n_1 = List::from("[[2]]");
        let n_2 = List::from("[[6]]");

        let (cnt_1, cnt_2) =
            data.input
                .lines()
                .filter(|l| !l.is_empty())
                .fold((1, 2), |(cnt_1, cnt_2), l| {
                    if n_1.clone().cmp(List::from(l)) == Ordering::Greater {
                        (cnt_1 + 1, cnt_2 + 1)
                    } else if n_2.clone().cmp(List::from(l)) == Ordering::Greater {
                        (cnt_1, cnt_2 + 1)
                    } else {
                        (cnt_1, cnt_2)
                    }
                });

        cnt_1 * cnt_2
    }

    #[cfg(test)]
    mod tests {
        use super::*;
        use crate::iter::node::{List, Node};

        #[test]
        pub fn test_next() {
            let mut list = List::from("[10,[[9,10,0],2]]");
            assert_eq!(Some("10".into()), list.next().map(|n| n.to_string()));
            assert_eq!(
                Some("[[9,10,0],2]".into()),
                list.next().map(|n| n.to_string())
            );
            assert!(list.next().is_none());
        }

        #[test]
        pub fn test_cmp() {
            let nodes = crate::tests::CONTENT
                .lines()
                .filter(|l| !l.is_empty())
                .map(List::from)
                .map(Node::List)
                .collect::<Vec<_>>();

            let cmp = nodes
                .iter()
                .step_by(2)
                .zip(nodes.iter().skip(1).step_by(2))
                .map(|(a, b)| a.cmp(b))
                .collect::<Vec<_>>();
            println!("{cmp:?}");
            assert_eq!(
                vec![
                    Ordering::Less,
                    Ordering::Less,
                    Ordering::Greater,
                    Ordering::Less,
                    Ordering::Greater,
                    Ordering::Less,
                    Ordering::Greater,
                    Ordering::Greater
                ],
                cmp
            );
        }

        #[test]
        pub fn test_star_1() {
            assert_eq!(13, star_1(&PuzzleData::from(crate::tests::CONTENT)))
        }

        #[test]
        pub fn test_star_2() {
            assert_eq!(140, star_2(&PuzzleData::from(crate::tests::CONTENT)))
        }
    }
}

Day 14: rust

Day 14: Regolith Reservoir

Rust solution to AoC|2022|14.

I was remembered of AoC|2018|17 even without the hint in the puzzle.

I don’t think my solution is very elegant, but at the moment, I do not have an idea how to create a nice one …​

Input

I parse the input in a vec of paths, where each path is vec of points. The structure PuzzleData has methods to compute the bounding box for all points (bbox) and build a 2D grid using the paths specified (grid).

pub mod input {
    #[derive(Debug)]
    pub struct PuzzleData {
        paths: Vec<Vec<(isize, isize)>>,
    }

    impl<'a> From<&'a str> for PuzzleData {
        /// parse the puzzle input
        fn from(s: &'a str) -> Self {
            Self {
                paths: s
                    .lines()
                    .map(|l| {
                        l.split(" -> ")
                            .map(|c| c.split_once(',').unwrap())
                            .map(|(x, y)| (x.parse().unwrap(), y.parse().unwrap()))
                            .collect()
                    })
                    .collect(),
            }
        }
    }

    impl<'a> PuzzleData {
        pub fn bbox(&self) -> (isize, isize, isize, isize) {
            self.paths.iter().fold((500, 0, 501, 1), |bbox, v| {
                v.iter().fold(bbox, |(x_mn, y_mn, x_mx, y_mx), (x, y)| {
                    (x_mn.min(*x), y_mn.min(*y), x_mx.max(x + 1), y_mx.max(y + 1))
                })
            })
        }

        /// get a grid as flat list of chars, the width of the grid, and the point of sand inflow
        pub fn grid(&self) -> (Vec<char>, usize, (usize, usize)) {
            let (x_mn, y_mn, x_mx, y_mx) = self.bbox();
            let width = (x_mx - x_mn) as usize;
            let height = (y_mx - y_mn) as usize;
            let mut grid = vec!['.'; width * height];

            for path in &self.paths {
                for k in 1..path.len() {
                    let (dx, dy, len) = if path[k].0 > path[k - 1].0 {
                        assert!(path[k].1 == path[k - 1].1);
                        (1, 0, path[k].0 - path[k - 1].0)
                    } else if path[k].0 < path[k - 1].0 {
                        assert!(path[k].1 == path[k - 1].1);
                        (-1, 0, path[k - 1].0 - path[k].0)
                    } else if path[k].1 > path[k - 1].1 {
                        assert!(path[k].0 == path[k - 1].0);
                        (0, 1, path[k].1 - path[k - 1].1)
                    } else if path[k].1 < path[k - 1].1 {
                        assert!(path[k].0 == path[k - 1].0);
                        (0, -1, path[k - 1].1 - path[k].1)
                    } else {
                        unreachable!()
                    };

                    let x0 = path[k - 1].0 - x_mn;
                    let y0 = path[k - 1].1 - y_mn;
                    for k in 0..len + 1 {
                        grid[(x0 + dx * k) as usize + (y0 + dy * k) as usize * width] = '#';
                    }
                }
            }

            (grid, width, ((500 - x_mn) as _, (0 - y_mn) as _))
        }
    }
}
Star 1

Just let the sand flow in the grid until sand starts flowing off to the big void.

pub fn star_1(data: &PuzzleData) -> usize {
    let (mut grid, width, (x_0, y_0)) = data.grid();
    let height = grid.len() / width;

    let mut cnt = 0;
    'inflow: loop {
        // first candidate is spot directly on top of something solid
        let mut x = x_0;
        let mut y = match (y_0..height).find(|y| grid[x + y * width] != '.') {
            Some(y) => y - 1,
            None => unreachable!("Nothing solid found to start with"),
        };

        loop {
            if y == height - 1 {
                // go to void below
                break 'inflow;
            }

            if grid[x + (y + 1) * width] == '.' {
                // bubble down
                y += 1;
            } else if x > 0 && grid[x - 1 + (y + 1) * width] == '.' {
                // bubble down-left
                y += 1;
                x -= 1;
            } else if x < width - 1 && grid[x + 1 + (y + 1) * width] == '.' {
                // bubble down-right
                y += 1;
                x += 1;
            } else if x == 0 || x == width - 1 {
                // go to void left/right
                break 'inflow;
            } else {
                grid[x + y * width] = 'o';
                cnt += 1;
                break;
            }
        }
    }

    cnt
}
Star 2

Create a grid with an additional line added at the bottom and big enough to make sure nothing flows off to the void anymore. The only thing on top to do is to change the condition for exiting the loop.

pub fn star_2(data: &PuzzleData) -> usize {
    let (grid_0, width_0, (x_0_0, y_0)) = data.grid();
    let height_0 = grid_0.len() / width_0;

    // wrap the grid in a bigger grid (append height columns to the left and right and an additional row below)
    let width = width_0 + 2 * height_0;
    let height = height_0 + 1;
    let mut grid = vec!['.'; width * height];
    for y in 0..height_0 {
        for x in 0..width_0 {
            grid[x + height_0 + y * width] = grid_0[x + y * width_0];
        }
    }
    let x_0 = x_0_0 + height_0;

    let mut cnt = 0;
    loop {
        // first candidate is spot directly on top of something solid
        let mut x = x_0;
        let mut y = match (y_0..height).find(|y| grid[x + y * width] != '.') {
            Some(y) => {
                if y == y_0 {
                    // No more sand can enter
                    break;
                }
                y - 1
            }
            None => unreachable!("Nothing solid found to start with"),
        };

        loop {
            if y == height - 1 {
                // floor reached
                grid[x + y * width] = 'o';
                cnt += 1;
                break;
            }

            if grid[x + (y + 1) * width] == '.' {
                // bubble down
                y += 1;
            } else if x > 0 && grid[x - 1 + (y + 1) * width] == '.' {
                // bubble down-left
                y += 1;
                x -= 1;
            } else if x < width - 1 && grid[x + 1 + (y + 1) * width] == '.' {
                // bubble down-right
                y += 1;
                x += 1;
            } else if x == 0 || x == width - 1 {
                // the grid should not be too small
                unreachable!("Grid is too small!");
            } else {
                // cannot move any further
                grid[x + y * width] = 'o';
                cnt += 1;
                break;
            }
        }
    }

    cnt
}
Tests
#[cfg(test)]
mod tests {
    use super::*;

    const CONTENT: &str = r#"498,4 -> 498,6 -> 496,6
503,4 -> 502,4 -> 502,9 -> 494,9
"#;

    #[test]
    pub fn test_from() {
        let data = PuzzleData::from(CONTENT);
        println!("{data:?}");

        let bbox = data.bbox();
        assert_eq!((494, 0, 504, 10), bbox);

        let (grid, width, (x0, y0)) = data.grid();
        assert_eq!((6, 0), (x0, y0));
        assert_eq!(10, width);
        let exp = "............................................#...##....#...#...###...#.........#.........#.#########.";
        assert_eq!(exp.chars().collect::<Vec<_>>(), grid);
        for row in 0..grid.len() / width {
            for col in 0..width {
                print!("{}", grid[col + row * width]);
            }
            println!();
        }
    }

    #[test]
    pub fn test_star_1() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(24, star_1(&data));
    }

    #[test]
    pub fn test_star_2() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(93, star_2(&data));
    }
}

Day 15: rust

Day 15: Beacon Exclusion Zone

Rust solution to AoC|2022|15.

This one was a challenge for me. Part one was initially solved with brute force. My current solution projects ranges on the row to scan.

For part 2, brute force was not a real option.

Input
pub mod input {
    #[derive(Debug)]
    pub struct PuzzleData {
        sensors: Vec<((isize, isize), (isize, isize))>,
        pub row: isize,
        pub width: isize,
    }

    fn parse_line(l: &str) -> ((isize, isize), (isize, isize)) {
        let words = l.split_ascii_whitespace();
        let mut words = words.skip(2); // Sensor at

        let x_s = words.next().unwrap();
        let x_s = x_s.strip_prefix("x=").unwrap().strip_suffix(",").unwrap();
        let x_s = x_s.parse().unwrap();

        let y_s = words.next().unwrap();
        let y_s = y_s.strip_prefix("y=").unwrap().strip_suffix(":").unwrap();
        let y_s = y_s.parse().unwrap();

        let mut words = words.skip(4); // closest beacon is at

        let x_b = words.next().unwrap();
        let x_b = x_b.strip_prefix("x=").unwrap().strip_suffix(",").unwrap();
        let x_b = x_b.parse().unwrap();

        let y_b = words.next().unwrap();
        let y_b = y_b.strip_prefix("y=").unwrap();
        let y_b = y_b.parse().unwrap();

        ((x_s, y_s), (x_b, y_b))
    }

    impl From<&str> for PuzzleData {
        /// parse the puzzle input
        fn from(s: &str) -> Self {
            Self {
                sensors: s.lines().map(parse_line).collect(),
                row: 2_000_000,
                width: 4_000_000,
            }
        }
    }

    impl PuzzleData {
        pub fn sensors(&self) -> &[((isize, isize), (isize, isize))] {
            &self.sensors
        }

        pub fn sensors_with_r(&self) -> Vec<((isize, isize), isize)> {
            self.sensors
                .iter()
                .map(|((x, y), (x_b, y_b))| ((*x, *y), (x - x_b).abs() + (y - y_b).abs()))
                .collect()
        }
    }
}
Star 1

Project ranges

/// determine ranges covered by sensors on given row
pub fn ranges(
    sensors: &[((isize, isize), (isize, isize))],
    mn: isize,
    mx: isize,
    row: isize,
) -> Vec<(isize, isize)> {
    sensors
        .iter()
        .map(|((x, y), (x_b, y_b))| (*x, (x - x_b).abs() + (y - y_b).abs() - (y - row).abs()))
        .map(|(x, d)| ((x - d).max(mn), (x + d).min(mx)))
        .filter(|(mn, mx)| mx >= mn)
        .fold(Vec::new(), |mut ranges, range| {
            ranges.push(range);
            let mut k_2 = ranges.len() - 1;
            for k_1 in (0..ranges.len() - 1).rev() {
                if ranges[k_1].0 <= ranges[k_2].0 && ranges[k_1].1 >= ranges[k_2].1 {
                    // k_2 contained in k_1
                    ranges.swap_remove(k_2);
                    break;
                } else if ranges[k_2].0 <= ranges[k_1].0 && ranges[k_2].1 >= ranges[k_1].1 {
                    // k_1 contained in k_2
                    ranges.swap_remove(k_1);
                    k_2 = if k_2 == ranges.len() { k_1 } else { k_2 };
                } else if ranges[k_2].0 >= ranges[k_1].0 && ranges[k_2].0 <= ranges[k_1].1 + 1 {
                    // k_2's min in k_1 or immediately after
                    ranges[k_1].1 = ranges[k_2].1;
                    ranges.swap_remove(k_2);
                    k_2 = k_1;
                } else if ranges[k_2].1 >= ranges[k_1].0 - 1 && ranges[k_2].1 <= ranges[k_1].1 {
                    // k_2's max in k_1 or immediately before
                    ranges[k_1].0 = ranges[k_2].0;
                    ranges.swap_remove(k_2);
                    k_2 = k_1;
                }
            }
            ranges
        })
}

pub fn star_1(data: &PuzzleData) -> usize {
    let ranges: Vec<(isize, isize)> = ranges(data.sensors(), isize::MIN, isize::MAX, data.row);
    let r = ranges
        .iter()
        .map(|(mn, mx)| (mx - mn + 1) as usize)
        .sum::<usize>();
    let s = data
        .sensors()
        .iter()
        .filter(|(_, (_, y))| *y == data.row)
        .map(|(_, b)| b)
        .collect::<HashSet<_>>()
        .len();
    r - s
}
Star 2

The idea is that the position of the distress beacon must be just outside the range of at least two sensors. Hence, for all pairs of sensors, I find the points that are just outside of both sensor’s ranges as candidates. Out of these candidates, I search for a point that is also outside of all the other sensor’s ranges.

Well …​ it works. But coming up with the formulas for the points was a pain and very error prone for me. I am sure there is something more elegant and simple.

There is also a flaw in the candidates function. Normally, it should be symmetric, i.e., candidates(s1, s2) == candidates(s2, s1); it is not in all cases. I guess this is rounding issues …​ It seems the function returns rather too many candidates than too few, which would not be an issue.

pub fn is_distress_beacon(
    sensors: &[((isize, isize), isize)],
    p: &(isize, isize),
    width: isize,
) -> bool {
    (0..=width).contains(&p.0)
        && (0..=width).contains(&p.1)
        && sensors
            .iter()
            .all(|((x, y), r)| (x - p.0).abs() + (y - p.1).abs() > *r)
}

pub fn candidates(
    (((x_1, y_1), r_1), ((x_2, y_2), r_2)): (&((isize, isize), isize), &((isize, isize), isize)),
) -> HashSet<(isize, isize)> {
    let dx = x_2 - x_1;
    let dy = y_2 - y_1;
    let dr = r_2 - r_1;

    [
        // TL - RT
        // x1+(dx+dy-dr)/2,y1-r1-1+(dx+dy-dr)/2
        (
            x_1 + (dx + dy - dr + 1) / 2,
            y_1 - r_1 - 1 + (dx + dy - dr) / 2,
        ),
        (x_1 + (dx + dy - dr) / 2, y_1 - r_1 - 1 + (dx + dy - dr) / 2),
        // LB - BR
        // x1+(dx+dy+dr)/2,y1+r1+1+(dx+dy+dr)/2
        (
            x_1 + (dx + dy + dr - 1) / 2,
            y_1 + r_1 + 1 + (dx + dy + dr) / 2,
        ),
        (x_1 + (dx + dy + dr) / 2, y_1 + r_1 + 1 + (dx + dy + dr) / 2),
        // RT - TL
        // x1+(dx-dy+dr)/2,y1-r1-1-(dx-dy+dr)/2
        (
            x_1 + (dx - dy + dr - 1) / 2,
            y_1 - r_1 - 1 - (dx - dy + dr) / 2,
        ),
        (x_1 + (dx - dy + dr) / 2, y_1 - r_1 - 1 - (dx - dy + dr) / 2),
        // BR - LB
        // x1+(dx-dy-dr)/2,y1+r1+1-(dx-dy-dr)/2
        (
            x_1 + (dx - dy - dr + 1) / 2,
            y_1 + r_1 + 1 - (dx - dy - dr) / 2,
        ),
        (x_1 + (dx - dy - dr) / 2, y_1 + r_1 + 1 - (dx - dy - dr) / 2),
        // LB - TL
        // x1-r1-1+(dx+dy-dr)/2,y1+(dx+dy-dr)/2
        (
            x_1 - r_1 - 1 + (dx + dy - dr) / 2,
            y_1 + (dx + dy - dr + 1) / 2,
        ),
        (x_1 - r_1 - 1 + (dx + dy - dr) / 2, y_1 + (dx + dy - dr) / 2),
        // BR - RT
        // x1+r1+1+(dx-dy+dr)/2,y1-(dx-dy+dr)/2
        (
            x_1 + r_1 + 1 + (dx - dy + dr) / 2,
            y_1 - (dx - dy + dr - 1) / 2,
        ),
        (x_1 + r_1 + 1 + (dx - dy + dr) / 2, y_1 - (dx - dy + dr) / 2),
        // TL - LB
        // x1-r1-1+(dx-dy-dr)/2,y1-(dx-dy-dr)/2
        (
            x_1 - r_1 - 1 + (dx - dy - dr) / 2,
            y_1 - (dx - dy - dr + 1) / 2,
        ),
        (x_1 - r_1 - 1 + (dx - dy - dr) / 2, y_1 - (dx - dy - dr) / 2),
        // RT - BR
        // x2+r2+1+(dx+dy+dr)/2,y2+(dx+dy+dr)/2
        (
            x_1 + r_1 + 1 + (dx + dy + dr) / 2,
            y_1 + (dx + dy + dr - 1) / 2,
        ),
        (x_1 + r_1 + 1 + (dx + dy + dr) / 2, y_1 + (dx + dy + dr) / 2),
    ]
    .into_iter()
    .filter(|(x, y)| {
        (1..=2).contains(&((x - x_1).abs() + (y - y_1).abs() - r_1))
            && (1..=2).contains(&((x - x_2).abs() + (y - y_2).abs() - r_2))
    })
    .collect()
}

fn star_2_geometry(data: &PuzzleData) -> usize {
    let sensors = data.sensors_with_r();
    let (x, y) = sensors
        .iter()
        .zip(sensors.iter().skip(1))
        .map(candidates)
        .map(HashSet::into_iter)
        .flatten()
        .find(|p| is_distress_beacon(&sensors, p, data.width))
        .unwrap();

    (x * 4_000_000 + y) as _
}
Star 2 — Scan Lines

A solution using the functionality from star 1 is as follows. Much simpler code but much longer run-time.

pub fn star_2_scan_lines(data: &PuzzleData) -> usize {
    let (y, ranges) = (0..data.width)
        .map(|row| (row, ranges(data.sensors(), 0, data.width, row)))
        .find(|(_, r)| r.len() == 2)
        .unwrap();

    let x = ranges[0].0.max(ranges[1].0) - 1;

    (x * 4_000_000 + y) as _
}
Tests
#[cfg(test)]
mod tests {
    use super::*;

    const CONTENT: &str = r#"Sensor at x=2, y=18: closest beacon is at x=-2, y=15
Sensor at x=9, y=16: closest beacon is at x=10, y=16
Sensor at x=13, y=2: closest beacon is at x=15, y=3
Sensor at x=12, y=14: closest beacon is at x=10, y=16
Sensor at x=10, y=20: closest beacon is at x=10, y=16
Sensor at x=14, y=17: closest beacon is at x=10, y=16
Sensor at x=8, y=7: closest beacon is at x=2, y=10
Sensor at x=2, y=0: closest beacon is at x=2, y=10
Sensor at x=0, y=11: closest beacon is at x=2, y=10
Sensor at x=20, y=14: closest beacon is at x=25, y=17
Sensor at x=17, y=20: closest beacon is at x=21, y=22
Sensor at x=16, y=7: closest beacon is at x=15, y=3
Sensor at x=14, y=3: closest beacon is at x=15, y=3
Sensor at x=20, y=1: closest beacon is at x=15, y=3
"#;

    #[test]
    pub fn test_is_distress_beacon() {
        let data = PuzzleData::from(CONTENT);
        let sensors = data.sensors_with_r();
        assert!(is_distress_beacon(&sensors, &(14, 11), 20));
    }

    #[test]
    pub fn test_candidates() {
        // 0...#..%...
        // 1..#.#%.%..
        // 2.#.A%#B.%.
        // 3..#.#%.%..
        // 4...#..%...
        //  0123456789
        let s1 = ((3, 2), 2);
        let s2 = ((6, 2), 2);
        let e = HashSet::from([(4, 0), (5, 0), (4, 4), (5, 4)]);
        assert_eq!(
            e,
            candidates((&s1, &s2)).intersection(&e).cloned().collect()
        );
        assert_eq!(
            e,
            candidates((&s2, &s1)).intersection(&e).cloned().collect()
        );

        // 0...#...%..
        // 1..#.#.%.%.
        // 2.#.A.$.B.%
        // 3..#.#.%.%.
        // 4...#...%..
        //  0123456789
        //
        // RT: [x1 + r1 + 1 - k1, y1 - k1];
        // TL: [x2 - k2, y2 - r2 - 1 + k2];
        // [x1+(dx+dy-dr)/2,y1-r1-1+(dx+dy-dr)/2];
        // --- [x1+(dx-dy+dr)/2,y1-r1-1-(dx-dy+dr)/2];
        //
        // BR: [x1 + k1, y1 + r1 + 1 - k1];
        // LB: [x2 - r2 - 1 + k2, y2 + k2];
        // [x1+(dx-dy-dr)/2,y1+r1+1-(dx-dy-dr)/2];
        // --- [x1+(dx+dy+dr)/2,y1+r1+1+(dx+dy+dr)/2];
        let s1 = ((3, 2), 2);
        let s2 = ((7, 2), 2);
        let e = HashSet::from([(5, 1), (5, 3)]);
        assert_eq!(
            e,
            candidates((&s1, &s2)).intersection(&e).cloned().collect()
        );
        assert_eq!(
            e,
            candidates((&s2, &s1)).intersection(&e).cloned().collect()
        );

        // 0...#....
        // 1..#.#...
        // 2.#.A%#..
        // 3..#%#%..
        // 4..%#..%.
        // 5.%..B..%
        // 6..%...%.
        // 7...%.%..
        // 8....%...
        //  01234567
        // LB: [x1 - r1 - 1 + k1, y1 + k1];
        // TL: [x2 - k2, y2 - r2 - 1 + k2];
        // [x1-r1-1+(dx+dy-dr)/2,y1+(dx+dy-dr)/2]
        // --- [x1-r1-1+(dx-dy-dr)/2,y1-(dx-dy-dr)/2]
        let s1 = ((3, 2), 2);
        let s2 = ((4, 5), 3);
        let e = HashSet::from([(1, 3), (1, 4), (6, 2), (6, 3)]);
        assert_eq!(
            e,
            candidates((&s1, &s2)).intersection(&e).cloned().collect()
        );
        assert_eq!(
            e,
            candidates((&s2, &s1)).intersection(&e).cloned().collect()
        );

        // 0...#...
        // 1..#.#..
        // 2.#.A.#.
        // 3..#.#..
        // 4...$...
        // 5.-%.%..
        // 6.%.B.%.
        // 7..%.%..
        // 8...%...
        //  0123456
        let s1 = ((3, 2), 2);
        let s2 = ((3, 6), 2);
        let e = HashSet::from([(2, 4), (4, 4)]);
        assert_eq!(
            e,
            candidates((&s1, &s2)).intersection(&e).cloned().collect()
        );
        assert_eq!(
            e,
            candidates((&s2, &s1)).intersection(&e).cloned().collect()
        );
    }

    #[test]
    pub fn test_from() {
        let data = PuzzleData::from(CONTENT);
        println!("{data:?}");
    }

    #[test]
    pub fn test_star_1() {
        let mut data = PuzzleData::from(CONTENT);
        data.row = 10;
        assert_eq!(26, star_1(&data));
    }

    #[test]
    pub fn test_solve_2() {
        let mut data = PuzzleData::from(CONTENT);
        data.width = 20;
        assert_eq!(56_000_011, star_2_geometry(&data));
        assert_eq!(56_000_011, star_2_scan_lines(&data));
        assert_eq!(56_000_011, star_2_brute_force(&data));
    }
}

Day 16: rust

Day 16: Proboscidea Volcanium

Rust solution to AoC|2022|16.

Today was a tough nut to crack for me.

My attempt is to use a path finding algorithm to explore the valve & tunnel system.

The current state (node) is modeled by

  • the potential, i.e, the max pressure released until the timer is elapsed if all valves were opened in subsequent steps. If all valves are actually open, the potential is equal to the pressure released until the timer elapses (the volcano erupts)

  • the pressure released

  • the flow of all currently opened valves

  • the idx (position) of the agents opening valves (just me for part 1 and me + an elephant for part 2)

    • Actually, for part two, the two agents exploring the valve & tunnel system are interchangeable, so I transform to an elephant from time to time

  • the opened valves

  • the timer (counting down to zero)

I use a priority queue which yields the state with the highest potential first. This results in the following properties:

  • If a state is expanded, it is guaranteed that all adjacent states have at most the same potential. They will have equal potential if and only if all valves with positive flow are open. In all other cases, they will have lower potential.

  • If a state is popped from the queue, all states popped later on will have at most the same potential.

  • The potential of the first state popped from the queue with all valves open is the max. possible pressure.

The key is to figure out how to narrow down the search space without eliding relevant states. I tried several ideas and do not quite understand why those are not working:

  • If there is a possibility to open a valve, open it and discard possibilities to move on without opening the valve — worked for the real data but not for the examples.

  • If a state is expanded with position and opened valves identical to a state seen previously, skip it — worked for the real data but not for the examples.

Eventually, I finished with a variant of the second option: If a state is expanded with position and opened valves identical to a state seen previously, skip it unless it has higher potential than the state seen previously (kind of 'decrease key').

Input

I parse the input into a vec of valves. Each valve has its tunnels stored as vec of indices to that list for cheap lookups.

pub mod input {
    #[derive(Debug)]
    pub struct PuzzleData<'a> {
        valves: Vec<Valve<'a>>,
        root: usize,
    }

    fn parse_line(line: &str) -> (&str, Vec<&str>, usize) {
        let mut words = line.split_ascii_whitespace().skip(1);

        let name = words.next().unwrap();

        let mut words = words.skip(2);

        let flow = words.next().unwrap();
        let flow = flow[5..flow.len() - 1].parse().unwrap();

        let tunnels = words
            .skip(4)
            .map(|word| word.trim_end_matches(','))
            .collect();

        (name, tunnels, flow)
    }

    impl<'a> From<&'a str> for PuzzleData<'a> {
        /// parse the puzzle input
        fn from(s: &'a str) -> Self {
            let lines = s.lines().map(parse_line).collect::<Vec<_>>();
            let valves = lines
                .iter()
                .enumerate()
                .map(|(idx, (name, tunnels, flow))| Valve {
                    name,
                    idx,
                    flow: *flow,
                    tunnels: tunnels
                        .iter()
                        .map(|&tunnel| {
                            lines
                                .iter()
                                .position(|&(name, _, _)| name == tunnel)
                                .unwrap()
                        })
                        .collect(),
                })
                .collect();
            let root = lines.iter().position(|&(name, _, _)| name == "AA").unwrap();
            Self { valves, root }
        }
    }

    impl<'a> PuzzleData<'a> {
        pub fn get(&self, idx: usize) -> &Valve {
            &self.valves[idx]
        }

        pub fn root(&self) -> &Valve {
            &self.valves[self.root]
        }

        pub fn valves(&self) -> &[Valve] {
            &self.valves
        }
    }

    #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
    pub struct Valve<'a> {
        pub idx: usize,
        pub name: &'a str,
        pub flow: usize,
        pub tunnels: Vec<usize>,
    }
}
Star 1

Solution with a single agent (me).

pub fn star_1(data: &PuzzleData) -> usize {
    // search state
    // - pressure potential
    // - pressure
    // - flow (valves opened so far)
    // - idx (position)
    // - opened (valves opened so far)
    // - timer (time left before eruption)
    #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    struct State {
        potential: usize,
        pressure: usize,
        flow: usize,
        idx: usize,
        opened: u64,
        timer: usize,
    }

    impl State {
        fn create(
            pressure: usize,
            flow: usize,
            idx: usize,
            opened: u64,
            timer: usize,
            data: &PuzzleData,
            max_flow: usize,
        ) -> Self {
            let mut potential = pressure;
            let mut flow_ = 0;
            if timer >= 1 {
                // in next step, pressure will be increased by flow
                flow_ += flow;
                potential += flow_;
            }
            if timer >= 2 {
                // in the 2nd step, pressure will at most be increased by flow
                // of current valve. If it is not opened in the next step
                // (agent moves instead), the flow will not change
                if opened & 1 << idx == 0 {
                    flow_ += data.get(idx).flow;
                }
                potential += flow_;

                // upper bound for subsequent steps: all valves open
                potential += (timer - 2) * max_flow;
            }

            Self {
                potential,
                pressure,
                flow,
                idx,
                opened,
                timer,
            }
        }
    }

    // all valves opened / max flow
    let (all_opened, max_flow) = data
        .valves()
        .iter()
        .filter(|v| v.flow > 0)
        .fold((0, 0), |(o, f), v| (o | 1 << v.idx, f + v.flow));

    // max time
    let timer: usize = 30;

    // start at root, no valves open
    let start = State::create(0, 0, data.root().idx, 0, timer, data, max_flow);

    // the queue for searching
    let mut queue = BinaryHeap::new();
    queue.push(start);

    // do not visit the same spot with the same opened valves again
    let mut seen = HashMap::from([((start.idx, start.opened), max_flow * timer)]);

    while let Some(s) = queue.pop() {
        // do not explore further if timer elapsed or all valves open,
        // just see if there is something better in the queue
        if s.timer == 0 || s.opened == all_opened {
            return s.potential;
        }

        let v = data.get(s.idx);

        let can_o = (s.opened & 1 << v.idx) == 0 && v.flow > 0;

        for (o, adj) in once((true, v))
            .chain(v.tunnels.iter().map(|&idx| (false, data.get(idx))))
            .filter(|&(o, _)| !o || can_o)
        {
            let opened = s.opened | if o { 1 << adj.idx } else { 0 };
            let flow = s.flow + if o { adj.flow } else { 0 };
            let next = State::create(
                s.pressure + s.flow,
                flow,
                adj.idx,
                opened,
                s.timer - 1,
                data,
                max_flow,
            );
            let v = seen.entry((next.idx, next.opened)).or_insert(0);
            if next.potential.gt(v) {
                *v = next.potential;
                queue.push(next);
            }
        }
    }

    unreachable!();
}
Star 2

Same solution with two agents (me + elephant), not really anything new compared to star 1.

pub fn star_2(data: &PuzzleData) -> usize {
    // search state
    // - pressure potential
    // - pressure
    // - flow (valves opened so far)
    // - idx (positions, sorted)
    // - opened (valves opened so far)
    // - timer (time left before eruption)
    #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    struct State {
        potential: usize,
        pressure: usize,
        flow: usize,
        idx: [usize; 2],
        opened: u64,
        timer: usize,
    }

    impl State {
        fn create(
            pressure: usize,
            flow: usize,
            idx: [usize; 2],
            opened: u64,
            timer: usize,
            data: &PuzzleData,
            max_flow: usize,
        ) -> Self {
            let mut potential = pressure;
            let mut flow_ = 0;
            if timer >= 1 {
                // in next step, pressure will be increased by flow
                flow_ += flow;
                potential += flow_;
            }
            if timer >= 2 {
                // in the 2nd step, pressure will at most be increased by flow
                // of current valves. If they are not opened in the next step
                // (agents move instead), the flow will not change
                for idx_ in idx {
                    if opened & 1 << idx_ == 0 {
                        flow_ += data.get(idx_).flow;
                    }
                }
                potential += flow_;

                // upper bound for subsequent steps: all valves open
                potential += (timer - 2) * max_flow;
            }

            Self {
                potential,
                pressure,
                flow,
                idx,
                opened,
                timer,
            }
        }
    }

    // all valves opened / max flow
    let (all_opened, max_flow) = data
        .valves()
        .iter()
        .filter(|v| v.flow > 0)
        .fold((0, 0), |(o, f), v| (o | 1 << v.idx, f + v.flow));

    // max time
    let timer: usize = 26;

    // start at root, no valves open
    let start = State::create(0, 0, [data.root().idx; 2], 0, timer, data, max_flow);

    // the queue for searching
    let mut queue = BinaryHeap::new();
    queue.push(start);

    // do not visit the same spot with the same opened valves again
    let mut seen = HashMap::from([((start.idx, start.opened), start.potential)]);

    while let Some(s) = queue.pop() {
        // do not explore further if timer elapsed or all valves open,
        // just see if there is something better in the queue
        if s.timer == 0 || s.opened == all_opened {
            return s.potential;
        }

        let v_1 = data.get(s.idx[0]);
        let v_2 = data.get(s.idx[1]);

        let can_o_1 = (s.opened & 1 << v_1.idx) == 0 && v_1.flow > 0;
        let can_o_2 = v_1.idx != v_2.idx && (s.opened & 1 << v_2.idx) == 0 && v_2.flow > 0;

        for (o_1, adj_1) in once((true, v_1))
            .chain(v_1.tunnels.iter().map(|&idx| (false, data.get(idx))))
            .filter(|&(o, _)| !o || can_o_1)
        {
            for (o_2, adj_2) in once((true, v_2))
                .chain(v_2.tunnels.iter().map(|&idx| (false, data.get(idx))))
                .filter(|&(o, _)| !o || can_o_2)
            {
                let opened = s.opened
                    | if o_1 { 1 << adj_1.idx } else { 0 }
                    | if o_2 { 1 << adj_2.idx } else { 0 };
                let flow =
                    s.flow + if o_1 { adj_1.flow } else { 0 } + if o_2 { adj_2.flow } else { 0 };
                let next = State::create(
                    s.pressure + s.flow,
                    flow,
                    [adj_1.idx.min(adj_2.idx), adj_1.idx.max(adj_2.idx)],
                    opened,
                    s.timer - 1,
                    data,
                    max_flow,
                );
                let v = seen.entry((next.idx, next.opened)).or_insert(0);
                if next.potential.gt(v) {
                    *v = next.potential;
                    queue.push(next);
                }
            }
        }
    }

    unreachable!();
}
Tests

One additional test to see whether my calculations come up with the correct result but following the path defined in the puzzle for part 2. They do ;)

#[cfg(test)]
mod tests {
    use std::collections::{HashMap, HashSet};

    use super::*;

    const CONTENT: &str = r#"Valve AA has flow rate=0; tunnels lead to valves DD, II, BB
Valve BB has flow rate=13; tunnels lead to valves CC, AA
Valve CC has flow rate=2; tunnels lead to valves DD, BB
Valve DD has flow rate=20; tunnels lead to valves CC, AA, EE
Valve EE has flow rate=3; tunnels lead to valves FF, DD
Valve FF has flow rate=0; tunnels lead to valves EE, GG
Valve GG has flow rate=0; tunnels lead to valves FF, HH
Valve HH has flow rate=22; tunnel leads to valve GG
Valve II has flow rate=0; tunnels lead to valves AA, JJ
Valve JJ has flow rate=21; tunnel leads to valve II
"#;

    #[test]
    pub fn test_from() {
        let data = PuzzleData::from(CONTENT);
        let root = data.root();
        assert_eq!("AA", root.name);
        assert_eq!(
            vec!["DD", "II", "BB"],
            root.tunnels
                .iter()
                .map(|&idx| data.get(idx).name)
                .collect::<Vec<_>>()
        );
        println!("{data:?}");
    }

    #[test]
    pub fn test_star_1() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(1_651, star_1(&data));
    }

    #[test]
    pub fn test_example() {
        let data = PuzzleData::from(CONTENT);
        let map = data
            .valves()
            .iter()
            .map(|v| (v.name, v.idx))
            .collect::<HashMap<_, _>>();

        let steps = [
            (1, false, "DD"),
            (2, true, "DD"),
            (3, false, "CC"),
            (4, false, "BB"),
            (5, true, "BB"),
            (6, false, "AA"),
            (7, false, "II"),
            (8, false, "JJ"),
            (9, true, "JJ"),
            (10, false, "II"),
            (11, false, "AA"),
            (12, false, "DD"),
            (13, false, "EE"),
            (14, false, "FF"),
            (15, false, "GG"),
            (16, false, "HH"),
            (17, true, "HH"),
            (18, false, "GG"),
            (19, false, "FF"),
            (20, false, "EE"),
            (21, true, "EE"),
            (22, false, "DD"),
            (23, false, "CC"),
            (24, true, "CC"),
        ];

        let max_flow: usize = data.valves().iter().map(|v| v.flow).sum();
        let timer = 30;

        #[derive(Debug)]
        struct State {
            potential: usize,
            pressure: usize,
            flow: usize,
            idx: usize,
            opened: u64,
            timer: usize,
        }

        let mut s = State {
            potential: timer * max_flow,
            pressure: 0,
            flow: 0,
            idx: *map.get("AA").unwrap(),
            opened: 0,
            timer: timer,
        };

        let mut seen = HashSet::new();
        seen.insert((s.idx, s.opened));

        for (minute, open, valve) in steps {
            let idx = *map.get(valve).unwrap();
            let v = data.get(idx);
            assert!(idx == s.idx || !open);
            assert!(open || data.get(s.idx).tunnels.contains(&idx));
            s = State {
                potential: s.pressure + s.flow + (s.timer - 1) * max_flow,
                pressure: s.pressure + s.flow,
                flow: if open { s.flow + v.flow } else { s.flow },
                idx: v.idx,
                opened: if open { s.opened | 1 << idx } else { s.opened },
                timer: s.timer - 1,
            };
            assert!(seen.insert((s.idx, s.opened)));
            println!(
                "{minute}, {} {valve}, open valves: {:?} ({s:?})",
                if open { "opened" } else { "moved to" },
                data.valves()
                    .iter()
                    .filter(|v| (s.opened & 1 << v.idx) > 0)
                    .map(|v| v.name)
                    .collect::<Vec<_>>()
            );
        }
        println!(
            "{} + {} * {} = {}, {}",
            s.pressure,
            s.timer,
            s.flow,
            s.pressure + s.timer * s.flow,
            s.potential
        );

        assert_eq!(1_651, s.potential);
    }

    #[test]
    pub fn test_star_2() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(1_707, star_2(&data));
    }
}

Day 17: rust

Day 17: Pyroclastic Flow

Rust solution to AoC|2022|17.

So let’s play Tetris ;)

Because I made stupid bugs, I spent lots of time displaying the chamber and figuring out what I did wrong. My biggest mistake was to overwrite occupied space by empty space when a rock comes to rest and is not filling a full rectangle. Unfortunately, with this bug, I still get the correct result for the example data.

Part 2 is about finding a pattern that repeats itself. I do that by looking at the top 30 rows. The number 30 is kind of arbitrarily chosen.

Input

I just read the input in a byte array.

pub mod input {
    #[derive(Debug)]
    pub struct PuzzleData<'a> {
        jets: &'a [u8],
    }

    impl<'a> From<&'a str> for PuzzleData<'a> {
        /// parse the puzzle input
        fn from(s: &'a str) -> Self {
            Self {
                jets: s.trim().as_bytes(),
            }
        }
    }

    impl<'a> PuzzleData<'a> {
        pub fn jets(&self) -> &'a [u8] {
            &self.jets
        }
    }
}
General solution

I have a struct Chamber with a method integrate_rock. This takes a rock and let’s it move in the chamber until it comes to rest.

My first version did not have the .filter(|&k| rock[k] == b'#') part in the update chamber loop at the end of the integrate_rock function. I typed these 29 characters with an average speed of about 12 character / hour.

pub struct Chamber<'a> {
    chamber: Vec<u8>,
    jets: RingBuffer<'a, u8>,
    rocks: RingBuffer<'static, (&'static str, usize)>,
}

impl<'a> From<&PuzzleData<'a>> for Chamber<'a> {
    fn from(data: &PuzzleData<'a>) -> Self {
        Self {
            chamber: Vec::new(),
            jets: data.jets().into(),
            rocks: Self::ROCKS.into(),
        }
    }
}

impl Chamber<'_> {
    const WIDTH: usize = 7;
    const ROCKS: &'static [(&'static str, usize)] = &[
        ("####", 4),
        (".#.###.#.", 3),
        ("###..#..#", 3),
        ("####", 1),
        ("####", 2),
    ];
    const X0: usize = 2;
    const DY0: usize = 3;

    pub fn check(&self, rock: &[u8], x: usize, y: usize, w: usize) -> bool {
        (0..rock.len())
            .filter(|&k| rock[k] == b'#')
            .filter(|k| y + k / w < self.height())
            .all(|k| self.chamber[x + k % w + (y + k / w) * Self::WIDTH] == b'.')
    }

    pub fn height(&self) -> usize {
        self.chamber.len() / Self::WIDTH
    }

    pub fn top(&self, rows: usize) -> (Vec<u8>, usize, usize) {
        (
            self.chamber[self.chamber.len() - rows * Self::WIDTH..].to_vec(),
            self.rocks.pos(),
            self.jets.pos(),
        )
    }

    pub fn integrate_rock<F>(&mut self, f: F)
    where
        F: Fn(&[u8], &[u8], usize, usize, usize),
    {
        let &(rock, w) = self.rocks.next();
        let rock = rock.as_bytes();

        let mut x = Self::X0;
        let mut y = self.height() + Self::DY0;

        let mut stop = false;

        while !stop {
            f(&self.chamber, &rock, x, y, w);
            let &jet = self.jets.next();
            x = if jet == b'<' && x > 0 && self.check(rock, x - 1, y, w) {
                x - 1
            } else if jet == b'>' && x + w < Chamber::WIDTH && self.check(rock, x + 1, y, w) {
                x + 1
            } else {
                x
            };

            if y == 0 {
                stop = true;
            } else if y <= self.height() {
                if self.check(rock, x, y - 1, w) {
                    y -= 1;
                } else {
                    stop = true;
                }
            } else {
                y -= 1;
            }

            if stop {
                // add new lines to chamber
                while self.height() < y + rock.len() / w {
                    self.chamber.extend([b'.'; Self::WIDTH])
                }
                // update chamber
                for k in (0..rock.len()).filter(|&k| rock[k] == b'#') {
                    self.chamber[x + k % w + (y + k / w) * Self::WIDTH] = rock[k];
                }
            }
        }
    }
}
Star 1

Just integrate 2022 rocks.

pub fn star_1(data: &PuzzleData) -> usize {
    let mut chamber = Chamber::from(data);

    for _ in 0..2022 {
        chamber.integrate_rock(|_, _, _, _, _| ());
    }

    chamber.height()
}
Star 2

Integrate rocks until a situation as defined by top rows of chamber, current jet position, current rock repeats. Calculate how much hight we gain by repeating this full cycle as often as it fits in the required number of rocks to integrate and simulate the remaining steps.

pub fn star_2(data: &PuzzleData) -> usize {
    let mut chamber = Chamber::from(data);

    let mut seen = HashMap::new();
    let rows = 30; // this is somehow arbitrary

    let rounds: usize = 1_000_000_000_000;

    let mut cnt = 0;
    while chamber.height() < rows {
        chamber.integrate_rock(|_, _, _, _, _| ());
        cnt += 1;
    }

    for cur in cnt..rounds {
        if let Some((prev, prev_height)) = seen.insert(chamber.top(rows), (cur, chamber.height())) {
            let d_round = cur - prev;
            let d_height = chamber.height() - prev_height;

            let n = (rounds - cur) / d_round;
            let h = n * d_height;

            let rem = (rounds - cur) % d_round;
            for _ in 0..rem {
                chamber.integrate_rock(|_, _, _, _, _| ());
            }

            return chamber.height() + h;
        }
        chamber.integrate_rock(|_, _, _, _, _| ());
    }

    unreachable!()
}
Tests

Tests did not help a lot today.

#[cfg(test)]
mod tests {
    use super::*;

    const CONTENT: &str = r#">>><<><>><<<>><>>><<<>>><<<><<<>><>><<>>"#;

    #[test]
    pub fn test_from() {
        let data = PuzzleData::from(CONTENT);
        println!("{data:?}");
    }

    #[test]
    pub fn test_star_1() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(3_068, star_1(&data));
    }

    #[test]
    pub fn test_star_2() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(1_514_285_714_288, star_2(&data));
    }
}

My testing approach was to print various configurations using very nice printing code. Obviously, I removed all the println! and if debug {} statements before I publish my code.

pub struct RockInChamber<'a, 'b> {
    pub chamber: &'a [u8],
    pub rock: &'b [u8],
    pub x: usize,
    pub y: usize,
    pub w: usize,
    pub rock_part: usize,
    pub print_lim: usize,
}

impl Default for RockInChamber<'_, '_> {
    fn default() -> Self {
        Self {
            chamber: &[],
            rock: &[],
            x: 0,
            y: 0,
            w: 1,
            rock_part: 8,
            print_lim: 17,
        }
    }
}

impl std::fmt::Display for RockInChamber<'_, '_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let h = self.chamber.len() / Chamber::WIDTH;
        let y_mx = h + self.rock_part;
        let y_mn = 0.max(h - self.print_lim.min(h));
        for y_ in (y_mn..y_mx).rev() {
            '|'.fmt(f)?;
            for x_ in 0..Chamber::WIDTH {
                if x_ >= self.x
                    && x_ < self.x + self.w
                    && y_ >= self.y
                    && y_ < self.y + self.rock.len() / self.w
                    && self.rock[x_ - self.x + (y_ - self.y) * self.w] == b'#'
                {
                    '@'.fmt(f)?;
                } else if y_ < h {
                    (self.chamber[x_ + y_ * Chamber::WIDTH] as char).fmt(f)?;
                } else {
                    '.'.fmt(f)?;
                }
            }
            "|\n".fmt(f)?;
        }
        if y_mn == 0 {
            '+'.fmt(f)?;
            for _ in 0..Chamber::WIDTH {
                '-'.fmt(f)?;
            }
            "+".fmt(f)?;
        } else {
            "|~y=".fmt(f)?;
            (y_mn - 1).fmt(f)?;
            "~".fmt(f)?;
        }
        Ok(())
    }
}

impl std::fmt::Display for Chamber<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        RockInChamber {
            chamber: &self.chamber,
            ..RockInChamber::default()
        }
        .fmt(f)
    }
}
Animation

Just for fun, I created an example with a little animation (simulates 2022 rocks using the example data). Run with cargo run --release --example animate — <ms> where <ms> is the amount of ms to sleep after each animation step, defaults to 40.

fn main() {
    // accept a single numeric argumnet
    let arg = env::args().skip(1).next();
    let wait = arg
        .map(|arg| arg.parse().expect("a positive amount of ms to pause"))
        .unwrap_or(40);

    let mut chamber = Chamber::from(&CONTENT.into());
    for k in 0..2022 {
        chamber.integrate_rock(|chamber, rock, x, y, w| {
            print!("\x1B[1;1H\x1B[J"); // clear console
            println!(
                "{}",
                RockInChamber {
                    chamber,
                    rock,
                    x,
                    y,
                    w,
                    ..RockInChamber::default()
                }
            );
            println!("Rock {}", k + 1);
            thread::sleep(Duration::from_millis(wait));
        });
    }
}

Day 18: rust

Day 18: Boiling Boulders

Rust solution to AoC|2022|18.

Input

Parse the input in a vec of 3D coordinates.

pub mod input {
    #[derive(Debug)]
    pub struct PuzzleData {
        cubes: Vec<(isize, isize, isize)>,
    }

    impl<'a> From<&'a str> for PuzzleData {
        /// parse the puzzle input
        fn from(s: &'a str) -> Self {
            Self {
                cubes: s
                    .lines()
                    .map(|line| line.split(','))
                    .map(|mut split| {
                        (
                            split.next().unwrap().parse().unwrap(),
                            split.next().unwrap().parse().unwrap(),
                            split.next().unwrap().parse().unwrap(),
                        )
                    })
                    .collect(),
            }
        }
    }

    impl<'a> PuzzleData {
        pub fn cubes(&self) -> &[(isize, isize, isize)] {
            &self.cubes
        }
    }
}
Star 1

I use bits of bytes to store whether a side of a cube is touching a side of another cube. I start with all bits set to 1 and set them to zero by looking at all pairs of cubes.

pub fn star_1_pairwise_comp(data: &PuzzleData) -> usize {
    let cubes = data.cubes();

    let mut sides = vec![0b111111u8; cubes.len()];

    for k1 in 0..cubes.len() - 1 {
        let (x_1, y_1, z_1) = cubes[k1];
        for k2 in k1 + 1..cubes.len() {
            let (x_2, y_2, z_2) = cubes[k2];
            if x_1 == x_2 + 1 && y_1 == y_2 && z_1 == z_2 {
                sides[k1] &= !(1 << 0);
                sides[k2] &= !(1 << 1);
            } else if x_1 == x_2 - 1 && y_1 == y_2 && z_1 == z_2 {
                sides[k1] &= !(1 << 1);
                sides[k2] &= !(1 << 0);
            } else if x_1 == x_2 && y_1 == y_2 + 1 && z_1 == z_2 {
                sides[k1] &= !(1 << 2);
                sides[k2] &= !(1 << 3);
            } else if x_1 == x_2 && y_1 == y_2 - 1 && z_1 == z_2 {
                sides[k1] &= !(1 << 3);
                sides[k2] &= !(1 << 2);
            } else if x_1 == x_2 && y_1 == y_2 && z_1 == z_2 + 1 {
                sides[k1] &= !(1 << 4);
                sides[k2] &= !(1 << 5);
            } else if x_1 == x_2 && y_1 == y_2 && z_1 == z_2 - 1 {
                sides[k1] &= !(1 << 5);
                sides[k2] &= !(1 << 4);
            }
        }
    }

    sides.iter().map(|s| s.count_ones() as usize).sum()
}
Star 2

The approach for part 2 is totally different from the approach for part 1.

I calculate the bounding box containing all cubes of a droplet and do a breadth first traversal starting from all 8 corners of the bounding box (enlarged by one in each direction) at the same time. The traversal will not exit this enlarged bounding box.

Whenever the traversal hits a cube contained in the droplet, there is exactly on side facing outwards to be added to the count.

Interestingly, the solution for the second part is calculated faster than the solution for the first part in my case.

pub fn star_2(data: &PuzzleData) -> usize {
    let ((x_mn, y_mn, z_mn), (x_mx, y_mx, z_mx)) = data.cubes().iter().fold(
        (
            (isize::MAX, isize::MAX, isize::MAX),
            (isize::MIN, isize::MIN, isize::MIN),
        ),
        |(mn, mx), c| {
            (
                (mn.0.min(c.0 - 1), mn.1.min(c.1 - 1), mn.2.min(c.2 - 1)),
                (mx.0.max(c.0 + 1), mx.1.max(c.1 + 1), mx.2.max(c.2 + 1)),
            )
        },
    );

    let droplet: HashSet<(isize, isize, isize)> = HashSet::from_iter(data.cubes().iter().cloned());

    let mut queue = VecDeque::new();
    for x in [x_mn, x_mx] {
        for y in [y_mn, y_mx] {
            for z in [z_mn, z_mx] {
                queue.push_back((x, y, z));
            }
        }
    }

    let mut seen: HashSet<(isize, isize, isize)> = HashSet::from_iter(queue.iter().cloned());

    let mut sides = 0;

    while let Some((x, y, z)) = queue.pop_front() {
        for a in [
            (x + 1, y, z),
            (x - 1, y, z),
            (x, y + 1, z),
            (x, y - 1, z),
            (x, y, z + 1),
            (x, y, z - 1),
        ]
        .into_iter()
        .filter(|&(x, y, z)| {
            x >= x_mn && y >= y_mn && z >= z_mn && x <= x_mx && y <= y_mx && z <= z_mx
        }) {
            if droplet.contains(&a) {
                sides += 1;
            } else if seen.insert(a) {
                queue.push_back(a);
            }
        }
    }

    sides
}
Star 1 variant

After part 2 was done, I re-created a solution for part 1 based on the same idea.

This time, I do a breadth first traversal inside the droplet. Since the droplet is not necessarily connected, I need to wrap everything in an additional loop until all cubes are processed. To do so, I replace the seen set (which is initially empty) by a remain set, which initially contains all cubes, and from which I remove all cubes that are processed.

pub fn star_1_traversal(data: &PuzzleData) -> usize {
    let droplet: HashSet<(isize, isize, isize)> = HashSet::from_iter(data.cubes().iter().cloned());

    let mut sides = 0;

    let mut queue = VecDeque::new();
    let mut remain = droplet.clone();
    while !remain.is_empty() {
        let &start = remain.iter().next().unwrap();
        remain.remove(&start);

        queue.push_back(start);

        while let Some((x, y, z)) = queue.pop_front() {
            for a in [
                (x + 1, y, z),
                (x - 1, y, z),
                (x, y + 1, z),
                (x, y - 1, z),
                (x, y, z + 1),
                (x, y, z - 1),
            ] {
                if remain.remove(&a) {
                    // cube in droplet and not yet seen
                    queue.push_back(a);
                } else if !droplet.contains(&a) {
                    // cube which is direct adjacent is not contained
                    sides += 1;
                }
            }
        }
    }

    sides
}
Tests
#[cfg(test)]
mod tests {
    use super::*;

    const CONTENT: &str = r#"2,2,2
1,2,2
3,2,2
2,1,2
2,3,2
2,2,1
2,2,3
2,2,4
2,2,6
1,2,5
3,2,5
2,1,5
2,3,5
"#;

    #[test]
    pub fn test_from() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(13, data.cubes().len());
        assert_eq!((2, 1, 2), data.cubes()[3]);
        println!("{data:?}");
    }

    #[test]
    pub fn test_star_1() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(64, star_1_pairwise_comp(&data));
        assert_eq!(64, star_1_traversal(&data));
    }

    #[test]
    pub fn test_star_2() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(58, star_2(&data));
    }
}

Day 19: rust

Day 19: Not Enough Minerals

Rust solution to AoC|2022|19.

I was basically poking in the fog and brute forcing the solution.

After I had a working solution, I looked for help in the reddit solution megathread and re-worked my solution.

There are three main elements that lead to acceptable solution time:

  1. Do not simulate every minute but rather decide which robot to build next and simulate as many minutes as required to build that robot at once

  2. Understand that only one robot can be made per minute, which implies that there is no point in having more Ore, Clay or Obsidian collecting robots than the maximum Ore, Clay or Obsidian required to produce a single robot of any kind

  3. Do a depth first search to have terminal states quickly and discard all branches for which an upper bound can be calculated which is below the current optimum (this is kind of an A* idea)

Input

I parse the blueprints into a vec containing the amount of Ore, Clay and Obsidian required to build any of the Ore collecting, Clay collecting, Obsidian collecting or Geode opening robots. This results in many 0’s but makes the implementation of the search algorithm much easier.

pub mod input {
    use crate::{CLAY, GEODE, OBSIDIAN, ORE};

    #[derive(Debug)]
    pub struct PuzzleData {
        pub blueprints: Vec<[usize; 12]>,
    }

    fn parse_blueprint(line: &str) -> [usize; 12] {
        let mut blueprint = [0; 12];

        let mut words = line.split_ascii_whitespace().skip(6); // blueprint <id>: each ore robot costs
        blueprint[ORE + 3 * ORE] = words.next().unwrap().parse().unwrap(); // XX: ore for ore robot
        let mut words = words.skip(5); // ore. each clay robot costs
        blueprint[ORE + 3 * CLAY] = words.next().unwrap().parse().unwrap(); // XX: ore for clay robot
        let mut words = words.skip(5); // ore. each obsidian robot costs
        blueprint[ORE + 3 * OBSIDIAN] = words.next().unwrap().parse().unwrap(); // XX: ore for obsidian robot
        let mut words = words.skip(2); // ore and
        blueprint[CLAY + 3 * OBSIDIAN] = words.next().unwrap().parse().unwrap(); // XX: clay for obsidian robot
        let mut words = words.skip(5); // clay. each geode robot costs
        blueprint[ORE + 3 * GEODE] = words.next().unwrap().parse().unwrap(); // XX: ore for geode robot
        let mut words = words.skip(2); // ore and
        blueprint[OBSIDIAN + 3 * GEODE] = words.next().unwrap().parse().unwrap(); // XX: obsidian for geode robot
        assert_eq!(Some("obsidian."), words.next());
        assert_eq!(None, words.next());

        blueprint
    }

    impl From<&str> for PuzzleData {
        /// parse the puzzle input
        fn from(s: &str) -> Self {
            Self {
                blueprints: s.lines().map(parse_blueprint).collect(),
            }
        }
    }
}
Star 1

My depth-first search implementation:

pub fn max_geodes(blueprint: &[usize], steps: usize) -> usize {
    // start with one ore robot, zero material and steps to go
    let start = ([1usize, 0, 0, 0], [0; 4], steps);

    // initialize queue
    let mut queue = Vec::from([start]);

    // maximum required amount of robots per type
    let max_req = (ORE..=OBSIDIAN).fold([usize::MAX; 4], |mut max_req, m| {
        max_req[m] = (ORE..=GEODE).map(|r| blueprint[m + 3 * r]).max().unwrap();
        max_req
    });

    // optimum
    let mut opt: usize = 0;

    while let Some((robots, materials, steps)) = queue.pop() {
        // update optimum, including geodes opened in remaining steps
        opt = opt.max(materials[GEODE] + robots[GEODE] * steps);

        // time elapsed
        if steps == 0 {
            continue;
        }

        // if geodes opened by making a geode robot in every subsequent step does not lead to
        // an improvement, stop here
        if (0..steps).fold(materials[GEODE], |bound, step| {
            bound + (robots[GEODE] + step) * (steps - step)
        }) < opt
        {
            continue;
        }

        for r in (ORE..=GEODE).rev().filter(|&r| robots[r] < max_req[r]) {
            // calculate steps required to build robot
            let Some(s) = (ORE..=OBSIDIAN)
                .map(|m| {
                    if materials[m] >= blueprint[m + 3 * r] {
                        Some(0)
                    } else if robots[m] == 0 {
                        None
                    } else {
                        Some((blueprint[m + 3 * r] - materials[m] + robots[m] - 1) / robots[m])
                    }
                })
                .fold(Some(0), |s_max, s| match (s_max, s) {
                    (Some(s_max), Some(s)) => Some(s_max.max(s)),
                    _ => None,
                }) else {
                    // can't build robot
                    continue
                };

            if s + 1 > steps {
                // time elapsed
                continue;
            }

            // update robots and materials
            let mut materials = materials;
            for m in ORE..=OBSIDIAN {
                materials[m] += (s + 1) * robots[m];
                materials[m] -= blueprint[m + 3 * r];
            }
            materials[GEODE] += (s + 1) * robots[GEODE];
            let mut robots = robots;
            robots[r] += 1;

            // push to queue
            queue.push((robots, materials, steps - s - 1));
        }
    }

    opt
}

This is used for part 1 as

pub fn star_1(data: &PuzzleData) -> usize {
    data.blueprints
        .iter()
        .map(|blueprint| max_geodes(blueprint, 24))
        .enumerate()
        .map(|(k, opt)| (k + 1) * opt)
        .sum()
}
Star 2

The same solution works for part 2:

pub fn star_2(data: &PuzzleData) -> usize {
    data.blueprints
        .iter()
        .take(3)
        .map(|blueprint| max_geodes(blueprint, 32))
        .product()
}
Tests
#[cfg(test)]
mod tests {
    use super::*;

    const CONTENT: &str = r#"Blueprint 1: Each ore robot costs 4 ore. Each clay robot costs 2 ore. Each obsidian robot costs 3 ore and 14 clay. Each geode robot costs 2 ore and 7 obsidian.
Blueprint 2: Each ore robot costs 2 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 8 clay. Each geode robot costs 3 ore and 12 obsidian.
"#;

    #[test]
    pub fn test_from() {
        let data = PuzzleData::from(CONTENT);
        println!("{data:?}");
    }

    #[test]
    pub fn test_star_1() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(9 * 1 + 12 * 2, star_1(&data));
    }

    #[test]
    pub fn test_star_2() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(56 * 62, star_2(&data));
    }
}
Today I learned

Brute force sometimes works but it is no fun, and there is a lot to learn on how to solve these kind of problems …​

Day 20: rust

Day 20: Grove Positioning System

Rust solution to AoC|2022|20.

Looked simple but was more tricky than expected.

The example data turned out to be too friendly for me.

Input
pub mod input {
    #[derive(Debug)]
    pub struct PuzzleData {
        pub numbers: Vec<isize>,
    }

    impl From<&str> for PuzzleData {
        /// parse the puzzle input
        fn from(s: &str) -> Self {
            Self {
                numbers: s.trim().lines().map(|l| l.parse().unwrap()).collect(),
            }
        }
    }
}
General Solution

I decided to store the data in a kind of doubly linked list. I implemented this by creating a list of indices to the predecessor and successor of each number. The numbers themselves are stored in a vec which is unchanged all the time (this is useful since we need to process along the initial order of the numbers)

pub fn mix(numbers: &[isize], times: usize) -> isize {
    let n = numbers.len();
    let mut indices = (0..n)
        .map(|k| ((k + n - 1) % n, (k + 1) % n))
        .collect::<Vec<_>>();

    let k0 = numbers.iter().position(|&v| v == 0).unwrap();

    for _ in 0..times {
        for k in 0..n {
            mix_step(&mut indices, numbers, k);
        }
    }

    let k1 = (0..1000).fold(k0, |k, _| indices[k].1);
    let k2 = (0..1000).fold(k1, |k, _| indices[k].1);
    let k3 = (0..1000).fold(k2, |k, _| indices[k].1);

    numbers[k1] + numbers[k2] + numbers[k3]
}

The key is to perform a single mix step correctly. I failed to do so in all possible ways. To be able to test this properly, I put it into a separate function and created my own test cases for it (see below).

pub fn mix_step(indices: &mut Vec<(usize, usize)>, numbers: &[isize], k: usize) {
    let v = numbers[k];
    let steps = v % (numbers.len() as isize - 1);

    // move in direction of lower number of steps
    let steps = if steps > (numbers.len() as isize - 1) / 2 {
        steps - (numbers.len() as isize - 1)
    } else if steps < -(numbers.len() as isize - 1) / 2 {
        steps + (numbers.len() as isize - 1)
    } else {
        steps
    };

    if steps != 0 {
        if steps > 0 {
            // k_pre k k_post .. idx idx_post => k_pre k_post .. idx k idx_post
            let idx = (0..steps).fold(k, |k, _| indices[k].1);
            let (k_pre, k_post) = indices[k];
            let (_, idx_post) = indices[idx];
            indices[k_post].0 = k_pre;
            indices[k_pre].1 = k_post;
            indices[k] = (idx, idx_post);
            indices[idx].1 = k;
            indices[idx_post].0 = k;
        } else {
            // idx_pre idx .. k_pre k k_post => idx_pre k idx .. k_pre k_post
            let idx = (steps..0).fold(k, |k, _| indices[k].0);
            let (k_pre, k_post) = indices[k];
            let (idx_pre, _) = indices[idx];
            indices[k_post].0 = k_pre;
            indices[k_pre].1 = k_post;
            indices[k] = (idx_pre, idx);
            indices[idx].0 = k;
            indices[idx_pre].1 = k;
        };
    }
}
Star 1

Just call the mix function with times = 1.

Star 2

Call the mix function with numbers multiplied by the decryption key and times = 10:

pub fn star_2(data: &PuzzleData) -> isize {
    mix(
        &data
            .numbers
            .iter()
            .map(|v| v * 811_589_153)
            .collect::<Vec<_>>(),
        10,
    )
}
Tests

The most difficult part today: create test_mix_step and make it pass.

#[cfg(test)]
mod tests {
    use std::collections::HashSet;

    use super::*;

    const CONTENT: &str = r#"1
2
-3
3
-2
0
4
"#;

    #[test]
    pub fn test_mix_step() {
        let n = 21;
        let numbers: &[isize] = &[
            0,
            -1,
            -2,
            1,
            2,
            n - 2,
            n - 1,
            n,
            2 * n - 4,
            2 * n - 3,
            2 * n - 2,
            2 * n - 1,
            2 * n,
            -n - 1,
            -n,
            -n + 1,
            -2 * n - 3,
            -2 * n - 2,
            -2 * n - 1,
            -2 * n,
            -2 * n + 1,
        ];

        println!(
            "{:?}",
            numbers
                .iter()
                .map(|v| (v, v % (n - 1), v.rem_euclid(n - 1)))
                .collect::<Vec<_>>()
        );

        let exp = vec![
            (-2 * n + 1, -1),       // 0
            (-2 * n + 1, 0),        // -1
            (-2 * n + 1, 0),        // -2
            (2, n - 2),             // 1
            (n - 1, n),             // 2
            (1, 2),                 // n - 2 = 19
            (n - 2, n),             // n - 1 = 20
            (2 * n - 4, 2 * n - 3), // n = 21
            (n - 2, n - 1),         // 2 n - 4 = 38
            (n, 2 * n - 4),         // 2 n - 3 = 39
            (2 * n - 3, 2 * n - 1), // 2 n - 2 = 40
            (2 * n, -n - 1),        // 2 n - 1 = 41
            (-n, -n + 1),           // 2 n = 42
            (2 * n - 2, 2 * n - 1), // -n-1 = -22
            (2 * n, -n - 1),        // -n = -21
            (-n, -2 * n - 3),       // -n+1 = -20
        ];
        let n = n as usize;

        let to_vec = |indices: &Vec<(usize, usize)>| {
            (0..n)
                .scan(0, |k, _| {
                    let v = numbers[*k];
                    *k = indices[*k].1;
                    Some(v)
                })
                .collect::<Vec<isize>>()
        };

        let indices = (0..n)
            .map(|k| ((k + n - 1) % n, (k + 1) % n))
            .collect::<Vec<_>>();
        assert_eq!(numbers, to_vec(&indices));

        for k in 0..n {
            let mut indices = (0..n)
                .map(|k| ((k + n - 1) % n, (k + 1) % n))
                .collect::<Vec<_>>();
            mix_step(&mut indices, numbers, k);
            println!("numbers[{k}] = {} => {:?}", numbers[k], to_vec(&indices));

            if k < exp.len() {
                assert_eq!(
                    exp[k],
                    (numbers[indices[k].0], numbers[indices[k].1]),
                    "at k = {k}"
                );
            }

            let mut k_fwd = 0;
            let mut k_rev = 0;
            let mut fwds = HashSet::from([0]);
            let mut revs = HashSet::from([0]);
            for _ in 0..n {
                k_fwd = indices[k_fwd].1;
                k_rev = indices[k_rev].1;
                fwds.insert(k_fwd);
                revs.insert(k_rev);
            }
            // full cycle in both directions
            assert_eq!(
                0, k_fwd,
                "incomplete forward cycle\nk: {k}\n numbers: {numbers:?}\n indices: {indices:?}"
            );
            assert_eq!(
                0, k_rev,
                "incomplete backwards cycle\nk: {k}\n numbers: {numbers:?}\n indices: {indices:?}"
            );
            assert_eq!(
                n,
                fwds.len(),
                "items not in forward cycle\nk: {k}\n numbers: {numbers:?}\n indices: {indices:?}"
            );
            assert_eq!(
                n,
                revs.len(),
                "items not in backwards cycle\nk: {k}\n numbers: {numbers:?}\n indices: {indices:?}"
            );
        }
    }

    #[test]
    pub fn test_from() {
        let data = PuzzleData::from(CONTENT);
        println!("{data:?}");
    }

    #[test]
    pub fn test_star_1() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(3, mix(&data.numbers, 1));
    }

    #[test]
    pub fn test_star_2() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(1_623_178_306, star_2(&data));
    }
}

Day 21: rust

Day 21: TODO

Rust solution to AoC|2022|21.

Primary school math today…​

Input

I parse the input into a map with monkey names as keys and Yell enums as values. A Yell is either a Number or an Operation referencing other monkey by name. The Unknown variant is required for star 2.

pub mod input {
    use std::collections::HashMap;

    #[derive(Debug, Clone)]
    pub enum Yell<'a> {
        Operation(&'a str, &'a str, &'a str),
        Number(isize),
        Unknown, // required for part 2
    }

    pub fn parse_yell<'a>(line: &'a str) -> (&'a str, Yell<'a>) {
        let mut words = line.split_ascii_whitespace();
        let name = words.next().unwrap().trim_end_matches(':');
        let word = words.next().unwrap();
        let yell = if (word.as_bytes()[0] as char).is_ascii_digit() {
            Yell::Number(word.parse().unwrap())
        } else {
            Yell::Operation(word, words.next().unwrap(), words.next().unwrap())
        };
        (name, yell)
    }

    #[derive(Debug)]
    pub struct PuzzleData<'a> {
        pub monkeys: HashMap<&'a str, Yell<'a>>,
    }

    impl<'a> From<&'a str> for PuzzleData<'a> {
        /// parse the puzzle input
        fn from(s: &'a str) -> Self {
            Self {
                monkeys: s.lines().map(parse_yell).collect(),
            }
        }
    }
}
Star 1

This is a trivial recursive get_result function.

pub fn star_1(data: &PuzzleData) -> isize {
    get_result(&data.monkeys, "root")
}
Star 2

For the second part, I update the map so that the value for key "humn" is Unknown and the operation for key "root" is -, so that I can solve the root value for 0.

I put the yells in a recursive tree enum YellRec reducing branches of the tree that do not contain Unknown on the fly with a reduce function.

Then I solve for the Unknown inverting operations one by one. This works if the Unknown appears in exactly one of the right hand side or left hand side arguments, which is the case for my input (and I guess for every one else’s input as well) and for the example data.

#[derive(Debug)]
pub enum YellRec<'a> {
    Operation(Box<(YellRec<'a>, &'a str, YellRec<'a>)>),
    Number(isize),
    Unknown,
}

pub fn reduce<'a>(monkeys: &HashMap<&str, Yell<'a>>, monkey: &str) -> YellRec<'a> {
    match monkeys.get(monkey) {
        Some(Yell::Operation(lhs, op, rhs)) => {
            let lhs = reduce(monkeys, lhs);
            let rhs = reduce(monkeys, rhs);
            match (lhs, rhs) {
                (YellRec::Number(lhs), YellRec::Number(rhs)) => match *op {
                    "+" => YellRec::Number(lhs + rhs),
                    "-" => YellRec::Number(lhs - rhs),
                    "*" => YellRec::Number(lhs * rhs),
                    "/" => YellRec::Number(lhs / rhs),
                    _ => panic!("Unknown operation: {op}"),
                },
                (lhs, rhs) => YellRec::Operation((lhs, *op, rhs).into()),
            }
        }
        Some(Yell::Number(v)) => YellRec::Number(*v),
        Some(Yell::Unknown) => YellRec::Unknown,
        yell => panic!("Can't get result for monkey {monkey} => {yell:?}"),
    }
}

pub fn solve(yell: &YellRec, tar: isize) -> isize {
    match yell {
        YellRec::Operation(b) => match b.as_ref() {
            (lhs, op, YellRec::Number(rhs)) => match *op {
                "+" => solve(lhs, tar - *rhs), // lhs + rhs = tar
                "-" => solve(lhs, tar + *rhs), // lhs - rhs = tar
                "*" => solve(lhs, tar / *rhs), // lhs * rhs = tar
                "/" => solve(lhs, tar * *rhs), // lhs / rhs = tar
                _ => panic!("Unknown operation: {op}"),
            },
            (YellRec::Number(lhs), op, rhs) => match *op {
                "+" => solve(rhs, tar - *lhs), // lhs + rhs = tar
                "-" => solve(rhs, *lhs - tar), // lhs - rhs = tar
                "*" => solve(rhs, tar / *lhs), // lhs * rhs = tar
                "/" => solve(rhs, *lhs / tar), // lhs / rhs = tar
                _ => panic!("Unknown operation: {op}"),
            },
            _ => panic!("solve expects either rhs or lhs to be a number"),
        },
        YellRec::Unknown => tar,
        YellRec::Number(_) => panic!("Can't solve a number"),
    }
}

pub fn star_2(data: &PuzzleData) -> isize {
    let mut monkeys = data.monkeys.clone();

    let Some(Yell::Operation(lhs, _, rhs)) = monkeys.get("root") else {panic!()};
    monkeys.insert("root", Yell::Operation(lhs, "-", rhs));
    monkeys.insert("humn", Yell::Unknown);

    solve(&reduce(&monkeys, "root"), 0)
}
Tests

I only created tests today, because my template contains them. I actually did not use them before submitting the result…​

#[cfg(test)]
mod tests {
    use super::*;

    const CONTENT: &str = r#"root: pppw + sjmn
dbpl: 5
cczh: sllz + lgvd
zczc: 2
ptdq: humn - dvpt
dvpt: 3
lfqf: 4
humn: 5
ljgn: 2
sjmn: drzm * dbpl
sllz: 4
pppw: cczh / lfqf
lgvd: ljgn * ptdq
drzm: hmdt - zczc
hmdt: 32
"#;

    #[test]
    pub fn test_from() {
        let data = PuzzleData::from(CONTENT);
        println!("{data:?}");
    }

    #[test]
    pub fn test_star_1() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(152, star_1(&data));
    }

    #[test]
    pub fn test_star_2() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(301, star_2(&data));
    }

    #[test]
    pub fn test_solution_range() {
        let data = PuzzleData::from(include_str!("../input.txt"));

        let sol_a = star_2(&data);
        let sol_b = star_2_bisection(&data);

        let sol_rg = sol_a.min(sol_b)..=sol_a.max(sol_b);
        println!("Solution range: {sol_rg:?}");


        let mut monkeys = data.monkeys.clone();
        let Some(Yell::Operation(lhs, _, rhs)) = monkeys.get("root") else {panic!()};
        monkeys.insert("root", Yell::Operation(lhs, "-", rhs));

        for sol in sol_rg {
            monkeys.insert("humn", Yell::Number(sol));
            assert_eq!(0, get_result(&monkeys, "root"));
        }
    }
}

Day 22: rust

Day 22: Monkey Map

Rust solution to AoC|2022|22.

Geometry! Cube layouts …​ what looked simple turned out to be very tedious

Input

Nothing special here today. I decided to make the lines of the grid equal length (padded with spaced) for easier processing.

pub mod input {
    #[derive(Debug, PartialEq, Eq)]
    pub enum Step {
        Fwd(usize),
        Left,
        Right,
    }

    #[derive(Debug)]
    pub struct PuzzleData {
        pub grid: Vec<u8>,
        pub width: usize,
        pub steps: Vec<Step>,
    }

    impl From<&str> for PuzzleData {
        /// parse the puzzle input
        fn from(s: &str) -> Self {
            let (grid_part, steps_part) = s.split_once("\n\n").unwrap();

            // store the grid with rows of equal width
            let lines = grid_part.lines().collect::<Vec<_>>();
            let width = lines.iter().map(|line| line.len()).max().unwrap();
            let mut grid = vec![b' '; width * lines.len()];
            for row in 0..lines.len() {
                for (col, b) in lines[row].as_bytes().iter().enumerate() {
                    grid[col + row * width] = *b;
                }
            }

            let mut steps = Vec::new();
            let mut fwd = 0;
            for b in steps_part.trim().as_bytes() {
                if (b'0'..=b'9').contains(b) {
                    fwd = 10 * fwd + (b - b'0') as usize;
                } else {
                    if fwd != 0 {
                        steps.push(Step::Fwd(fwd));
                        fwd = 0;
                    }
                    if *b == b'L' {
                        steps.push(Step::Left);
                    } else if *b == b'R' {
                        steps.push(Step::Right);
                    } else {
                        panic!("Unexpected {} in {steps_part}", *b as char);
                    }
                }
            }
            if fwd != 0 {
                steps.push(Step::Fwd(fwd));
            }

            Self { grid, width, steps }
        }
    }
}
Star 1

I used an iterator to move forward. Looks much nicer that for loops ;)

pub fn star_1(data: &PuzzleData) -> usize {
    let mut col = data.grid.iter().position(|&b| b == b'.').unwrap();
    let mut row = 0;
    let mut d: usize = 0;
    let height = data.grid.len() / data.width;

    for step in &data.steps {
        match step {
            Step::Fwd(fwd) => {
                let (d_col, d_row, n) = match d {
                    0 => (1, 0, data.width),
                    1 => (0, 1, height),
                    2 => (data.width - 1, 0, data.width),
                    3 => (0, height - 1, height),
                    _ => unreachable!(),
                };
                (col, row) = (0..n)
                    .cycle()
                    .map(|s| ((col + s * d_col) % data.width, (row + s * d_row) % height))
                    .filter(|(col, row)| data.grid[col + data.width * row] != b' ')
                    .take(fwd + 1)
                    .take_while(|(col, row)| data.grid[col + row * data.width] != b'#')
                    .last()
                    .unwrap();
            }
            Step::Left => d = (d + 3) % 4,
            Step::Right => d = (d + 1) % 4,
        }
    }

    (row + 1) * 1000 + (col + 1) * 4 + d
}
Star 2

I did not see the cubes come when working on part 1.

This turned out to be much more complicated than what I first thought.

There is quite a bit of repetition in my code which multiplies the opportunities for mistakes. I used a lot of the opportunities and made a lot of them. And I had no efficient way for debugging. Which branch of the code is actually used, depends a lot on the cube layout. Since this is different for the example and the real data, the example was not good enough to fix all the issues.

pub const EAST: usize = 0; // [1 0 0]
pub const NORTH: usize = 1; // [0 1 0]
pub const UP: usize = 2; // [0 0 1]
pub const WEST: usize = 3; // [-1 0 0]
pub const SOUTH: usize = 4; // [0 -1 0]
pub const DOWN: usize = 5; // [0 0 -1]

pub const HEAD_RIGHT: usize = 0;
pub const HEAD_DOWN: usize = 1;
pub const HEAD_LEFT: usize = 2;
pub const HEAD_UP: usize = 3;

pub fn star_2(data: &PuzzleData, face_width: usize) -> usize {
    let layout_w = data.width / face_width;
    let layout_h = (data.grid.len() / data.width) / face_width;
    assert_eq!(
        layout_w * face_width * layout_h * face_width,
        data.grid.len(),
        "Grid len does not match layout"
    );

    // get the cube layout (each face is represented by a '#')
    let cube_layout = (0..data.grid.len() / (face_width * face_width))
        .map(|p| (p % layout_w, p / layout_w))
        .map(|(c, r)| {
            if data.grid[c * face_width + r * face_width * data.width] == b' ' {
                ' '
            } else {
                '#'
            }
        })
        .collect::<Vec<_>>();

    // compile a map of cube faces with the normal direction as key and
    // ((col, row), (n_dir, x_dir, y_dir)) as value where (col, row) is the
    // position in the grid and (n_dir, x_dir, y_dir) is normal direction
    // and direction of x- and y- respectively
    let pos: usize = cube_layout.iter().position(|&face| face == '#').unwrap();
    let start = ((pos % layout_w, pos / layout_w), (UP, EAST, SOUTH));
    let mut faces = HashMap::from([(UP, start)]);
    let mut queue = VecDeque::from([start]);
    while let Some(((col, row), (n_dir, x_dir, y_dir))) = queue.pop_front() {
        if col > 0 && cube_layout[col - 1 + row * layout_w] == '#' {
            let next = ((col - 1, row), ((x_dir + 3) % 6, n_dir, y_dir));
            if !faces.contains_key(&next.1 .0) {
                faces.insert(next.1 .0, next);
                queue.push_back(next);
            }
        }
        if col < layout_w - 1 && cube_layout[col + 1 + row * layout_w] == '#' {
            let next = ((col + 1, row), (x_dir, (n_dir + 3) % 6, y_dir));
            if !faces.contains_key(&next.1 .0) {
                faces.insert(next.1 .0, next);
                queue.push_back(next);
            }
        }
        if row > 0 && cube_layout[col + (row - 1) * layout_w] == '#' {
            let next = ((col, row - 1), ((y_dir + 3) % 6, x_dir, n_dir));
            if !faces.contains_key(&next.1 .0) {
                faces.insert(next.1 .0, next);
                queue.push_back(next);
            }
        }
        if row < layout_h - 1 && cube_layout[col + (row + 1) * layout_w] == '#' {
            let next = ((col, row + 1), (y_dir, x_dir, (n_dir + 3) % 6));
            if !faces.contains_key(&next.1 .0) {
                faces.insert(next.1 .0, next);
                queue.push_back(next);
            }
        }
    }

    // current position is given by (x, y) within face and (col, row) of face
    // in addition normal direction and x-/y-direction of current face and
    // the current orientation `d`
    let pos = data.grid.iter().position(|&b| b == b'.').unwrap();
    let mut x = pos % face_width;
    let mut y = 0;
    let mut col = pos / face_width;
    let mut row = 0;
    let (_, (mut n_dir, mut x_dir, mut y_dir)) = faces.get(&UP).unwrap();
    let mut d: usize = 0;

    #[cfg(feature = "print-path")]
    let mut path: HashMap<usize, usize> = HashMap::from([(pos, d)]);

    for step in &data.steps {
        match step {
            Step::Fwd(fwd) => {
                for _ in 0..*fwd {
                    // determine x and y updates (wrapping)
                    // and normal direction of next face (if wrapping)
                    let (x_upd, y_upd, n_dir_upd) = match d {
                        HEAD_RIGHT => (
                            (x + 1) % face_width,
                            y,
                            if x < face_width - 1 { n_dir } else { x_dir },
                        ),
                        HEAD_DOWN => (
                            x,
                            (y + 1) % face_width,
                            if y < face_width - 1 { n_dir } else { y_dir },
                        ),
                        HEAD_LEFT => (
                            (x + face_width - 1) % face_width,
                            y,
                            if x > 0 { n_dir } else { (x_dir + 3) % 6 },
                        ),
                        HEAD_UP => (
                            x,
                            (y + face_width - 1) % face_width,
                            if y > 0 { n_dir } else { (y_dir + 3) % 6 },
                        ),
                        _ => unreachable!(),
                    };

                    let (x_upd, y_upd, d_upd, col_upd, row_upd, x_dir_upd, y_dir_upd) = if n_dir_upd
                        != n_dir
                    {
                        // calculate update on a new face
                        // get face
                        let ((col_upd, row_upd), (n_dir_upd, x_dir_upd, y_dir_upd)) =
                            *faces.get(&n_dir_upd).unwrap();

                        // do the transformation
                        let (x_upd, y_upd, d_upd) = if n_dir_upd == x_dir {
                            // move right
                            if y_dir_upd == y_dir && x_dir_upd == (n_dir + 3) % 6 {
                                (0, y, HEAD_RIGHT)
                            } else if y_dir_upd == (y_dir + 3) % 6 && x_dir_upd == n_dir {
                                (face_width - 1, face_width - 1 - y, HEAD_LEFT)
                            } else if x_dir_upd == y_dir && y_dir_upd == n_dir {
                                (y, face_width - 1, HEAD_UP)
                            } else if x_dir_upd == (y_dir + 3) % 6 && y_dir_upd == (n_dir + 3) % 6 {
                                (face_width - 1 - y, 0, HEAD_DOWN)
                            } else {
                                unreachable!(
                                    "right: {}, _{}_, {} --> _{}_, {}, {}",
                                    n_dir, x_dir, y_dir, n_dir_upd, x_dir_upd, y_dir_upd
                                )
                            }
                        } else if n_dir_upd == (x_dir + 3) % 6 {
                            // move left
                            if y_dir_upd == y_dir && x_dir_upd == n_dir {
                                (face_width - 1, y, HEAD_LEFT)
                            } else if y_dir_upd == (y_dir + 3) % 6 && x_dir_upd == (n_dir + 3) % 6 {
                                (0, face_width - 1 - y, HEAD_RIGHT)
                            } else if x_dir_upd == y_dir && y_dir_upd == (n_dir + 3) % 6 {
                                (y, 0, HEAD_DOWN)
                            } else if x_dir_upd == (y_dir + 3) % 6 && y_dir_upd == n_dir {
                                (face_width - 1 - y, face_width - 1, HEAD_UP)
                            } else {
                                unreachable!(
                                    "left: {}, _{}_, {} --> _{}_, {}, {}",
                                    n_dir, x_dir, y_dir, n_dir_upd, x_dir_upd, y_dir_upd
                                )
                            }
                        } else if n_dir_upd == y_dir {
                            // move down
                            if x_dir_upd == x_dir && y_dir_upd == (n_dir + 3) % 6 {
                                (x, 0, HEAD_DOWN)
                            } else if x_dir_upd == (x_dir + 3) % 6 && y_dir_upd == n_dir {
                                (face_width - 1 - x, face_width - 1, HEAD_UP)
                            } else if y_dir_upd == x_dir && x_dir_upd == n_dir {
                                (face_width - 1, x, HEAD_LEFT)
                            } else if y_dir_upd == (x_dir + 3) % 6 && x_dir_upd == (n_dir + 3) % 6 {
                                (0, face_width - 1 - x, HEAD_RIGHT)
                            } else {
                                unreachable!(
                                    "down: {}, {}, _{}_ --> _{}_, {}, {}",
                                    n_dir, x_dir, y_dir, n_dir_upd, x_dir_upd, y_dir_upd
                                )
                            }
                        } else if n_dir_upd == (y_dir + 3) % 6 {
                            // move up
                            if x_dir_upd == x_dir && y_dir_upd == n_dir {
                                (x, face_width - 1, HEAD_UP)
                            } else if x_dir_upd == (x_dir + 3) % 6 && y_dir_upd == (n_dir + 3) % 6 {
                                (face_width - 1 - x, 0, HEAD_DOWN)
                            } else if y_dir_upd == x_dir && x_dir_upd == (n_dir + 3) % 6 {
                                (0, x, HEAD_RIGHT)
                            } else if y_dir_upd == (x_dir + 3) % 6 && x_dir_upd == n_dir {
                                (face_width - 1, face_width - 1 - x, HEAD_LEFT)
                            } else {
                                unreachable!(
                                    "up: {}, {}, _{}_ --> _{}_, {}, {}",
                                    n_dir, x_dir, y_dir, n_dir_upd, x_dir_upd, y_dir_upd
                                )
                            }
                        } else {
                            unreachable!()
                        };

                        (x_upd, y_upd, d_upd, col_upd, row_upd, x_dir_upd, y_dir_upd)
                    } else {
                        // update within a face
                        (x_upd, y_upd, d, col, row, x_dir, y_dir)
                    };

                    if data.grid
                        [x_upd + col_upd * face_width + (y_upd + row_upd * face_width) * data.width]
                        == b'#'
                    {
                        // wall, can't move any further
                        break;
                    }

                    // update
                    x = x_upd;
                    y = y_upd;
                    d = d_upd;
                    col = col_upd;
                    row = row_upd;
                    n_dir = n_dir_upd;
                    x_dir = x_dir_upd;
                    y_dir = y_dir_upd;

                    #[cfg(feature = "print-path")]
                    path.insert(
                        x + col * face_width + (y + row * face_width) * data.width,
                        d,
                    );
                }
            }
            Step::Left => d = (d + 3) % 4,
            Step::Right => d = (d + 1) % 4,
        }

        #[cfg(feature = "print-path")]
        path.insert(
            x + col * face_width + (y + row * face_width) * data.width,
            d,
        );
    }

    #[cfg(feature = "print-path")]
    for y in 0..data.grid.len() / data.width {
        for x in 0..data.width {
            let p = x + y * data.width;
            match (path.get(&p), data.grid[p]) {
                (Some(0), b'.') => {
                    print!(">");
                }
                (Some(1), b'.') => {
                    print!("v");
                }
                (Some(2), b'.') => {
                    print!("<");
                }
                (Some(3), b'.') => {
                    print!("^");
                }
                (None, b'#') => {
                    print!("#");
                }
                (None, b'.') => {
                    print!(".");
                }
                (None, b' ') => {
                    print!(" ");
                }
                (a, b) => panic!("Unexpected {a:?}, {}", b as char),
            }
        }
        println!();
    }

    (y + row * face_width + 1) * 1000 + (x + col * face_width + 1) * 4 + d
}
Tests

Tests were very necessary today to come up with a solution. I created several additional simple test cases.

#[cfg(test)]
mod tests {
    use super::*;

    const CONTENT: &str = r#"        ...#
        .#..
        #...
        ....
...#.......#
........#...
..#....#....
..........#.
        ...#....
        .....#..
        .#......
        ......#.

10R5L5R10L4R5L5
"#;

    #[test]
    pub fn test_from() {
        let data = PuzzleData::from(CONTENT);
        println!("{data:?}");
    }

    #[test]
    pub fn test_star_1() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(6_032, star_1(&data));
    }

    #[test]
    pub fn test_star_2() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(5_031, star_2(&data, 4));

        let data = PuzzleData::from(include_str!("../tests_1.txt"));
        assert_eq!(1_033, star_2(&data, 5));

        let data = PuzzleData::from(include_str!("../tests_2.txt"));
        assert_eq!(1_033, star_2(&data, 5));

        let data = PuzzleData::from(include_str!("../tests_3.txt"));
        assert_eq!(12_013, star_2(&data, 5));

        let data = PuzzleData::from(include_str!("../tests_4.txt"));
        assert_eq!(3_028, star_2(&data, 5));
    }
}

Day 23: rust

Day 23: Unstable Diffusion

Rust solution to AoC|2022|23.

Elves extend in a space, open in all directions.

So the usual question: shall I use hash sets / hash maps or a more efficient grid based solution?

I decided to go with sets and maps first and I was not happy with that choice. It solved part 1 in about 20ms and part 2 in a bit more than 1s.

In a second attempt, I implemented a grid based solution which solves part 1 in less than 2ms and part 2 in about 80ms. There is certainly potential to do even better…​

Input

The input is directly parsed into a grid.

The most important part of the implementation is the increase_to_gap function which is used to grow the grid to make space for the elves moving.

mod grid {
    #[derive(Clone)]
    pub struct Grid<T>
    where
        T: Clone + PartialEq,
    {
        data: Vec<T>,
        width: usize,
        default: T,
        bbox: (usize, usize, usize, usize),
    }

    impl From<&str> for Grid<u16> {
        fn from(value: &str) -> Self {
            let width = value.as_bytes().iter().position(|&b| b == b'\n').unwrap();
            let data: Vec<u16> = value
                .as_bytes()
                .iter()
                .filter(|&&b| b != b'\n')
                .map(|&b| (b == b'#') as u16)
                .collect();
            assert!(
                (data.len() / width) * width == data.len(),
                "All lines shall have equal length {width}"
            );
            let bbox = Self::get_bbox(&data, width, 0);
            let default = 0;
            Self {
                data,
                width,
                bbox,
                default,
            }
        }
    }

    impl std::fmt::Display for Grid<u16> {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            let (col_min, row_min, col_max, row_max) = self.bbox;
            for row in row_min..row_max {
                for col in col_min..col_max {
                    if self.data[col + row * self.width] & 255 > 0 {
                        '#'.fmt(f)?;
                    } else {
                        '.'.fmt(f)?;
                    }
                }
                '\n'.fmt(f)?;
            }
            Ok(())
        }
    }

    impl<T> Grid<T>
    where
        T: Copy + PartialEq,
    {
        fn get_bbox(data: &[T], width: usize, default: T) -> (usize, usize, usize, usize) {
            data.iter()
                .enumerate()
                .filter(|&(_, e)| e != &default)
                .map(|(pos, _)| (pos % width, pos / width))
                .fold((usize::MAX, usize::MAX, 0, 0), Self::add_to_bbox)
        }

        fn add_to_bbox(
            (col_min, row_min, col_max, row_max): (usize, usize, usize, usize),
            (col, row): (usize, usize),
        ) -> (usize, usize, usize, usize) {
            (
                col_min.min(col),
                row_min.min(row),
                col_max.max(col + 1),
                row_max.max(row + 1),
            )
        }

        /// make sure there is at least `gap` space between bounding
        /// box and end of grid
        pub fn increase_to_gap(
            &mut self,
            gap_min: usize,
            gap_max: usize,
        ) -> (usize, usize, usize, usize) {
            let (col_min, row_min, col_max, row_max) = self.bbox;
            let height = self.data.len() / self.width;

            if col_min < gap_min
                || row_min < gap_min
                || self.width - col_max < gap_min
                || height - row_max < gap_min
            {
                let (gap_c_l, gap_r_t, gap_c_r, gap_r_b) = (
                    gap_max - col_min.min(gap_max),
                    gap_max - row_min.min(gap_max),
                    gap_max - (self.width - col_max).min(gap_max),
                    gap_max - (height - row_max).min(gap_max),
                );

                let mut data = vec![
                    self.default;
                    (self.width + gap_c_l + gap_c_r) * (height + gap_r_t + gap_r_b)
                ];
                for row in 0..height {
                    let off = (row + gap_r_t) * (self.width + gap_c_l + gap_c_r) + gap_c_l;
                    data[off..off + self.width]
                        .clone_from_slice(&self.data[row * self.width..(row + 1) * self.width]);
                }

                self.data = data;
                self.width += gap_c_l + gap_c_r;
                self.bbox = (
                    self.bbox.0 + gap_c_l,
                    self.bbox.1 + gap_r_t,
                    self.bbox.2 + gap_c_l,
                    self.bbox.3 + gap_r_t,
                );

                (gap_c_l, gap_r_t, gap_c_r, gap_r_b)
            } else {
                (0, 0, 0, 0)
            }
        }

        /// set value and increase bbox if necessary
        pub fn set(&mut self, (col, row): (usize, usize), value: T) {
            self.data[col + row * self.width] = value;
            if value != self.default {
                self.bbox = Self::add_to_bbox(self.bbox, (col, row));
            }
        }

        pub fn get(&self, (col, row): (usize, usize)) -> T {
            self.data[col + row * self.width]
        }

        /// get current bbox
        pub fn bbox(&self) -> (usize, usize, usize, usize) {
            self.bbox
        }

        pub fn list<F>(&self, predicate: F) -> Vec<(usize, usize)>
        where
            F: Fn(&T) -> bool,
        {
            self.data
                .iter()
                .enumerate()
                .filter(|(_, v)| predicate(v))
                .map(|(pos, _)| (pos % self.width, pos / self.width))
                .collect()
        }

        pub fn width(&self) -> usize {
            self.width
        }

        pub fn len(&self) -> usize {
            self.data.len()
        }
    }
}
Star 1

The interesting part is in the simulate_round function.

In each round, I first iterate over all elves and identify potential targets which are stored in the targets vec. Then I iterate over the targets and move the elves if applicable.

/// helper function to get adjacent in given direction
fn adjacent((col, row): (usize, usize), dir: usize) -> (usize, usize) {
    let (d_col, d_row) = DELTAS[dir];
    (
        col.wrapping_add_signed(d_col),
        row.wrapping_add_signed(d_row),
    )
}

pub fn simulate_round(
    elves: &mut Vec<(usize, usize)>,
    grid: &mut Grid<u16>,
    targets: &mut Vec<(usize, (usize, usize))>,
    search_start: usize,
) -> bool {
    // make sure grid has enough space, require at least a gap of 1
    // in every direction, increase to gap of 10 if not satisfied
    let (col_off, row_off, _, _) = grid.increase_to_gap(1, 10);

    // clear buffer keeping capacity
    targets.truncate(0);

    // loop over elves
    for idx in 0..elves.len() {
        // apply offset
        elves[idx].0 += col_off;
        elves[idx].1 += row_off;

        // get coordinates
        let (col, row) = elves[idx];

        // if elf has no neighbors, don't move
        if (0..DELTAS.len()).all(|dir| grid.get(adjacent((col, row), dir)) & 255 == 0) {
            continue;
        }

        // find a direction to move to, starting from given search_start
        if let Some((tar_col, tar_row)) = (0..DIRS.len())
            .map(|pos| DIRS[(pos + search_start) % DIRS.len()])
            .find(|&dir| {
                [
                    (dir + DELTAS.len() - 1) % DELTAS.len(),
                    dir,
                    (dir + 1) % DELTAS.len(),
                ]
                .iter()
                .all(|&dir| grid.get(adjacent((col, row), dir)) & 255 == 0)
            })
            .map(|dir| adjacent((col, row), dir))
        {
            // increase 2nd byte by one (counting elves with the same target)
            let tar = grid.get((tar_col, tar_row)) + (1 << 8);
            grid.set((tar_col, tar_row), tar);

            // if first to have this target, push to targets
            if tar >> 8 == 1 {
                targets.push((idx, (tar_col, tar_row)));
            }
        }
    }

    // there are no targets -> no elves move anymore
    if targets.is_empty() {
        return false;
    }

    // update elves to targets where applicable
    for &(idx, (col_tar, row_tar)) in targets.iter() {
        if grid.get((col_tar, row_tar)) >> 8 == 1 {
            // can move -> move
            grid.set(elves[idx], 0);
            grid.set((col_tar, row_tar), 1);
            elves[idx] = (col_tar, row_tar);
        } else {
            // cannot move -> reset target count
            grid.set((col_tar, row_tar), 0);
        }
    }

    // some elves moved
    true
}

pub fn simulate(s: &str, rounds: usize) -> (usize, Vec<(usize, usize)>) {
    // build grid
    let mut grid = Grid::from(s);
    // build list of elves
    let mut elves = grid.list(|&v| v & 255 > 0);
    // buffer to store targets
    let mut targets = Vec::with_capacity(elves.len());

    // loop over rounds
    let mut round = 0;
    while round < rounds {
        if !simulate_round(&mut elves, &mut grid, &mut targets, round % 4) {
            // stop if no more elves move
            break;
        }
        round += 1;
    }

    (round + 1, elves)
}

pub const DELTAS: [(isize, isize); 8] = [
    (1, 1),   // SOUTH EAST
    (0, 1),   // SOUTH
    (-1, 1),  // SOUTH WEST
    (-1, 0),  // WEST
    (-1, -1), // NORTH WEST
    (0, -1),  // NORTH
    (1, -1),  // NORTH EAST
    (1, 0),   // EAST
];

/// NORTH, SOUTH, WEST, EAST
pub const DIRS: [usize; 4] = [5, 1, 3, 7];

pub fn star_1(data: &str) -> usize {
    let (_, elves) = simulate(data, 10);
    let (col_min, row_min, col_max, row_max) = elves.iter().fold(
        (usize::MAX, usize::MAX, 0, 0),
        |(col_min, row_min, col_max, row_max), &(col, row)| {
            (
                col_min.min(col),
                row_min.min(row),
                col_max.max(col + 1),
                row_max.max(row + 1),
            )
        },
    );

    (col_max - col_min) * (row_max - row_min) - elves.len()
}
Star 2

Just simulate rounds until nothing more happens.

pub fn star_2(data: &str) -> usize {
    simulate(data, usize::MAX).0
}
Tests

Kind of visual tests today for the simple example. Just compared my output to the one on the website manually.

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    pub fn test_star_1() {
        assert_eq!(110, star_1(CONTENT));
    }

    #[test]
    pub fn test_star_2() {
        assert_eq!(20, star_2(CONTENT));
    }

    #[test]
    pub fn test_increase_to_gap() {
        let mut grid = Grid::from("#....\n.#...\n..#..");
        let str_a = grid.to_string();

        // check initial conditions
        assert_eq!((0, 0, 3, 3), grid.bbox(), "incorrect bbox");
        assert_eq!(5, grid.width(), "unexpected width");
        assert_eq!(5 * 3, grid.len(), "unexpected length");

        // perform and verify increase
        assert_eq!(
            (2, 2, 0, 2),
            grid.increase_to_gap(1, 2),
            "unexpected amount of increase"
        );
        assert_eq!(
            (0, 0, 0, 0),
            grid.increase_to_gap(2, 10),
            "unexpected increase beyond min"
        );
        assert_eq!((2, 2, 5, 5), grid.bbox(), "unexpected bbox after increase");
        assert_eq!(7, grid.width(), "unexpected width after increase");
        assert_eq!(7 * 7, grid.len(), "unexpected length after increase");
        assert_eq!(
            str_a,
            grid.to_string(),
            "string representation changed after increase"
        );
    }

    #[test]
    pub fn test_simulate_round() {
        let mut grid = Grid::from(CONTENT_SMALL);
        let mut elves = grid.list(|v| v & 255 > 0);
        let mut targets = Vec::with_capacity(elves.len());

        println!("{grid}");
        simulate_round(&mut elves, &mut grid, &mut targets, 0);
        println!("{grid}");
        simulate_round(&mut elves, &mut grid, &mut targets, 1);
        println!("{grid}");
        simulate_round(&mut elves, &mut grid, &mut targets, 2);
        println!("{grid}");
    }

    const CONTENT: &str = r#"....#..
..###.#
#...#.#
.#...##
#.###..
##.#.##
.#..#..
"#;

    const CONTENT_SMALL: &str = r#".....
..##.
..#..
.....
..##.
.....
"#;
}

Day 24: rust

Day 24: Blizzard Basin

Rust solution to AoC|2022|24.

Another day of path finding.

Input

I store blizzards separately by direction in a grid (stored as flat vec) which will never change over time. For each direction, there is an row / column offset (modulo width / hight) added depending on when we look at them later on.

The blizzards just contain the inner portion of the basin for easier wrapping around.

In addition, I store the position of the entry and the exit on the first and last row.

pub mod input {
    #[derive(Debug)]
    pub struct PuzzleData {
        pub blizzards_r: Vec<bool>,
        pub blizzards_u: Vec<bool>,
        pub blizzards_l: Vec<bool>,
        pub blizzards_d: Vec<bool>,
        pub width: usize,
        pub height: usize,
        pub entry: usize,
        pub exit: usize,
    }

    impl From<&str> for PuzzleData {
        /// parse the puzzle input
        fn from(s: &str) -> Self {
            let b = s.as_bytes();

            let width = b.iter().position(|&b| b == b'\n').unwrap() - 2;
            let entry = b.iter().position(|&b| b == b'.').unwrap() - 1;
            let exit = width
                - b.iter()
                    .rev()
                    .filter(|&&b| b != b'\n')
                    .position(|&b| b == b'.')
                    .unwrap();

            let blizzards_r: Vec<bool> = b
                .iter()
                .skip(width + 3)
                .take(b.len() - 2 * width - 6)
                .filter(|&&b| b != b'\n' && b != b'#')
                .map(|&b| b == b'>')
                .collect();
            let blizzards_l = b
                .iter()
                .skip(width + 3)
                .take(b.len() - 2 * width - 6)
                .filter(|&&b| b != b'\n' && b != b'#')
                .map(|&b| b == b'<')
                .collect();
            let blizzards_u = b
                .iter()
                .skip(width + 3)
                .take(b.len() - 2 * width - 6)
                .filter(|&&b| b != b'\n' && b != b'#')
                .map(|&b| b == b'^')
                .collect();
            let blizzards_d = b
                .iter()
                .skip(width + 3)
                .take(b.len() - 2 * width - 6)
                .filter(|&&b| b != b'\n' && b != b'#')
                .map(|&b| b == b'v')
                .collect();

            let height = blizzards_r.len() / width;

            Self {
                blizzards_r,
                blizzards_l,
                blizzards_u,
                blizzards_d,
                width,
                height,
                entry,
                exit,
            }
        }
    }
}
Star 1

I implemented a function is_blizzard which checks whether there is a blizzard at a given position and a given time.

With this, I implement a A* style search (since the graph is unweighted, nodes are immediately settled once reached and no decrease key or similar is required)

impl PuzzleData {
    pub fn is_blizzard_r(&self, (col, row): (usize, usize), time: usize) -> bool {
        self.blizzards_r[(col + self.width - time % self.width) % self.width + self.width * row]
    }

    pub fn is_blizzard_u(&self, (col, row): (usize, usize), time: usize) -> bool {
        self.blizzards_u[col + self.width * ((row + time) % self.height)]
    }

    pub fn is_blizzard_l(&self, (col, row): (usize, usize), time: usize) -> bool {
        self.blizzards_l[(col + time) % self.width + self.width * row]
    }

    pub fn is_blizzard_d(&self, (col, row): (usize, usize), time: usize) -> bool {
        self.blizzards_d
            [col + self.width * ((row + self.height - time % self.height) % self.height)]
    }

    pub fn is_blizzard(&self, pos: (usize, usize), time: usize) -> bool {
        self.is_blizzard_r(pos, time)
            || self.is_blizzard_u(pos, time)
            || self.is_blizzard_l(pos, time)
            || self.is_blizzard_d(pos, time)
    }

    pub fn print(&self, col: usize, row: usize, time: usize) {
        // first row
        print!("#");
        for col_ in 0..self.width {
            if col_ != self.entry {
                print!("#");
            } else if col_ == col && row == 0 {
                print!("E");
            } else {
                print!(".");
            }
        }
        println!("#");

        for row_ in 0..self.height {
            print!("#");
            for col_ in 0..self.width {
                let r = self.is_blizzard_r((col_, row_), time) as u8;
                let u = self.is_blizzard_u((col_, row_), time) as u8;
                let l = self.is_blizzard_l((col_, row_), time) as u8;
                let d = self.is_blizzard_d((col_, row_), time) as u8;
                if (r + u + l + d) > 1 {
                    print!("{}", r + u + l + d);
                } else if r > 0 {
                    print!(">");
                } else if u > 0 {
                    print!("^");
                } else if l > 0 {
                    print!("<");
                } else if d > 0 {
                    print!("v");
                } else if row_ + 1 == row && col_ == col {
                    print!("E");
                } else {
                    print!(".");
                }
            }
            println!("#");
        }

        // last row
        print!("#");
        for col_ in 0..self.width {
            if col_ != self.exit {
                print!("#");
            } else if col_ == col && row == self.height + 1 {
                print!("E");
            } else {
                print!(".");
            }
        }
        println!("#");
    }

    pub fn shortest_path(
        &self,
        start: (usize, usize),
        target: (usize, usize),
        start_time: usize,
    ) -> usize {
        let w = self.width;
        let h = self.height;

        let mut queue = BinaryHeap::new();

        // items on the queue are
        // - lower bound of time to target (Manhattan distance)
        // - time used so far
        // - current position (row, col)
        queue.push((
            !(self.entry.max(self.exit) - self.entry.min(self.exit) + h + 1),
            !start_time,
            start,
        ));

        // do not visit nodes twice
        let mut seen = HashSet::from([(start_time % (w * h), start)]);

        while let Some((_n_bound, n_time, (col, row))) = queue.pop() {
            let time = !n_time;
            if (col, row) == target {
                return time;
            }

            let time_upd = time + 1;

            let mut enqueue = |(c, r), t| {
                if (r == 0 || r == h + 1 || !self.is_blizzard((c, r - 1), t))
                    && seen.insert((t % (w * h), (c, r)))
                {
                    let bound =
                        target.0.max(c) - target.0.min(c) + target.1.max(r) - target.1.min(r) + t;
                    queue.push((!bound, !t, (c, r)))
                }
            };

            // move to entry
            if row == 1 && col == self.entry {
                enqueue((self.entry, 0), time_upd);
            }
            // move to exit
            if row == h && col == self.exit {
                enqueue((self.exit, h + 1), time_upd);
            }
            // don't move at entry or exit or any position where there is no blizzard
            enqueue((col, row), time_upd);
            // move left (not in entry / exit rows)
            if row != 0 && row != h + 1 && col > 0 {
                enqueue((col - 1, row), time_upd);
            }
            // move up (not in entry row and row immediately below)
            if row > 1 {
                enqueue((col, row - 1), time_upd);
            }
            // move right (not in entry / exit rows)
            if row != 0 && row != h + 1 && col < w - 1 {
                enqueue((col + 1, row), time_upd);
            }
            // move down (not in exit row and row immediately above)
            if row < h {
                enqueue((col, row + 1), time_upd);
            }
        }

        panic!(
            "I've seen {} nodes but did not find a path from {start:?} at t = {start_time} to {target:?}",
            seen.len()
        );
    }
}

pub fn star_1(data: &PuzzleData) -> usize {
    data.shortest_path((data.entry, 0), (data.exit, data.height + 1), 0)
}
Star 2

My initial implementation was not quite generic (I never went back to the entry and actually stopped the search one step before the exit). Making it bi-directional was a simple extension.

pub fn star_2(data: &PuzzleData) -> usize {
    let start = (data.entry, 0);
    let target = (data.exit, data.height + 1);
    let m1 = data.shortest_path(start, target, 0);
    let m2 = data.shortest_path(target, start, m1);
    data.shortest_path(start, target, m2)
}
Tests
#[cfg(test)]
mod tests {
    use super::*;

    const CONTENT: &str = r#"#.######
#>>.<^<#
#.<..<<#
#>v.><>#
#<^v^^>#
######.#
"#;

    const CONTENT_SIMPLE: &str = r#"#.#####
#.....#
#>....#
#.....#
#...v.#
#.....#
#####.#
"#;

    #[test]
    pub fn test_from() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(24, data.blizzards_r.len());
        assert_eq!(24, data.blizzards_u.len());
        assert_eq!(24, data.blizzards_l.len());
        assert_eq!(24, data.blizzards_d.len());
        assert_eq!(6, data.width);
        assert_eq!(0, data.entry);
        assert_eq!(5, data.exit);
        println!("{data:?}");
    }

    #[test]
    pub fn test_blizzards_move() {
        let data = PuzzleData::from(CONTENT_SIMPLE);

        for time in 0..=12 {
            println!("t = {time}");
            data.print(data.entry, 0, time);
        }
    }

    #[test]
    pub fn test_blizzards_is_blizzard() {
        let data = PuzzleData::from(CONTENT);
        println!(
            "entry: ({}, 0), exit: ({}, {})",
            data.entry,
            data.exit,
            data.height + 1
        );

        let steps_col = [0, 0, 0, 0, 1, 1, 0, -1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0];
        let steps_row = [1, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 1, 0, 0, 0, 1, 1];
        let mut time = 0;
        let mut col: usize = 0;
        let mut row: usize = 0;
        for k in 0..steps_col.len() {
            col = col.wrapping_add_signed(steps_col[k]);
            row = row.wrapping_add_signed(steps_row[k]);
            time += 1;
            println!("t = {time}: ({col}, {row})");
            data.print(col, row, time);
            assert!(row == data.height + 1 || !data.is_blizzard((col, row - 1), time));
        }
    }

    #[test]
    pub fn test_star_1() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(18, star_1(&data));
    }

    #[test]
    pub fn test_star_2() {
        let data = PuzzleData::from(CONTENT);
        assert_eq!(54, star_2(&data));
    }
}

Day 25: rust

Day 25: Full of Hot Air

Rust solution to AoC|2022|25.

A nice, simple last one for Christmas!

Star 1

There is not much to say. Here is the solution:

pub fn star_1(data: &&str) -> String {
    // convert SNAFU to decimal and sum
    let mut sum: isize = data
        .lines()
        .map(|l| {
            l.as_bytes().iter().fold(0, |value, &digit| {
                5 * value
                    + match digit {
                        b'2' => 2,
                        b'1' => 1,
                        b'0' => 0,
                        b'-' => -1,
                        b'=' => -2,
                        _ => unreachable!(),
                    }
            })
        })
        .sum();
    println!("The sum is {sum}");

    // convert decimal to SNAFU
    let mut digits = Vec::new();
    while sum != 0 {
        let v = sum % 5;
        sum /= 5;
        if v > 2 {
            sum += 1;
        }
        digits.push(match v {
            3 => '=',
            4 => '-',
            0 => '0',
            1 => '1',
            2 => '2',
            _ => unreachable!(),
        });
    }
    digits.iter().rev().collect()
}
Tests

And the tests:

#[cfg(test)]
mod tests {
    use super::*;

    const CONTENT: &str = r#"1=-0-2
12111
2=0=
21
2=01
111
20012
112
1=-1=
1-12
12
1=
122
"#;

    #[test]
    pub fn test_star_1() {
        assert_eq!("2=-1=0", star_1(&CONTENT));
    }
}