Skip to main content

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