-
Notifications
You must be signed in to change notification settings - Fork 16
/
Copy pathday20.rs
135 lines (115 loc) · 4.42 KB
/
day20.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
//! # Race Condition
//!
//! Examining the input shows that there is only a single path from start to finish with
//! no branches. This simplifies checking for shortcuts as any empty space will be on the shortest
//! path from start to end. The cheating rules allow us to "warp" up to `n` squares away to any
//! empty space as measured by [manhattan distance](https://en.wikipedia.org/wiki/Taxicab_geometry).
//!
//! For part one this is a distance of 2. When checking surrounding squares we make 2 optimizations:
//!
//! * Don't check any squares only 1 away as we can always just move to these normally.
//! * Checking from point `p => q` is always the negative of `q => p`, e.g if `p = 30, q = 50` then
//! `p => q = 20` and `q => p = -20`. This means we only ever need to check any pair once.
//! * Additionally we only need to check down and right. Previous rows and columns will already
//! have checked points above and to the left when we reach an empty square.
//!
//! ```none
//! # .
//! ### ...
//! ##P## => ....#
//! ### ...
//! # #
//! ```
//!
//! For part two the distance is increased to 20 and the shape now resembles a wonky diamond.
//! This shape ensures complete coverage without duplicating checks.
//!
//! ```none
//! # .
//! ### ...
//! ##P## => ..P##
//! ### ###
//! # #
//! ```
use crate::util::grid::*;
use crate::util::point::*;
use crate::util::thread::*;
use std::sync::atomic::{AtomicU32, Ordering};
/// Create a grid the same size as input with the time taken from start to any location.
pub fn parse(input: &str) -> Grid<i32> {
let grid = Grid::parse(input);
let start = grid.find(b'S').unwrap();
let end = grid.find(b'E').unwrap();
let mut time = grid.same_size_with(i32::MAX);
let mut elapsed = 0;
// Find starting direction, assuming start position is surrounded by 3 walls.
let mut position = start;
let mut direction = ORTHOGONAL.into_iter().find(|&o| grid[position + o] != b'#').unwrap();
while position != end {
time[position] = elapsed;
elapsed += 1;
// There are no branches so we only ever need to go straight ahead or turn left or right.
direction = [direction, direction.clockwise(), direction.counter_clockwise()]
.into_iter()
.find(|&d| grid[position + d] != b'#')
.unwrap();
position += direction;
}
time[end] = elapsed;
time
}
pub fn part1(time: &Grid<i32>) -> u32 {
let mut cheats = 0;
for y in 1..time.height - 1 {
for x in 1..time.width - 1 {
let point = Point::new(x, y);
// We only need to check 2 points to the right and down as previous empty squares
// have already checked up and to the left.
if time[point] != i32::MAX {
cheats += check(time, point, Point::new(2, 0));
cheats += check(time, point, Point::new(0, 2));
}
}
}
cheats
}
/// Searches for all cheats up to distance 20, parallelizing the work over multiple threads.
pub fn part2(time: &Grid<i32>) -> u32 {
let mut items = Vec::with_capacity(10_000);
for y in 1..time.height - 1 {
for x in 1..time.width - 1 {
let point = Point::new(x, y);
if time[point] != i32::MAX {
items.push(point);
}
}
}
// Use as many cores as possible to parallelize the remaining search.
let total = AtomicU32::new(0);
spawn_parallel_iterator(&items, |iter| worker(time, &total, iter));
total.into_inner()
}
fn worker(time: &Grid<i32>, total: &AtomicU32, iter: ParIter<'_, Point>) {
let mut cheats = 0;
// (p1, p2) is the reciprocal of (p2, p1) so we only need to check each pair once.
for &point in iter {
for x in 2..21 {
cheats += check(time, point, Point::new(x, 0));
}
for y in 1..21 {
for x in (y - 20)..(21 - y) {
cheats += check(time, point, Point::new(x, y));
}
}
}
// Update global total.
total.fetch_add(cheats, Ordering::Relaxed);
}
// Check if we save enough time warping to another square.
#[inline]
fn check(time: &Grid<i32>, first: Point, delta: Point) -> u32 {
let second = first + delta;
(time.contains(second)
&& time[second] != i32::MAX
&& (time[first] - time[second]).abs() - first.manhattan(second) >= 100) as u32
}