Skip to content

Commit 37f07bb

Browse files
committed
solve: day16
1 parent cebba2f commit 37f07bb

File tree

4 files changed

+168
-1
lines changed

4 files changed

+168
-1
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
resolver = "2"
33
members = [
44
"utils",
5-
"aoc_derive", "day1", "day2", "day3", "day4", "day5", "day6", "day7", "day8", "day9", "day10", "day11", "day12", "day13", "day14", "day15",
5+
"aoc_derive", "day1", "day2", "day3", "day4", "day5", "day6", "day7", "day8", "day9", "day10", "day11", "day12", "day13", "day14", "day15", "day16",
66
]
77

88
[workspace.dependencies]

day16/Cargo.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "day16"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[dependencies]
7+
aoc_derive.path = '../aoc_derive'
8+
utils.path = '../utils'
9+
derive_more.workspace = true
10+
itertools.workspace = true
11+
lazy-regex.workspace = true
12+
parse-display.workspace = true
13+
rayon.workspace = true
14+
regex.workspace = true
15+
num.workspace = true
16+
priority-queue.workspace = true
17+
18+
[dev-dependencies]
19+
pretty_assertions.workspace = true

day16/src/main.rs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
use std::collections::{HashMap, HashSet};
2+
3+
use aoc_derive::aoc_main;
4+
use graphs::{WeightedGraph, dijkstra};
5+
use grid::Grid;
6+
use itertools::Itertools;
7+
use math::Vec2D;
8+
use utils::*;
9+
10+
#[derive(Debug, Clone, derive_more::Into, derive_more::Deref)]
11+
struct Maze(Grid<char>);
12+
13+
impl WeightedGraph for Maze {
14+
type Node = (Vec2D, Vec2D);
15+
16+
fn neighbors<'a, 'b: 'a>(
17+
&'a self,
18+
(pos, heading): &'b Self::Node,
19+
) -> impl Iterator<Item = (Self::Node, graphs::Cost)> + 'a {
20+
self.orthogonal_neighbors(pos).filter_map(|neighbor| {
21+
(self[neighbor] != '#').then_some((
22+
(neighbor, *pos - neighbor),
23+
1 + if *pos - neighbor == *heading { 0 } else { 1000 },
24+
))
25+
})
26+
}
27+
}
28+
29+
fn all_paths(
30+
maze: &Maze,
31+
pos: Vec2D,
32+
heading: Vec2D,
33+
score: usize,
34+
max_score: usize,
35+
mut path: HashSet<Vec2D>,
36+
visited: &mut HashMap<(Vec2D, Vec2D), usize>,
37+
) -> HashSet<Vec2D> {
38+
// This is the main optimization that makes DFS work here:
39+
// If we already visited this node with a lower score, stop the search
40+
if let Some(&prev_score) = visited.get(&(pos, heading)) {
41+
if prev_score < score {
42+
return HashSet::new();
43+
}
44+
}
45+
visited.insert((pos, heading), score);
46+
47+
// Stop if we already have a higher cost than the optimal solution
48+
if score > max_score {
49+
return HashSet::new();
50+
}
51+
52+
path.insert(pos);
53+
if maze[pos] == 'E' {
54+
return path;
55+
}
56+
57+
maze.neighbors(&(pos, heading))
58+
// Don't go backwards
59+
.filter(|((pos, _), _)| !path.contains(pos))
60+
// Sort ascending, so that we try to go forward before doing a turn
61+
.sorted_by_key(|((_, _), new_score)| *new_score)
62+
.flat_map(|((neighbor, new_heading), new_score)| {
63+
all_paths(
64+
maze,
65+
neighbor,
66+
new_heading,
67+
score + new_score,
68+
max_score,
69+
path.clone(),
70+
visited,
71+
)
72+
})
73+
.collect()
74+
}
75+
76+
#[aoc_main]
77+
fn solve(input: Input) -> impl Into<Solution> {
78+
let maze = Maze(input.char_grid());
79+
80+
let (start, heading) = (maze.find_position(&'S').unwrap(), Vec2D::new(1, 0));
81+
82+
let part1 = dijkstra(&maze, [(start, heading)], |(pos, _)| maze[*pos] == 'E').unwrap();
83+
84+
let part2 =
85+
all_paths(&maze, start, heading, 0, part1, HashSet::new(), &mut HashMap::new()).len();
86+
87+
(part1, part2)
88+
}
89+
90+
#[cfg(test)]
91+
mod tests {
92+
use super::*;
93+
#[test]
94+
fn test_examples() {
95+
use utils::assert_example;
96+
assert_example!(
97+
r#"###############
98+
#.......#....E#
99+
#.#.###.#.###.#
100+
#.....#.#...#.#
101+
#.###.#####.#.#
102+
#.#.#.......#.#
103+
#.#.#####.###.#
104+
#...........#.#
105+
###.#.#####.#.#
106+
#...#.....#.#.#
107+
#.#.#.###.#.#.#
108+
#.....#...#.#.#
109+
#.###.#.#.#.#.#
110+
#S..#.....#...#
111+
###############
112+
"#,
113+
7036,
114+
45
115+
);
116+
117+
assert_example!(
118+
"#################
119+
#...#...#...#..E#
120+
#.#.#.#.#.#.#.#^#
121+
#.#.#.#...#...#^#
122+
#.#.#.#.###.#.#^#
123+
#>>v#.#.#.....#^#
124+
#^#v#.#.#.#####^#
125+
#^#v..#.#.#>>>>^#
126+
#^#v#####.#^###.#
127+
#^#v#..>>>>^#...#
128+
#^#v###^#####.###
129+
#^#v#>>^#.....#.#
130+
#^#v#^#####.###.#
131+
#^#v#^........#.#
132+
#^#v#^#########.#
133+
#S#>>^..........#
134+
#################",
135+
11048,
136+
64
137+
);
138+
}
139+
}

utils/src/grid.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,15 @@ impl<T> Grid<T> {
189189
}
190190
}
191191

192+
impl<T> Grid<T>
193+
where
194+
T: PartialEq,
195+
{
196+
pub fn find_position(&self, val: &T) -> Option<Vec2D> {
197+
self.iter().find_map(|(pos, v)| (v == val).then_some(pos))
198+
}
199+
}
200+
192201
impl<T> Grid<T>
193202
where
194203
T: Clone,

0 commit comments

Comments
 (0)