aoc/year2018/day15.rs
1//! # Beverage Bandits
2//!
3//! This problem is notoriously tricky due to the finicky rules that must be followed precisely and
4//! that not all inputs trigger all edge cases. However from a performance aspect most of the time
5//! is consumed finding the nearest target whenever a unit needs to move.
6//!
7//! For each move we perform two [BFS](https://en.wikipedia.org/wiki/Breadth-first_search).
8//! The first search from the current unit finds the nearest target in reading order.
9//! The second *reverse* search from the target to the current unit finds the correct direction
10//! to move.
11//!
12//! Since the cave dimensions are 32 x 32 we use a fixed sized array of bitmasks stored in `u32`
13//! to execute each BFS efficiently. Each step we expand the frontier using the bitwise logic
14//! applied to each row:
15//!
16//! ```none
17//! (previous | (current << 1) | current | (current >> 1) | next) & !walls
18//! ```
19//!
20//! We represent the goal using bits and stop searching once that intersects with the frontier.
21//! First example:
22//!
23//! * Goblin's turn.
24//! * We should choose the first target square in reading order (to the right of the nearest elf)
25//! * There are two equal shortest paths to that square, so we should choose the first *step* in
26//! reading order (up).
27//!
28//! ```none
29//! Map Walls In Range
30//! ####### 1111111 0000000
31//! #E # 1000001 0110000
32//! # E # 1000001 0111000
33//! # G# 1000001 0010000
34//! ####### 1111111 0000000
35//!
36//! Forward BFS frontier Intersection
37//! 0000000 0000000 0000000 0000000 0000000
38//! 0000000 0000000 0000010 0000110 0000000
39//! 0000000 => 0000010 => 0000110 => 0001110 => 0001000 <= Choose first target square
40//! 0000010 0000110 0001110 0011110 0010000 in reading order
41//! 0000000 0000000 0000000 0000000 0000000
42//!
43//! Reverse BFS frontier Intersection
44//! 0000000 0000000 0000000 0000000
45//! 0000000 0001000 0011100 0000000
46//! 0001000 => 0011100 => 0111110 => 0000010 <= Choose first step
47//! 0000000 0001000 0011100 0000100 in reading order
48//! 0000000 0000000 0000000 0000000
49//! ```
50//!
51//! Choosing the first intersection in reading order the Goblin correctly moves upwards.
52//! Second example:
53//!
54//! * Elf's turn.
55//! * There are two equal shortest paths.
56//! * We should choose the first *unit* in reading order (left).
57//!
58//! ```none
59//! Map Walls In Range
60//! ########### 11111111111 00000000000
61//! #G..#....G# 10001000001 01100000110
62//! ###..E##### 11100011111 00000000000
63//! ########### 11111111111 00000000000
64//!
65//! Forward BFS frontier Intersection
66//! 00000000000 00000000000 00000000000 00000000000 00000000000 00000000000
67//! 00000000000 00000100000 00000110000 00010111000 00110111100 00100000100
68//! 00000100000 => 00001100000 => 00011100000 => 00011100000 => 00011100000 => 00000000000
69//! 00000000000 00000000000 00000000000 00000000000 00000000000 00000000000
70//!
71//! Reverse BFS frontier Intersection
72//! 00000000000 00000000000 00000000000 00000000000 00000000000
73//! 00100000000 01110000000 01110000000 01110000000 00000000000
74//! 00000000000 => 00000000000 => 00010000000 => 00011000000 => 00001000000
75//! 00000000000 00000000000 00000000000 00000000000 00000000000
76//! ```
77//!
78//! Choosing the first intersection in reading order the Elf correctly moves left.
79use crate::util::grid::*;
80use crate::util::point::*;
81use crate::util::thread::*;
82
83const READING_ORDER: [Point; 4] = [UP, LEFT, RIGHT, DOWN];
84
85pub struct Input {
86 walls: [u32; 32],
87 elves: Vec<Point>,
88 goblins: Vec<Point>,
89}
90
91#[derive(Clone, Copy, PartialEq, Eq)]
92enum Kind {
93 Elf,
94 Goblin,
95}
96
97#[derive(Clone, Copy)]
98struct Unit {
99 position: Point,
100 kind: Kind,
101 health: i32,
102 power: i32,
103}
104
105/// Parse the input into a bitmask for the cave walls
106/// and a list of point coordinates for each Elf and Goblin.
107pub fn parse(input: &str) -> Input {
108 let grid = Grid::parse(input);
109
110 let mut walls = [0; 32];
111 let mut elves = Vec::new();
112 let mut goblins = Vec::new();
113
114 for y in 0..grid.height {
115 for x in 0..grid.width {
116 let position = Point::new(x, y);
117
118 match grid[position] {
119 b'#' => set_bit(&mut walls, position),
120 b'E' => elves.push(position),
121 b'G' => goblins.push(position),
122 _ => (),
123 }
124 }
125 }
126
127 Input { walls, elves, goblins }
128}
129
130/// Simulate a full fight until only Goblins remain.
131pub fn part1(input: &Input) -> i32 {
132 fight(input, 3, false).unwrap()
133}
134
135/// Find the lowest attack power where no Elf dies. We can short circuit any fight once a
136/// single Elf is killed. Since each fight is independent we can parallelize the search over
137/// multiple threads.
138pub fn part2(input: &Input) -> i32 {
139 let iter = AtomicIter::new(4, 1);
140
141 // Use as many cores as possible to parallelize the search.
142 let result = spawn(|| worker(input, &iter));
143 // Find lowest possible power.
144 result.into_iter().flatten().min_by_key(|&(eap, _)| eap).map(|(_, score)| score).unwrap()
145}
146
147fn worker(input: &Input, iter: &AtomicIter) -> Option<(u32, i32)> {
148 while let Some(power) = iter.next() {
149 // If the Elves win then set the score and signal all threads to stop.
150 // Use a channel to queue all potential scores as another thread may already have sent a
151 // different value.
152 if let Some(score) = fight(input, power as i32, true) {
153 iter.stop();
154 return Some((power, score));
155 }
156 }
157
158 None
159}
160
161/// Careful implementation of the game rules.
162fn fight(input: &Input, elf_attack_power: i32, part_two: bool) -> Option<i32> {
163 let mut units = Vec::new();
164 let mut elves = input.elves.len();
165 let mut goblins = input.goblins.len();
166 let mut grid = Grid::new(32, 32, None);
167
168 // Initialize each unit.
169 for &position in &input.elves {
170 units.push(Unit { position, kind: Kind::Elf, health: 200, power: elf_attack_power });
171 }
172 for &position in &input.goblins {
173 units.push(Unit { position, kind: Kind::Goblin, health: 200, power: 3 });
174 }
175
176 for turn in 0.. {
177 // Remove dead units for efficiency.
178 units.retain(|u| u.health > 0);
179 // Units take turns in reading order.
180 units.sort_unstable_by_key(|u| 32 * u.position.y + u.position.x);
181 // Grid is used for reverse lookup from location to index.
182 units.iter().enumerate().for_each(|(i, u)| grid[u.position] = Some(i));
183
184 for index in 0..units.len() {
185 let Unit { position, kind, health, power } = units[index];
186
187 // Unit may have been killed during this turn.
188 if health <= 0 {
189 continue;
190 }
191
192 // Check if there are no more remaining targets then return *complete* turns.
193 // Determining a complete turn is subtle. If the last unit to act (in reading order)
194 // kills the last remaining enemy then that counts as a complete turn. Otherwise the
195 // turn is considered incomplete and doesn't count.
196 if elves == 0 || goblins == 0 {
197 return Some(turn * units.iter().map(|u| u.health.max(0)).sum::<i32>());
198 }
199
200 // Search for neighboring enemies.
201 let mut nearby = attack(&grid, &units, position, kind);
202
203 // If no enemy next to unit then move toward nearest enemy in reading order,
204 // breaking equal distance ties in reading order.
205 if nearby.is_none()
206 && let Some(next) = double_bfs(input.walls, &units, position, kind)
207 {
208 grid[position] = None;
209 grid[next] = Some(index);
210 units[index].position = next;
211
212 nearby = attack(&grid, &units, next, kind);
213 }
214
215 // Attack enemy if possible.
216 if let Some(target) = nearby {
217 units[target].health -= power;
218
219 if units[target].health <= 0 {
220 grid[units[target].position] = None;
221
222 // For part two, short circuit if a single elf is killed.
223 match units[target].kind {
224 Kind::Elf if part_two => return None,
225 Kind::Elf => elves -= 1,
226 Kind::Goblin => goblins -= 1,
227 }
228 }
229 }
230 }
231 }
232
233 unreachable!()
234}
235
236/// Search for weakest neighboring enemy. Equal health ties are broken in reading order.
237fn attack(grid: &Grid<Option<usize>>, units: &[Unit], point: Point, kind: Kind) -> Option<usize> {
238 let mut enemy_health = i32::MAX;
239 let mut enemy_index = None;
240
241 for next in READING_ORDER.iter().filter_map(|&o| grid[point + o]) {
242 if units[next].kind != kind && units[next].health < enemy_health {
243 enemy_health = units[next].health;
244 enemy_index = Some(next);
245 }
246 }
247
248 enemy_index
249}
250
251/// Performs two BFS searches. The first search from the current unit finds the nearest target
252/// in reading order. The second reverse search from the target to the current unit, finds the
253/// correct direction to move.
254fn double_bfs(mut walls: [u32; 32], units: &[Unit], point: Point, kind: Kind) -> Option<Point> {
255 let frontier = &mut [0; 32];
256 set_bit(frontier, point);
257
258 let walls = &mut walls;
259 let in_range = &mut [0; 32];
260
261 for unit in units.iter().filter(|u| u.health > 0) {
262 if unit.kind == kind {
263 // Units of the same type are obstacles.
264 set_bit(walls, unit.position);
265 } else {
266 // Add enemy units to the list of potential targets.
267 set_bit(in_range, unit.position);
268 }
269 }
270
271 // We're interested in the 4 orthogonal squares around each enemy unit.
272 expand(walls, in_range);
273
274 // Search for reachable squares. There could be no reachable squares, for example friendly
275 // units already have the enemy surrounded or are blocking the path.
276 while expand(walls, frontier) {
277 if let Some(target) = intersect(in_range, frontier) {
278 // Reverse search from target to determine correct movement direction.
279 let frontier = &mut [0; 32];
280 set_bit(frontier, target);
281
282 let in_range = &mut [0; 32];
283 set_bit(in_range, point);
284 expand(walls, in_range);
285
286 // This will always succeed as there was a path from the current unit.
287 loop {
288 expand(walls, frontier);
289 if let Some(target) = intersect(in_range, frontier) {
290 return Some(target);
291 }
292 }
293 }
294 }
295
296 None
297}
298
299/// Use bitwise logic to expand the frontier. Returns a boolean indicating if the frontier
300/// actually expanded.
301fn expand(walls: &[u32], frontier: &mut [u32]) -> bool {
302 let mut previous = frontier[0];
303 let mut changed = 0;
304
305 for i in 1..31 {
306 let current = frontier[i];
307 let next = frontier[i + 1];
308
309 frontier[i] = (previous | (current << 1) | current | (current >> 1) | next) & !walls[i];
310
311 previous = current;
312 changed |= current ^ frontier[i];
313 }
314
315 changed != 0
316}
317
318/// Check if we have reached a target, returning the first target in reading order.
319fn intersect(in_range: &[u32], frontier: &[u32]) -> Option<Point> {
320 for i in 1..31 {
321 let both = in_range[i] & frontier[i];
322
323 if both != 0 {
324 let x = both.trailing_zeros() as i32;
325 let y = i as i32;
326 return Some(Point::new(x, y));
327 }
328 }
329
330 None
331}
332
333/// Convenience function to set a single bit from a point's location.
334#[inline]
335fn set_bit(slice: &mut [u32], point: Point) {
336 slice[point.y as usize] |= 1 << point.x;
337}