Skip to content

Commit e461dd2

Browse files
committed
Implement part 1 more efficiently via skips
1 parent 55a4fba commit e461dd2

File tree

1 file changed

+64
-44
lines changed

1 file changed

+64
-44
lines changed

day20/src/day20.rs

+64-44
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ struct Racetrack {
2121
struct Cheat {
2222
start: Vec2<i32>,
2323
end: Option<Vec2<i32>>,
24-
picos_left: i32,
24+
picos_left: usize,
2525
}
2626

2727
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
2828
struct Node {
2929
pos: Vec2<i32>,
30-
picos: i32,
31-
cost: i32,
30+
picos: usize,
31+
cost: usize,
3232
cheat: Option<Cheat>,
3333
}
3434

@@ -46,10 +46,21 @@ impl PartialOrd for Node {
4646

4747
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
4848
enum CheatPolicy {
49-
Allowed { picos: i32 },
49+
Allowed { picos: usize },
5050
Forbidden,
5151
}
5252

53+
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
54+
struct Skip {
55+
positions: Vec<Vec2<i32>>,
56+
}
57+
58+
impl Skip {
59+
fn len(&self) -> usize {
60+
self.positions.len()
61+
}
62+
}
63+
5364
impl Racetrack {
5465
fn height(&self) -> i32 {
5566
self.rows.len() as i32
@@ -67,8 +78,8 @@ impl Racetrack {
6778
(-1..=1)
6879
.flat_map(move |dy| (-1..=1)
6980
.filter(move |&dx| (dx != 0) ^ (dy != 0))
70-
.map(move |dx| Vec2::new(pos.x + dx, pos.y + dy)))
71-
.filter(move |&neigh| self.in_bounds(neigh))
81+
.map(move |dx| Vec2::new(pos.x + dx, pos.y + dy))
82+
.filter(move |&neigh| self.in_bounds(neigh)))
7283
}
7384

7485
fn locate(&self, c: char) -> Option<Vec2<i32>> {
@@ -78,7 +89,7 @@ impl Racetrack {
7889
}
7990

8091
/// Traces out the racetrack, finding the distances to the given end.
81-
fn find_distances_to_end(&self, start: Vec2<i32>, end: Vec2<i32>) -> HashMap<Vec2<i32>, i32> {
92+
fn find_skips_to_end(&self, start: Vec2<i32>, end: Vec2<i32>) -> HashMap<Vec2<i32>, Skip> {
8293
let mut stack = Vec::new();
8394
let mut distances = HashMap::new();
8495

@@ -92,60 +103,71 @@ impl Racetrack {
92103
}
93104

94105
for (i, &pos) in stack.iter().enumerate() {
95-
distances.insert(pos, (stack.len() - 1 - i) as i32);
106+
distances.insert(pos, Skip { positions: stack[(i + 1)..].iter().cloned().collect() });
96107
}
97108

98109
distances
99110
}
100111

101112
/// Finds paths through the racetrack, ordered ascendingly by total picoseconds.
102-
fn find_paths(&self, start: Vec2<i32>, end: Vec2<i32>, cheat_policy: CheatPolicy, condition: impl Fn(Node) -> bool) -> Vec<Node> {
113+
fn count_paths(&self, start: Vec2<i32>, end: Vec2<i32>, cheat_policy: CheatPolicy, condition: impl Fn(usize) -> bool) -> i32 {
103114
// Your run-of-the-mill A* (Dijkstra + heuristic) implementation
104115

116+
let skips = self.find_skips_to_end(start, end);
105117
let mut queue = BinaryHeap::new();
106118
let mut visited = HashSet::new();
107-
let mut paths = Vec::new();
119+
let mut paths = 0;
108120

109121
queue.push(Node { pos: start, picos: 0, cost: 0, cheat: None });
110122
visited.insert((start, None));
111123

112124
while let Some(node) = queue.pop() {
125+
let cheats_allowed = matches!(cheat_policy, CheatPolicy::Allowed { .. });
126+
let can_cheat = cheats_allowed && node.cheat.map_or(true, |c| c.picos_left > 0);
127+
113128
if node.pos == end {
114-
if !condition(node) {
129+
if !condition(node.picos) {
115130
break;
116131
}
117-
println!("{node:?}");
118-
paths.push(node);
132+
// println!("{}", skips[&start].len() - node.cost);
133+
paths += 1;
134+
continue;
119135
}
120136

121-
for neigh in self.neighbors(node.pos) {
122-
let is_wall = self[neigh] == '#';
123-
124-
let cheats_allowed = matches!(cheat_policy, CheatPolicy::Allowed { .. });
125-
let can_cheat = cheats_allowed && node.cheat.map_or(true, |c| c.picos_left > 0);
126-
let new_cheat = if let Some(cheat) = node.cheat {
127-
let is_ending = cheat.picos_left == 1;
128-
if is_ending && is_wall {
129-
continue;
130-
}
131-
Some(Cheat { start: cheat.start, end: if is_ending { Some(neigh) } else { cheat.end }, picos_left: (cheat.picos_left - 1).max(0) })
132-
} else if cheats_allowed && is_wall {
133-
if let CheatPolicy::Allowed { picos: cheat_picos } = cheat_policy {
134-
Some(Cheat { start: node.pos, end: None, picos_left: cheat_picos })
137+
if !can_cheat && skips.contains_key(&node.pos) {
138+
// Skip the intermediate positions and hop straight to the end
139+
let skip = &skips[&node.pos];
140+
let new_picos = node.picos + skip.len();
141+
queue.push(Node { pos: end, cost: new_picos, picos: new_picos, cheat: node.cheat });
142+
} else {
143+
// Search as usual
144+
for neigh in self.neighbors(node.pos) {
145+
let is_wall = self[neigh] == '#';
146+
147+
let new_cheat = if let Some(cheat) = node.cheat {
148+
let is_ending = cheat.picos_left == 1;
149+
if is_ending && is_wall {
150+
continue;
151+
}
152+
Some(Cheat { start: cheat.start, end: if is_ending { Some(neigh) } else { cheat.end }, picos_left: cheat.picos_left.max(1) - 1 })
153+
} else if cheats_allowed && is_wall {
154+
if let CheatPolicy::Allowed { picos: cheat_picos } = cheat_policy {
155+
Some(Cheat { start: node.pos, end: None, picos_left: cheat_picos - 1 })
156+
} else {
157+
unreachable!()
158+
}
135159
} else {
136-
unreachable!()
137-
}
138-
} else {
139-
node.cheat
140-
};
160+
node.cheat
161+
};
141162

142-
if !visited.contains(&(neigh, new_cheat)) && (!is_wall || can_cheat) {
143-
visited.insert((neigh, new_cheat));
163+
if !visited.contains(&(neigh, new_cheat)) && (!is_wall || can_cheat) {
164+
visited.insert((neigh, new_cheat));
144165

145-
let new_picos = node.picos + 1;
146-
let new_dist_to_end = (neigh.x.abs_diff(end.x) + neigh.y.abs_diff(end.y)) as i32;
147-
let new_cost = new_picos + new_dist_to_end;
148-
queue.push(Node { pos: neigh, picos: new_picos, cost: new_cost, cheat: new_cheat });
166+
let new_picos = node.picos + 1;
167+
let new_dist_to_end = (neigh.x.abs_diff(end.x) + neigh.y.abs_diff(end.y)) as usize;
168+
let new_cost = new_picos + new_dist_to_end;
169+
queue.push(Node { pos: neigh, picos: new_picos, cost: new_cost, cheat: new_cheat });
170+
}
149171
}
150172
}
151173
}
@@ -175,14 +197,12 @@ fn main() {
175197
let start = track.locate('S').unwrap();
176198
let end = track.locate('E').unwrap();
177199

178-
let distances_to_end = track.find_distances_to_end(start, end);
179-
let base_picos = distances_to_end[&start];
200+
let skips = track.find_skips_to_end(start, end);
201+
let base_picos = skips[&start].len();
180202

181-
let cheat_paths1 = track.find_paths(start, end, CheatPolicy::Allowed { picos: 2 }, |n| n.picos <= base_picos - 100);
182-
let part1 = cheat_paths1.len();
203+
let part1 = track.count_paths(start, end, CheatPolicy::Allowed { picos: 2 }, |picos| picos + 100 <= base_picos);
183204
println!("Part 1: {part1}");
184205

185-
let cheat_paths2 = track.find_paths(start, end, CheatPolicy::Allowed { picos: 20 }, |n| n.picos <= base_picos - 100);
186-
let part2 = cheat_paths2.len();
206+
let part2 = track.count_paths(start, end, CheatPolicy::Allowed { picos: 20 }, |picos| picos + 100 <= base_picos);
187207
println!("Part 2: {part2}");
188208
}

0 commit comments

Comments
 (0)