Skip to content

Commit 3d7fce1

Browse files
committed
Day 20 solutions
1 parent c369895 commit 3d7fce1

File tree

5 files changed

+425
-4
lines changed

5 files changed

+425
-4
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,4 @@ This should start the server at `localhost:8080`.
105105
❄️ [Day 17](aoc-solver/src/y2024/day17.rs)
106106
❄️ [Day 18](aoc-solver/src/y2024/day18.rs)
107107
❄️ [Day 19](aoc-solver/src/y2024/day19.rs)
108+
❄️ [Day 20](aoc-solver/src/y2024/day20.rs)

aoc-solver/src/y2024/day20.rs

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
use std::collections::{HashMap, HashSet};
2+
3+
use crate::solution::{AocError, Solution};
4+
5+
type Coords = (usize, usize);
6+
type Grid = Vec<Vec<bool>>;
7+
8+
const DIRECTIONS: [(isize, isize); 4] = [(0, -1), (1, 0), (0, 1), (-1, 0)];
9+
10+
fn parse(input: &str) -> Result<(Grid, Coords, Coords), AocError> {
11+
let mut grid = vec![];
12+
let mut start = None;
13+
let mut end = None;
14+
15+
for (y, line) in input.trim().lines().enumerate() {
16+
let mut row = vec![];
17+
for (x, tile) in line.chars().enumerate() {
18+
match tile {
19+
'#' => row.push(false),
20+
'.' => row.push(true),
21+
'S' => {
22+
row.push(true);
23+
start = Some((x, y))
24+
}
25+
'E' => {
26+
row.push(true);
27+
end = Some((x, y))
28+
}
29+
other => return Err(AocError::parse(other, "Unexpected tile")),
30+
}
31+
}
32+
grid.push(row);
33+
}
34+
35+
match (start, end) {
36+
(Some(start), Some(end)) => Ok((grid, start, end)),
37+
_ => Err(AocError::parse(input, "Missing start or end")),
38+
}
39+
}
40+
41+
fn is_outside_bounds(height: isize, width: isize, x: isize, y: isize) -> bool {
42+
x < 0 || y < 0 || x >= width || y >= height
43+
}
44+
45+
fn race_to_end(grid: &Grid, start: Coords, end: Coords) -> (Vec<Coords>, HashMap<Coords, u32>) {
46+
let mut distances: HashMap<Coords, u32> = HashMap::new();
47+
let mut route: Vec<Coords> = vec![];
48+
49+
let height = grid.len() as isize;
50+
let width = grid[0].len() as isize;
51+
52+
let mut distance = 0;
53+
let mut position = start;
54+
55+
loop {
56+
route.push(position);
57+
distances.insert(position, distance);
58+
59+
if position == end {
60+
return (route, distances);
61+
}
62+
63+
for (dx, dy) in DIRECTIONS {
64+
let (x, y) = (position.0 as isize + dx, position.1 as isize + dy);
65+
66+
if is_outside_bounds(height, width, x, y) {
67+
continue;
68+
}
69+
70+
let next_pos = (x as usize, y as usize);
71+
72+
if grid[y as usize][x as usize] && !distances.contains_key(&next_pos) {
73+
position = next_pos;
74+
distance += 1;
75+
break;
76+
}
77+
}
78+
}
79+
}
80+
81+
fn manhattan(a: &Coords, b: &Coords) -> u32 {
82+
(a.0.abs_diff(b.0) + a.1.abs_diff(b.1)) as u32
83+
}
84+
85+
fn cheat_targets(grid: &Grid, start: &Coords, cheat_duration: u32) -> HashSet<Coords> {
86+
let height = grid.len() as isize;
87+
let width = grid[0].len() as isize;
88+
89+
let mut targets = HashSet::new();
90+
let duration = cheat_duration as isize;
91+
92+
for dy in -duration..=duration {
93+
let max_x = duration - dy.abs();
94+
for dx in -max_x..=max_x {
95+
let (x, y) = (start.0 as isize + dx, start.1 as isize + dy);
96+
if !is_outside_bounds(height, width, x, y) && grid[y as usize][x as usize] {
97+
targets.insert((x as usize, y as usize));
98+
}
99+
}
100+
}
101+
102+
targets
103+
}
104+
105+
fn explore_cheats(
106+
grid: &Grid,
107+
start: Coords,
108+
end: Coords,
109+
cheat_duration: u32,
110+
) -> HashMap<u32, u32> {
111+
let (route, distances) = race_to_end(grid, start, end);
112+
let fair_distance = distances[&end];
113+
114+
let mut cheats: HashMap<u32, u32> = HashMap::new();
115+
116+
for (distance, position) in route.iter().enumerate() {
117+
for cheat_target in cheat_targets(grid, position, cheat_duration).iter() {
118+
let remaining_distance = fair_distance - distances[cheat_target];
119+
let cheat_distance =
120+
distance as u32 + manhattan(position, cheat_target) + remaining_distance;
121+
let savings = fair_distance.saturating_sub(cheat_distance);
122+
123+
*cheats.entry(savings).or_default() += 1;
124+
}
125+
}
126+
127+
cheats
128+
}
129+
130+
fn filter_threshold(cheats: HashMap<u32, u32>, threshold: u32) -> u32 {
131+
cheats
132+
.into_iter()
133+
.filter(|(distance, _cheats)| *distance >= threshold)
134+
.map(|(_distance, cheats)| cheats)
135+
.sum()
136+
}
137+
138+
pub struct Day20;
139+
impl Solution for Day20 {
140+
type A = u32;
141+
type B = u32;
142+
143+
fn default_input(&self) -> &'static str {
144+
include_str!("../../../inputs/2024/day20.txt")
145+
}
146+
147+
fn part_1(&self, input: &str) -> Result<u32, AocError> {
148+
let (grid, start, end) = parse(input)?;
149+
let cheats = explore_cheats(&grid, start, end, 2);
150+
let good_cheats = filter_threshold(cheats, 100);
151+
152+
Ok(good_cheats)
153+
}
154+
155+
fn part_2(&self, input: &str) -> Result<u32, AocError> {
156+
let (grid, start, end) = parse(input)?;
157+
let cheats = explore_cheats(&grid, start, end, 20);
158+
let good_cheats = filter_threshold(cheats, 100);
159+
160+
Ok(good_cheats)
161+
}
162+
}
163+
164+
#[cfg(test)]
165+
mod tests {
166+
use super::*;
167+
168+
#[rustfmt::skip]
169+
const INPUT: &str =
170+
"###############\n\
171+
#...#...#.....#\n\
172+
#.#.#.#.#.###.#\n\
173+
#S#...#.#.#...#\n\
174+
#######.#.#.###\n\
175+
#######.#.#...#\n\
176+
#######.#.###.#\n\
177+
###..E#...#...#\n\
178+
###.#######.###\n\
179+
#...###...#...#\n\
180+
#.#####.#.###.#\n\
181+
#.#...#.#.#...#\n\
182+
#.#.#.#.#.#.###\n\
183+
#...#...#...###\n\
184+
###############";
185+
186+
#[test]
187+
fn it_solves_part1_example() {
188+
let (grid, start, end) = parse(INPUT).unwrap();
189+
190+
let cheats = explore_cheats(&grid, start, end, 2);
191+
assert_eq!(cheats[&2], 14);
192+
assert_eq!(cheats[&4], 14);
193+
assert_eq!(cheats[&6], 2);
194+
assert_eq!(cheats[&8], 4);
195+
assert_eq!(cheats[&10], 2);
196+
assert_eq!(cheats[&12], 3);
197+
assert_eq!(cheats[&20], 1);
198+
assert_eq!(cheats[&36], 1);
199+
assert_eq!(cheats[&38], 1);
200+
assert_eq!(cheats[&40], 1);
201+
assert_eq!(cheats[&64], 1);
202+
}
203+
204+
#[test]
205+
fn it_solves_part2_example() {
206+
let (grid, start, end) = parse(INPUT).unwrap();
207+
208+
let cheats = explore_cheats(&grid, start, end, 20);
209+
210+
assert_eq!(cheats[&50], 32);
211+
assert_eq!(cheats[&52], 31);
212+
assert_eq!(cheats[&56], 39);
213+
assert_eq!(cheats[&58], 25);
214+
assert_eq!(cheats[&60], 23);
215+
assert_eq!(cheats[&62], 20);
216+
assert_eq!(cheats[&64], 19);
217+
assert_eq!(cheats[&66], 12);
218+
assert_eq!(cheats[&68], 14);
219+
assert_eq!(cheats[&70], 12);
220+
assert_eq!(cheats[&72], 22);
221+
assert_eq!(cheats[&74], 4);
222+
assert_eq!(cheats[&76], 3);
223+
}
224+
225+
#[test]
226+
fn it_finds_expected_cheats_1() {
227+
let (grid, _start, _end) = parse(INPUT).unwrap();
228+
229+
assert_eq!(
230+
cheat_targets(&grid, &(2, 3), 1),
231+
HashSet::from([(1, 3), (3, 3)])
232+
);
233+
}
234+
235+
#[test]
236+
fn it_finds_expected_cheats_2() {
237+
let (grid, _start, _end) = parse(INPUT).unwrap();
238+
239+
assert_eq!(
240+
cheat_targets(&grid, &(2, 3), 2),
241+
HashSet::from([(1, 2), (2, 1), (3, 2), (4, 3), (1, 3), (3, 3)])
242+
);
243+
}
244+
245+
#[test]
246+
fn it_finds_expected_cheats_3() {
247+
let (grid, _start, _end) = parse(INPUT).unwrap();
248+
249+
assert_eq!(
250+
cheat_targets(&grid, &(2, 3), 3),
251+
HashSet::from([
252+
(3, 3),
253+
(3, 2),
254+
(3, 1),
255+
(1, 1),
256+
(2, 1),
257+
(4, 3),
258+
(1, 2),
259+
(1, 3),
260+
(5, 3)
261+
])
262+
);
263+
}
264+
265+
#[test]
266+
fn it_finds_expected_cheats_5() {
267+
let (grid, _start, _end) = parse(INPUT).unwrap();
268+
269+
assert_eq!(cheat_targets(&grid, &(2, 3), 5).len(), 13);
270+
}
271+
272+
#[test]
273+
fn it_finds_expected_cheats_20() {
274+
let (grid, _start, _end) = parse(INPUT).unwrap();
275+
276+
assert_eq!(cheat_targets(&grid, &(2, 3), 20).len(), 85);
277+
}
278+
}

