Same person as @Gobbel2000@feddit.de, different instance.

  • 1 Post
  • 5 Comments
Joined 8 months ago
cake
Cake day: April 3rd, 2024

help-circle
  • Rust

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

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

    Also on github


  • Rust

    Areas are found by flooding, in the meantime whenever the adjacent plot would be outside the region (or out of bounds) the edge (inside plot, outside plot) is saved in a perimeter list. Part 1 takes just the size of that list, in part 2 we remove fence parts and all entries directly next to it on both sides.

    Solution
    use std::collections::{HashSet, VecDeque};
    
    use euclid::{default::*, point2, vec2};
    
    type Fences = HashSet<(Point2D<i32>, Point2D<i32>)>;
    const DIRS: [Vector2D<i32>; 4] = [vec2(0, -1), vec2(1, 0), vec2(0, 1), vec2(-1, 0)];
    
    fn parse(input: &str) -> Vec<&[u8]> {
        input.lines().map(|l| l.as_bytes()).collect()
    }
    
    fn price(field: &[&[u8]], start: (usize, usize), visited: &mut [Vec<bool>]) -> (u32, Fences) {
        let crop = field[start.1][start.0];
        let width = field[0].len();
        let height = field.len();
        let mut area_visited = vec![vec![false; width]; height];
        let mut area = 0;
        let mut fences: Fences = HashSet::new();
    
        area_visited[start.1][start.0] = true;
        visited[start.1][start.0] = true;
        let start = point2(start.0 as i32, start.1 as i32);
        let bounds = Rect::new(Point2D::origin(), Size2D::new(width, height).to_i32());
        let mut frontier = VecDeque::from([start]);
        while let Some(p) = frontier.pop_front() {
            area += 1;
            for dir in DIRS {
                let next = p + dir;
                if bounds.contains(next) {
                    let next_u = next.to_usize();
                    if area_visited[next_u.y][next_u.x] {
                        continue;
                    }
                    if field[next_u.y][next_u.x] == crop {
                        visited[next_u.y][next_u.x] = true;
                        area_visited[next_u.y][next_u.x] = true;
                        frontier.push_back(next);
                        continue;
                    }
                }
                fences.insert((p, next));
            }
        }
        (area, fences)
    }
    
    fn part1(input: String) {
        let field = parse(&input);
        let width = field[0].len();
        let height = field.len();
        let mut visited = vec![vec![false; width]; height];
        let mut total_price = 0;
        for y in 0..height {
            for x in 0..width {
                if !visited[y][x] {
                    let (area, fences) = price(&field, (x, y), &mut visited);
                    total_price += area * fences.len() as u32;
                }
            }
        }
        println!("{total_price}");
    }
    
    fn count_perimeter(mut fences: Fences) -> u32 {
        let list: Vec<_> = fences.iter().copied().collect();
        let mut perimeter = 0;
        for (v, w) in list {
            if fences.contains(&(v, w)) {
                perimeter += 1;
                let dir = w - v;
                let orth = dir.yx();
                let mut next = v + orth;
                while fences.remove(&(next, next + dir)) {
                    next += orth;
                }
                let mut next = v - orth;
                while fences.remove(&(next, next + dir)) {
                    next -= orth;
                }
            }
        }
        perimeter
    }
    
    fn part2(input: String) {
        let field = parse(&input);
        let width = field[0].len();
        let height = field.len();
        let mut visited = vec![vec![false; width]; height];
        let mut total_price = 0;
        for y in 0..height {
            for x in 0..width {
                if !visited[y][x] {
                    let (area, fences) = price(&field, (x, y), &mut visited);
                    total_price += area * count_perimeter(fences);
                }
            }
        }
        println!("{total_price}");
    }
    
    util::aoc_main!();
    

    Also on github


  • Rust

    Part 2 is solved with recursion and a cache, which is indexed by stone numbers and remaining rounds and maps to the previously calculated expansion size. In my case, the cache only grew to 139320 entries, which is quite reasonable given the size of the result.

    Solution
    use std::collections::HashMap;
    
    fn parse(input: String) -> Vec<u64> {
        input
            .split_whitespace()
            .map(|w| w.parse().unwrap())
            .collect()
    }
    
    fn part1(input: String) {
        let mut stones = parse(input);
        for _ in 0..25 {
            let mut new_stones = Vec::with_capacity(stones.len());
            for s in &stones {
                match s {
                    0 => new_stones.push(1),
                    n => {
                        let digits = s.ilog10() + 1;
                        if digits % 2 == 0 {
                            let cutoff = 10u64.pow(digits / 2);
                            new_stones.push(n / cutoff);
                            new_stones.push(n % cutoff);
                        } else {
                            new_stones.push(n * 2024)
                        }
                    }
                }
            }
            stones = new_stones;
        }
        println!("{}", stones.len());
    }
    
    fn expansion(s: u64, rounds: u32, cache: &mut HashMap<(u64, u32), u64>) -> u64 {
        // Recursion anchor
        if rounds == 0 {
            return 1;
        }
        // Calculation is already cached
        if let Some(res) = cache.get(&(s, rounds)) {
            return *res;
        }
    
        // Recurse
        let res = match s {
            0 => expansion(1, rounds - 1, cache),
            n => {
                let digits = s.ilog10() + 1;
                if digits % 2 == 0 {
                    let cutoff = 10u64.pow(digits / 2);
                    expansion(n / cutoff, rounds - 1, cache) +
                    expansion(n % cutoff, rounds - 1, cache)
                } else {
                    expansion(n * 2024, rounds - 1, cache)
                }
            }
        };
        // Save in cache
        cache.insert((s, rounds), res);
        res
    }
    
    fn part2(input: String) {
        let stones = parse(input);
        let mut cache = HashMap::new();
        let sum: u64 = stones.iter().map(|s| expansion(*s, 75, &mut cache)).sum();
        println!("{sum}");
    }
    
    util::aoc_main!();
    

    Also on github


  • Rust

    This was a nice one. Basically 9 rounds of Breadth-First-Search, which could be neatly expressed using fold. The only difference between part 1 and part 2 turned out to be the datastructure for the search frontier: The HashSet in part 1 unifies paths as they join back to the same node, the Vec in part 2 keeps all paths separate.

    Solution
    use std::collections::HashSet;
    
    fn parse(input: &str) -> Vec<&[u8]> {
        input.lines().map(|l| l.as_bytes()).collect()
    }
    
    fn adj(grid: &[&[u8]], (x, y): (usize, usize)) -> Vec<(usize, usize)> {
        let n = grid[y][x];
        let mut adj = Vec::with_capacity(4);
        if x > 0 && grid[y][x - 1] == n + 1 {
            adj.push((x - 1, y))
        }
        if y > 0 && grid[y - 1][x] == n + 1 {
            adj.push((x, y - 1))
        }
        if x + 1 < grid[0].len() && grid[y][x + 1] == n + 1 {
            adj.push((x + 1, y))
        }
        if y + 1 < grid.len() && grid[y + 1][x] == n + 1 {
            adj.push((x, y + 1))
        }
        adj
    }
    
    fn solve(input: String, trailhead: fn(&[&[u8]], (usize, usize)) -> u32) -> u32 {
        let grid = parse(&input);
        let mut sum = 0;
        for (y, row) in grid.iter().enumerate() {
            for (x, p) in row.iter().enumerate() {
                if *p == b'0' {
                    sum += trailhead(&grid, (x, y));
                }
            }
        }
        sum
    }
    
    fn part1(input: String) {
        fn score(grid: &[&[u8]], start: (usize, usize)) -> u32 {
            (1..=9)
                .fold(HashSet::from([start]), |frontier, _| {
                    frontier.iter().flat_map(|p| adj(grid, *p)).collect()
                })
                .len() as u32
        }
        println!("{}", solve(input, score))
    }
    
    fn part2(input: String) {
        fn rating(grid: &[&[u8]], start: (usize, usize)) -> u32 {
            (1..=9)
                .fold(vec![start], |frontier, _| {
                    frontier.iter().flat_map(|p| adj(grid, *p)).collect()
                })
                .len() as u32
        }
        println!("{}", solve(input, rating))
    }
    
    util::aoc_main!();
    

    Also on github


  • Rust

    A bunch of fiddling with indices and sizes. In part 1 the disk is simulated in a Vec, for part 2 files and spaces are represented by their offset and size, collected in separate lists. Then these values are changed as necessary, with a whole bunch of mut. In particular, files are never moved within the list of files, only their offset changes.

    Solution
    fn part1(input: String) {
        let mut id: u64 = 0;
        let mut disk = Vec::new();
        let mut file = true;
        for b in input.trim().bytes() {
            let num: usize = (b - b'0') as usize;
            if file {
                disk.extend(vec![Some(id); num]);
                id += 1;
            } else {
                disk.extend(vec![None; num]);
            }
            file = !file;
        }
    
        let mut first_free = 0;
        while disk[first_free].is_some() {
            first_free += 1
        }
    
        let mut last_file = disk.len() - 1;
        while disk[last_file].is_none() {
            last_file -= 1
        }
    
        while first_free < last_file {
            disk[first_free] = disk[last_file];
            disk[last_file] = None;
            while disk[first_free].is_some() {
                first_free += 1
            }
            while disk[last_file].is_none() {
                last_file -= 1
            }
        }
    
        let checksum = disk
            .iter()
            .filter_map(|e| *e)
            .enumerate()
            .map(|(i, id)| i as u64 * id)
            .sum::<u64>();
        println!("{checksum}");
    }
    
    fn part2(input: String) {
        // Tuples of (idx, size)
        let mut free_spaces = Vec::new();
        // Tuples of (idx, size, id)
        let mut files = Vec::new();
    
        let mut id: u64 = 0;
        let mut disk_len = 0;
        let mut file = true;
        for b in input.trim().bytes() {
            let num = (b - b'0') as u64;
            if file {
                files.push((disk_len, num, id));
                id += 1;
            } else {
                free_spaces.push((disk_len, num));
            }
            disk_len += num;
            file = !file;
        }
    
        for (idx, size, _id) in files.iter_mut().rev() {
            match free_spaces
                .iter_mut()
                // Only search spaces left of file
                .take_while(|(sp_idx, _space)| sp_idx < idx)
                .find(|(_sp_idx, space)| space >= size)
            {
                None => {} // No space found
                Some((sp_idx, space)) => {
                    // Move file into space
                    *idx = *sp_idx;
                    // Reduce free space
                    *sp_idx += *size;
                    *space -= *size;
                }
            }
        }
    
        let sum_range = |n| if n == 0 { 0 } else { (n * (n - 1)) / 2 };
        let checksum = files
            .iter()
            .map(|(idx, size, id)| (sum_range(idx + size) - sum_range(*idx)) * id)
            .sum::<u64>();
        println!("{checksum}");
    }
    
    util::aoc_main!();
    

    Also on github