Day 16 master
authorGreg Burri <greg.burri@gmail.com>
Fri, 30 Jan 2026 17:07:24 +0000 (18:07 +0100)
committerGreg Burri <greg.burri@gmail.com>
Fri, 30 Jan 2026 17:07:24 +0000 (18:07 +0100)
Cargo.toml
advent_of_code_common
src/day16.rs [new file with mode: 0644]
src/days.rs
src/main.rs

index d436b78..2967fde 100644 (file)
@@ -2,7 +2,7 @@
 name = "advent_of_code_2024"
 version = "0.1.0"
 authors = ["Greg Burri <greg.burri@gmail.com>"]
-edition = "2021"
+edition = "2024"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
index b9fa090..cdc5927 160000 (submodule)
@@ -1 +1 @@
-Subproject commit b9fa0908044042af2ca3dd66281b58afd289b4e4
+Subproject commit cdc59273501b20214236ea944a525b0d99007d61
diff --git a/src/day16.rs b/src/day16.rs
new file mode 100644 (file)
index 0000000..57d04ee
--- /dev/null
@@ -0,0 +1,251 @@
+use std::{collections::HashMap, io::BufRead};
+
+use nalgebra::DMatrix;
+
+type Maze = DMatrix<char>;
+type Pos = (usize, usize);
+
+#[repr(u32)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+pub enum Dir {
+    North = 0,
+    West = 1,
+    South = 2,
+    Est = 3,
+}
+
+pub fn read(reader: &mut dyn BufRead) -> (Maze, Pos, Dir) {
+    let mut reinder_pos = Pos::default();
+    let mut maze: Vec<char> = Vec::new();
+    let mut nb_rows = 0;
+    let mut line_skipped = 0;
+
+    for (i, l) in reader.lines().enumerate() {
+        let l = l.as_ref().unwrap().trim();
+        if l.is_empty() {
+            line_skipped += 1;
+            continue;
+        }
+        nb_rows += 1;
+        for (j, c) in l.chars().enumerate() {
+            if c == 'S' {
+                reinder_pos = (i - line_skipped, j);
+                maze.push('.');
+            } else {
+                maze.push(c);
+            }
+        }
+    }
+
+    (
+        Maze::from_row_slice(nb_rows, maze.len() / nb_rows, &maze),
+        reinder_pos,
+        Dir::Est,
+    )
+}
+
+fn neighbours(pos: &Pos) -> [(Pos, Dir); 4] {
+    let (i, j) = *pos;
+    [
+        ((i - 1, j), Dir::North),
+        ((i, j - 1), Dir::West),
+        ((i + 1, j), Dir::South),
+        ((i, j + 1), Dir::Est),
+    ]
+}
+
+fn turn_cost(d1: &Dir, d2: &Dir) -> i64 {
+    let diff = (*d1 as u32).abs_diff(*d2 as u32) as i64;
+    1000 * if diff == 3 { 1 } else { diff }
+}
+
+pub fn best_score_and_nb_of_tiles_best_spot(
+    maze: &Maze,
+    start_pos: &Pos,
+    start_direction: &Dir,
+) -> (i64, i64) {
+    let mut scores: Vec<DMatrix<i64>> = std::iter::repeat_n(
+        DMatrix::from_element(maze.nrows(), maze.ncols(), i64::MAX),
+        4,
+    )
+    .collect();
+    scores[*start_direction as usize][(start_pos.0, start_pos.1)] = 0;
+
+    let mut heads = vec![(*start_pos, *start_direction)];
+    let mut best_score = i64::MAX;
+    let mut pos_end = (0, 0);
+
+    while !heads.is_empty() {
+        let mut new_heads = vec![];
+        for (pos, dir) in heads.iter() {
+            let current_score = scores[*dir as usize][*pos];
+            if maze[*pos] == 'E' && current_score < best_score {
+                best_score = current_score;
+                pos_end = *pos;
+            } else {
+                for (pos_neighbour, dir_neighbour) in neighbours(pos) {
+                    if maze[pos_neighbour] != '#' {
+                        let score = current_score + turn_cost(dir, &dir_neighbour) + 1;
+                        if score < scores[dir_neighbour as usize][pos_neighbour] {
+                            scores[dir_neighbour as usize][pos_neighbour] = score;
+                            new_heads.push((pos_neighbour, dir_neighbour));
+                        }
+                    }
+                }
+            }
+        }
+        heads = new_heads;
+    }
+
+    // Backtracking to find the number of tiles with the best spot.
+    let mut visited: HashMap<Pos, [bool; 4]> = HashMap::new();
+
+    for dir in [Dir::Est, Dir::North, Dir::South, Dir::West] {
+        if scores[dir as usize][pos_end] == best_score {
+            heads.push((pos_end, dir));
+            visited.entry(pos_end).or_default()[dir as usize] = true;
+        }
+    }
+
+    while !heads.is_empty() {
+        let mut new_heads = vec![];
+        for (pos, dir) in heads.iter() {
+            let (i, j) = *pos;
+            let pos_neighbour = match dir {
+                Dir::North => (i + 1, j),
+                Dir::West => (i, j + 1),
+                Dir::South => (i - 1, j),
+                Dir::Est => (i, j - 1),
+            };
+
+            for dir_neighbour in [Dir::Est, Dir::North, Dir::South, Dir::West] {
+                if scores[*dir as usize][*pos] - 1 - turn_cost(&dir_neighbour, dir)
+                    == scores[dir_neighbour as usize][pos_neighbour]
+                {
+                    let entry = visited.entry(pos_neighbour).or_default();
+                    if !entry[dir_neighbour as usize] {
+                        entry[dir_neighbour as usize] = true;
+                        new_heads.push((pos_neighbour, dir_neighbour));
+                    }
+                }
+            }
+        }
+        heads = new_heads;
+    }
+
+    (best_score, visited.len() as i64)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    static MAP_AND_MOVEMENTS_SIMPLE: &str = "
+#####
+#E..#
+###.#
+#S..#
+#####
+";
+
+    static MAP_AND_MOVEMENTS_A: &str = "
+###############
+#.......#....E#
+#.#.###.#.###.#
+#.....#.#...#.#
+#.###.#####.#.#
+#.#.#.......#.#
+#.#.#####.###.#
+#...........#.#
+###.#.#####.#.#
+#...#.....#.#.#
+#.#.#.###.#.#.#
+#.....#...#.#.#
+#.###.#.#.#.#.#
+#S..#.....#...#
+###############
+";
+
+    static MAP_AND_MOVEMENTS_B: &str = "
+#################
+#...#...#...#..E#
+#.#.#.#.#.#.#.#.#
+#.#.#.#...#...#.#
+#.#.#.#.###.#.#.#
+#...#.#.#.....#.#
+#.#.#.#.#.#####.#
+#.#...#.#.#.....#
+#.#.#####.#.###.#
+#.#.#.......#...#
+#.#.###.#####.###
+#.#.#...#.....#.#
+#.#.#.#####.###.#
+#.#.#.........#.#
+#.#.#.#########.#
+#S#.............#
+#################
+";
+
+    #[test]
+    fn part1() {
+        let (maze, start, direction) = read(&mut MAP_AND_MOVEMENTS_SIMPLE.as_bytes());
+        assert_eq!(
+            best_score_and_nb_of_tiles_best_spot(&maze, &start, &direction).0,
+            2006
+        );
+
+        let (maze, start, direction) = read(&mut MAP_AND_MOVEMENTS_A.as_bytes());
+        assert_eq!(
+            best_score_and_nb_of_tiles_best_spot(&maze, &start, &direction).0,
+            7036
+        );
+
+        let (maze, start, direction) = read(&mut MAP_AND_MOVEMENTS_B.as_bytes());
+        assert_eq!(
+            best_score_and_nb_of_tiles_best_spot(&maze, &start, &direction).0,
+            11048
+        );
+    }
+
+    static MAP_AND_MOVEMENTS_MULTIPLE_PATHS_1: &str = "
+######
+##..E#
+#S..##
+######
+";
+
+    static MAP_AND_MOVEMENTS_MULTIPLE_PATHS_2: &str = "
+######
+#....#
+#S##E#
+#....#
+######
+";
+
+    #[test]
+    fn part2() {
+        let (maze, start, direction) = read(&mut MAP_AND_MOVEMENTS_MULTIPLE_PATHS_1.as_bytes());
+        assert_eq!(
+            best_score_and_nb_of_tiles_best_spot(&maze, &start, &direction).1,
+            6
+        );
+
+        let (maze, start, direction) = read(&mut MAP_AND_MOVEMENTS_MULTIPLE_PATHS_2.as_bytes());
+        assert_eq!(
+            best_score_and_nb_of_tiles_best_spot(&maze, &start, &direction).1,
+            10
+        );
+
+        let (maze, start, direction) = read(&mut MAP_AND_MOVEMENTS_A.as_bytes());
+        assert_eq!(
+            best_score_and_nb_of_tiles_best_spot(&maze, &start, &direction).1,
+            45
+        );
+
+        let (maze, start, direction) = read(&mut MAP_AND_MOVEMENTS_B.as_bytes());
+        assert_eq!(
+            best_score_and_nb_of_tiles_best_spot(&maze, &start, &direction).1,
+            64
+        );
+    }
+}
index 3635b3d..afb47a7 100644 (file)
@@ -143,3 +143,9 @@ pub fn day15(reader: &mut dyn BufRead) -> String {
         day15::gps_coordinates_sum(pos, map, &movements, true),
     )
 }
+
+pub fn day16(reader: &mut dyn BufRead) -> String {
+    let (maze, start, direction) = day16::read(reader);
+    let (score, nb_tiles) = day16::best_score_and_nb_of_tiles_best_spot(&maze, &start, &direction);
+    format!("part1: {score}, part2: {nb_tiles}",)
+}
index fefa9ec..b962cd5 100644 (file)
@@ -1,7 +1,5 @@
 use std::io::BufRead;
 
-use advent_of_code_common;
-
 mod day01;
 mod day02;
 mod day03;
@@ -17,7 +15,7 @@ mod day12;
 mod day13;
 mod day14;
 mod day15;
-// mod day16;
+mod day16;
 // mod day17;
 // mod day18;
 // mod day19;
@@ -48,7 +46,7 @@ fn main() {
         days::day13,
         days::day14,
         days::day15,
-        // days::day16,
+        days::day16,
         // days::day17,
         // days::day18,
         // days::day19,