aoc-solver/src/y2024/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ pub mod day16;
1919
pub mod day17;
2020
pub mod day18;
2121
pub mod day19;
22-
// pub mod day20;
22+
pub mod day20;
2323
// pub mod day21;
2424
// pub mod day22;
2525
// pub mod day23;
2626
// pub mod day24;
2727
// pub mod day25;
2828

29-
pub const MAX_DAYS: u8 = 19;
29+
pub const MAX_DAYS: u8 = 20;
3030

3131
pub struct Y2024;
3232

@@ -52,7 +52,7 @@ impl Solver for Y2024 {
5252
17 => day17::Day17.run(input, 17, 2024),
5353
18 => day18::Day18.run(input, 18, 2024),
5454
19 => day19::Day19.run(input, 19, 2024),
55-
// 20 => day20::Day20.run(input, 20, 2024),
55+
20 => day20::Day20.run(input, 20, 2024),
5656
// 21 => day21::Day21.run(input, 21, 2024),
5757
// 22 => day22::Day22.run(input, 22, 2024),
5858
// 23 => day23::Day23.run(input, 23, 2024),
@@ -94,7 +94,7 @@ impl Solver for Y2024 {
9494
17 => include_str!("./day17.rs"),
9595
18 => include_str!("./day18.rs"),
9696
19 => include_str!("./day19.rs"),
97-
// 20 => include_str!("./day20.rs"),
97+
20 => include_str!("./day20.rs"),
9898
// 21 => include_str!("./day21.rs"),
9999
// 22 => include_str!("./day22.rs"),
100100
// 23 => include_str!("./day23.rs"),

aoc-web/src/header.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ pub fn header(props: &HeaderProps) -> Html {
4040
<NavLink route={Route::Solution { year: 2024, day: 17 }} current={props.route.clone()} text={"17"}/>
4141
<NavLink route={Route::Solution { year: 2024, day: 18 }} current={props.route.clone()} text={"18"}/>
4242
<NavLink route={Route::Solution { year: 2024, day: 19 }} current={props.route.clone()} text={"19"}/>
43+
<NavLink route={Route::Solution { year: 2024, day: 20 }} current={props.route.clone()} text={"20"}/>
4344
</>
4445
}
4546
},

0 commit comments

Comments
 (0)