Skip to main content

aoc/year2017/
day22.rs

1//! # Sporifica Virus
2//!
3//! Part two is made faster by a factor of two by packing 4 nodes into each byte using
4//! 2 bits per node. Then multiple steps are memoized for each of the 256 possible states,
5//! for each of the 4 positions and each of the 4 directions, for a total of 4,096 combinations.
6//! This allows us to skip forward up to 8 steps at a time. For example:
7//!
8//! ```none
9//! . = Clean   # = Infected   F = Flagged   W = Weakened
10//!
11//! State    Direction    Steps    Infected
12//! [W] #    Down         0        0
13//!  F  W
14//!
15//!  #  #    Down         1        1
16//! [F] W
17//!
18//! [#] #    Up           2        1
19//!  .  W
20//!
21//!  F [#]   Right        3        1
22//!  .  W
23//!
24//!  F  F    Down         4        1
25//!  . [W]
26//!
27//!  F  F    Down         5        2
28//!  .  #
29//!    [ ]
30//! ```
31//!
32//! Starting in the top-left corner facing down, after 5 steps the virus carrier leaves the 2x2
33//! block having infected 2 nodes. This is memoized as:
34//!
35//! ```none
36//! [0][2][01111001] => (2, 5, 10001111)
37//! ```
38use std::array::from_fn;
39use std::mem::take;
40
41use crate::util::grid::*;
42use crate::util::point::*;
43
44const SIZE: usize = 250;
45const HALF: usize = SIZE / 2;
46const CENTER: usize = SIZE * HALF + HALF;
47
48pub fn parse(input: &str) -> Grid<u8> {
49    Grid::parse(input)
50}
51
52/// Direct implementation on a fixed-size grid.
53pub fn part1(input: &Grid<u8>) -> u32 {
54    let size = SIZE as i32;
55    let center = Point::new(size, size);
56    let offset = center - Point::new(input.width / 2, input.height / 2);
57
58    // Assume the virus carrier will never leave a 500 x 500 grid, starting at the center.
59    let mut grid = Grid::new(2 * size, 2 * size, false);
60    let mut position = center;
61    let mut direction = UP;
62    let mut infected = 0;
63
64    // Copy the smaller initial input grid to the center of the larger grid.
65    for y in 0..input.height {
66        for x in 0..input.width {
67            let point = Point::new(x, y);
68            grid[point + offset] = input[point] == b'#';
69        }
70    }
71
72    // The grid toggles between clean and infected.
73    for _ in 0..10_000 {
74        direction = if grid[position] {
75            direction.clockwise()
76        } else {
77            infected += 1;
78            direction.counter_clockwise()
79        };
80        grid[position] = !grid[position];
81        position += direction;
82    }
83
84    infected
85}
86
87/// Use a compressed grid where each byte stores 4 cells (2x2 block) with 2 bits per cell.
88pub fn part2(input: &Grid<u8>) -> usize {
89    // Assume that the carrier will never go outside the range 0 to 500 in both x and y axes
90    // starting at the center. As we store 4 nodes per byte, we compress the x and y axes by two.
91    let mut grid = vec![0; SIZE * SIZE];
92
93    // Precompute all 4 * 4 * 256 possible state transitions for faster simulation.
94    let cache: [[[_; 256]; 4]; 4] = from_fn(|quadrant| {
95        from_fn(|direction| from_fn(|state| compute_block(&mut grid, quadrant, direction, state)))
96    });
97
98    // Copy the smaller initial input grid to the center of the larger grid,
99    // packing 4 nodes into each byte.
100    let offset = SIZE - (input.width / 2) as usize;
101
102    for y in 0..input.height {
103        for x in 0..input.width {
104            if input[Point::new(x, y)] == b'#' {
105                let (adjusted_x, adjusted_y) = (x as usize + offset, y as usize + offset);
106                let index = SIZE * (adjusted_y / 2) + (adjusted_x / 2);
107                let offset = 4 * (adjusted_y % 2) + 2 * (adjusted_x % 2);
108                // Mark node as infected.
109                grid[index] |= 2 << offset;
110            }
111        }
112    }
113
114    // Start in the center of the grid, in the top-left corner of a 2x2 cell, facing up.
115    let mut index = CENTER;
116    let mut quadrant = 0; // Top-left corner
117    let mut direction = 0; // Facing up
118    let mut infected = 0;
119    let mut remaining = 10_000_000;
120
121    // Memoized blocks can combine up to 8 steps. Handle the last few steps individually to
122    // prevent overshooting the step target and overcounting the infected node transitions.
123    while remaining > 8 {
124        let state = grid[index] as usize;
125        let packed = cache[quadrant][direction][state];
126
127        // With 10 million repetitions, saving time inside this hot loop is essential.
128        // By bit-packing 6 fields into a single `u32`, we limit the size of the array to 16kB
129        // making sure that it fits into L1 cache.
130        grid[index] = packed as u8; // bits 0-7
131        index = index + (packed >> 20) as usize - SIZE; // bits 20-31
132        quadrant = ((packed >> 8) % 4) as usize; // bits 8-9
133        direction = ((packed >> 10) % 4) as usize; // bits 10-11
134        infected += ((packed >> 12) % 16) as usize; // bits 12-15
135        remaining -= ((packed >> 16) % 16) as usize; // bits 16-19
136    }
137
138    // Handle up to 8 remaining steps individually to prevent overcounting.
139    for _ in 0..remaining {
140        let delta;
141        [index, quadrant, direction, delta] = step(&mut grid, index, quadrant, direction);
142        infected += delta;
143    }
144
145    infected
146}
147
148/// Computes the number of steps taken, infected nodes and next location for 2 x 2 blocks of nodes.
149#[inline]
150fn compute_block(grid: &mut [u8], mut quadrant: usize, mut direction: usize, state: usize) -> u32 {
151    let mut index = CENTER;
152    let mut infected = 0;
153    let mut steps = 0;
154
155    // Temporarily use the grid. This allows the index to move without exceeding bounds.
156    grid[CENTER] = state as u8;
157
158    // Count steps and infected nodes until we leave this cell.
159    while index == CENTER {
160        let delta;
161        [index, quadrant, direction, delta] = step(grid, index, quadrant, direction);
162        infected += delta;
163        steps += 1;
164    }
165
166    // Reset the grid to zero and figure out the next index. We offset index by SIZE to keep the
167    // value positive for easier bit manipulation.
168    let next_state = take(&mut grid[CENTER]);
169    let next_index = index + SIZE - CENTER;
170
171    // Pack six fields into a single `u32`, maximizing cache locality by minimizing space.
172    next_state as u32
173        | (quadrant << 8) as u32
174        | (direction << 10) as u32
175        | (infected << 12) as u32
176        | (steps << 16)
177        | (next_index << 20) as u32
178}
179
180/// Process a single step in any arbitrary location on the grid.
181#[inline]
182fn step(grid: &mut [u8], index: usize, quadrant: usize, direction: usize) -> [usize; 4] {
183    // 4 nodes are packed into a single byte with quadrants arranged as:
184    // [ 0 1 ]
185    // [ 2 3 ]
186    let shift = 2 * quadrant;
187    let node = (grid[index] >> shift) % 4;
188
189    // Nodes cycle between 4 possible values:
190    // 0 = Clean, 1 = Weakened, 2 = Infected, 3 = Flagged
191    let next_node = (node + 1) % 4;
192    // Direction changes based on the *previous* value of the node. In clockwise order:
193    // 0 = Up, 1 = Right, 2 = Down, 3 = Left
194    let next_direction = (direction + node as usize + 3) % 4;
195
196    // Update the 2 bits representing the current node.
197    let mask = !(0b11 << shift);
198    grid[index] = (grid[index] & mask) | (next_node << shift);
199
200    // Calculate x and y coordinates as if a single node was stored in each cell.
201    // This is used in the next step in order to calculate if the index has changed.
202    let (x, y) = (2 * (index % SIZE) + quadrant % 2, 2 * (index / SIZE) + quadrant / 2);
203    let (x, y) = match next_direction {
204        0 => (x, y - 1),
205        1 => (x + 1, y),
206        2 => (x, y + 1),
207        _ => (x - 1, y),
208    };
209
210    // Convert the x and y coordinates back into the compressed values for 2 x 2 nodes in each cell.
211    let next_index = SIZE * (y / 2) + (x / 2);
212    let next_quadrant = 2 * (y % 2) + (x % 2);
213    let infected = usize::from(next_node == 2);
214
215    [next_index, next_quadrant, next_direction, infected]
216}