Day 20: Race Condition

Megathread guidelines

  • Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
  • You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL

FAQ

  • cabhan
    link
    fedilink
    English
    arrow-up
    3
    ·
    edit-2
    13 hours ago

    Rust

    I was stuck for a while, even after getting a few hints, until I read the problem more closely and realized: there is only one non-cheating path, and every free space is on it. This means that the target of any shortcut is guaranteed to be on the shortest path to the end.

    This made things relatively simple. I used Dijkstra to calculate the distance from the start to each space. I then looked at every pair of points: if they are a valid distance away from each other, check how much time I would save jumping from one to the next. If that amount of time is in the range we want, then this is a valid cheat.

    https://gitlab.com/bricka/advent-of-code-2024-rust/-/blob/main/src/days/day20.rs?ref_type=heads

    The Code
    // Critical point to note: EVERY free space is on the shortest path.
    
    use itertools::Itertools;
    
    use crate::search::dijkstra;
    use crate::solver::DaySolver;
    use crate::grid::{Coordinate, Grid};
    
    type MyGrid = Grid<MazeElement>;
    
    enum MazeElement {
        Wall,
        Free,
        Start,
        End,
    }
    
    impl MazeElement {
        fn is_free(&self) -> bool {
            !matches!(self, MazeElement::Wall)
        }
    }
    
    fn parse_input(input: String) -> (MyGrid, Coordinate) {
        let grid: MyGrid = input.lines()
            .map(|line| line.chars().map(|c| match c {
                '#' => MazeElement::Wall,
                '.' => MazeElement::Free,
                'S' => MazeElement::Start,
                'E' => MazeElement::End,
                _ => panic!("Invalid maze element: {}", c)
            })
                 .collect())
            .collect::<Vec<Vec<MazeElement>>>()
            .into();
    
        let start_pos = grid.iter().find(|(_, me)| matches!(me, MazeElement::Start)).unwrap().0;
    
        (grid, start_pos)
    }
    
    fn solve<R>(grid: &MyGrid, start_pos: Coordinate, min_save_time: usize, in_range: R) -> usize
    where R: Fn(Coordinate, Coordinate) -> bool {
        let (cost_to, _) = dijkstra(
            start_pos,
            |&c| grid.orthogonal_neighbors_iter(c)
                .filter(|&n| grid[n].is_free())
                .map(|n| (n, 1))
                .collect()
        );
    
        cost_to.keys()
            .cartesian_product(cost_to.keys())
            .map(|(&c1, &c2)| (c1, c2))
            // We don't compare with ourself
            .filter(|&(c1, c2)| c1 != c2)
            // The two points need to be within range
            .filter(|&(c1, c2)| in_range(c1, c2))
            // We need to save at least `min_save_time`
            .filter(|(c1, c2)| {
                // Because we are working with `usize`, the subtraction
                // could underflow. So we need to use `checked_sub`
                // instead, and check that a) no underflow happened, and
                // b) that the time saved is at least the minimum.
                cost_to.get(c2).copied()
                    .and_then(|n| n.checked_sub(*cost_to.get(c1).unwrap()))
                    .and_then(|n| n.checked_sub(c1.distance_to(c2)))
                    .map(|n| n >= min_save_time)
                    .unwrap_or(false)
            })
            .count()
    }
    
    pub struct Day20Solver;
    
    impl DaySolver for Day20Solver {
        fn part1(&self, input: String) -> String {
            let (grid, start_pos) = parse_input(input);
            solve(
                &grid,
                start_pos,
                100,
                |c1, c2| c1.distance_to(&c2) == 2,
            ).to_string()
        }
    
        fn part2(&self, input: String) -> String {
            let (grid, start_pos) = parse_input(input);
            solve(
                &grid,
                start_pos,
                100,
                |c1, c2| c1.distance_to(&c2) <= 20,
            ).to_string()
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_part1() {
            let input = include_str!("../../inputs/test/20");
            let (grid, start_pos) = parse_input(input.to_string());
            let actual = solve(&grid, start_pos, 1, |c1, c2| c1.distance_to(&c2) == 2);
            assert_eq!(44, actual);
        }
    
        #[test]
        fn test_part2() {
            let input = include_str!("../../inputs/test/20");
            let (grid, start_pos) = parse_input(input.to_string());
            let actual = solve(&grid, start_pos, 50, |c1, c2| c1.distance_to(&c2) <= 20);
            assert_eq!(285, actual);
        }
    }