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}