aoc/year2020/
day24.rs

1//! # Lobby Layout
2//!
3//! Hex grid parsing and navigation uses
4//! [Axial Coordinates](https://www.redblobgames.com/grids/hexagons/#coordinates-cube)
5//! exactly as described in the excellent [Red Blob Games](https://www.redblobgames.com/) blog.
6//!
7//! Part two uses exactly the same approach as [`day 17`] and most of the code is identical.
8//!
9//! As the black tiles are very sparse (about 8% for my input) it's faster to switch from
10//! a "pull" model where we check the surroundings neighbors of each tile, to a "push" model
11//! where we update the neighbors of each black tile instead.
12//!
13//! [`day 17`]: crate::year2020::day17
14use crate::util::hash::*;
15use std::array::from_fn;
16
17#[derive(PartialEq, Eq, Hash)]
18pub struct Hex {
19    q: i32,
20    r: i32,
21}
22
23pub fn parse(input: &str) -> FastSet<Hex> {
24    let mut tiles = FastSet::new();
25
26    for line in input.lines() {
27        let mut iter = line.bytes();
28        let mut q = 0;
29        let mut r = 0;
30
31        while let Some(b) = iter.next() {
32            match b {
33                b'e' => q += 1,
34                b'w' => q -= 1,
35                b'n' => {
36                    if b'e' == iter.next().unwrap() {
37                        q += 1;
38                    }
39                    r -= 1;
40                }
41                b's' => {
42                    if b'e' != iter.next().unwrap() {
43                        q -= 1;
44                    }
45                    r += 1;
46                }
47                _ => unreachable!(),
48            }
49        }
50
51        let tile = Hex { q, r };
52        if tiles.contains(&tile) {
53            tiles.remove(&tile);
54        } else {
55            tiles.insert(tile);
56        }
57    }
58
59    tiles
60}
61
62pub fn part1(input: &FastSet<Hex>) -> usize {
63    input.len()
64}
65
66pub fn part2(input: &FastSet<Hex>) -> usize {
67    // Determine bounds
68    let mut q1 = i32::MAX;
69    let mut q2 = i32::MIN;
70    let mut r1 = i32::MAX;
71    let mut r2 = i32::MIN;
72
73    for hex in input {
74        q1 = q1.min(hex.q);
75        q2 = q2.max(hex.q);
76        r1 = r1.min(hex.r);
77        r2 = r2.max(hex.r);
78    }
79
80    // Create array with enough space to allow expansion for 100 generations.
81    // 2 * (100 generations + 1 buffer) + Origin = 203 extra in each dimension
82    let width = q2 - q1 + 203;
83    let height = r2 - r1 + 203;
84    let neighbors: [i32; 6] = [-1, 1, -width, width, 1 - width, width - 1];
85    let neighbors: [usize; 6] = from_fn(|i| neighbors[i] as usize);
86
87    let mut active = Vec::with_capacity(5_000);
88    let mut candidates = Vec::with_capacity(5_000);
89    let mut next_active = Vec::with_capacity(5_000);
90
91    // Create initial active state, offsetting tiles so that all indices are positive.
92    for hex in input {
93        let index = width * (hex.r - r1 + 101) + (hex.q - q1 + 101);
94        active.push(index as usize);
95    }
96
97    for _ in 0..100 {
98        let mut state: Vec<u8> = vec![0; (width * height) as usize];
99
100        for &tile in &active {
101            for &offset in &neighbors {
102                // Earlier we converted the offsets from signed `i32` to unsigned `usize`. To
103                // achieve subtraction for negative indices, we use a `wrapping_add` that performs
104                // [two's complement](https://en.wikipedia.org/wiki/Two%27s_complement) arithmetic.
105                let index = tile.wrapping_add(offset);
106                state[index] += 1;
107
108                if state[index] == 2 {
109                    candidates.push(index);
110                }
111            }
112        }
113
114        // Active tiles remain active with both one and two neighbors.
115        for &tile in &active {
116            if state[tile] == 1 {
117                next_active.push(tile);
118            }
119        }
120
121        // Check that the neighbor count for inactive tiles hasn't exceeded two.
122        for &tile in &candidates {
123            if state[tile] == 2 {
124                next_active.push(tile);
125            }
126        }
127
128        // Swap to make next generation the current generation.
129        (active, next_active) = (next_active, active);
130        candidates.clear();
131        next_active.clear();
132    }
133
134    active.len()
135}