|
| 1 | +//! # Settlers of The North Pole |
| 2 | +//! |
| 3 | +//! This problem is a cellular automaton similar to the well known |
| 4 | +//! [Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life). To solve part two |
| 5 | +//! we look for a [cycle](https://en.wikipedia.org/wiki/Cycle_detection) then |
| 6 | +//! extrapolate forward a billion generations. |
| 7 | +//! |
| 8 | +//! To efficiently compute the next generation a [SWAR](https://en.wikipedia.org/wiki/SWAR) |
| 9 | +//! approach is used. The count of trees and lumberyards is packed into a `u64` so that we can |
| 10 | +//! process 8 acres at a time. Lumberyards are stored in the high nibble of each byte |
| 11 | +//! and trees in the low nibble. For example: |
| 12 | +//! |
| 13 | +//! ```none |
| 14 | +//! .#.#...| |
| 15 | +//! .....#|# => 11 11 21 11 21 02 21 02 => 0x1111211121022102 |
| 16 | +//! .|..|... |
| 17 | +//! ``` |
| 18 | +//! |
| 19 | +//! The total number of adjacent trees or lumberyards is then calculated in two passes. |
| 20 | +//! First the horizontal sum of each row is computed by bit shifting left and right by 8. |
| 21 | +//! Then the vertical sum of 3 horizontal sums gives the total. |
| 22 | +//! |
| 23 | +//! Bitwise logic then computes the next generation in batches of 8 acres at a time. |
| 24 | +use crate::util::hash::*; |
| 25 | +use std::hash::{Hash, Hasher}; |
| 26 | + |
| 27 | +/// Bitwise logic galore. |
| 28 | +const OPEN: u64 = 0x00; |
| 29 | +const TREE: u64 = 0x01; |
| 30 | +const LUMBERYARD: u64 = 0x10; |
| 31 | +const EDGE: u64 = 0xffff000000000000; |
| 32 | +const LOWER: u64 = 0x0f0f0f0f0f0f0f0f; |
| 33 | +const UPPER: u64 = 0xf0f0f0f0f0f0f0f0; |
| 34 | +const THIRTEENS: u64 = 0x0d0d0d0d0d0d0d0d; |
| 35 | +const FIFTEENS: u64 = 0x0f0f0f0f0f0f0f0f; |
| 36 | + |
| 37 | +/// New type wrapper so that we can use a custom hash function. |
| 38 | +#[derive(PartialEq, Eq)] |
| 39 | +pub struct Key { |
| 40 | + area: [u64; 350], |
| 41 | +} |
| 42 | + |
| 43 | +/// Hash only two cells as a reasonable tradeoff between speed and collision resistance. |
| 44 | +impl Hash for Key { |
| 45 | + fn hash<H: Hasher>(&self, state: &mut H) { |
| 46 | + self.area[100].hash(state); |
| 47 | + self.area[200].hash(state); |
| 48 | + } |
| 49 | +} |
| 50 | + |
| 51 | +/// Pack the input into an array of `u64`. |
| 52 | +/// The input is 50 acres wide, so requires `ceil(50 / 8) = 7` elements for each row. |
| 53 | +pub fn parse(input: &str) -> Key { |
| 54 | + let mut area = [0; 350]; |
| 55 | + |
| 56 | + for (y, line) in input.lines().map(str::as_bytes).enumerate() { |
| 57 | + for (x, byte) in line.iter().enumerate() { |
| 58 | + let acre = match byte { |
| 59 | + b'|' => TREE, |
| 60 | + b'#' => LUMBERYARD, |
| 61 | + _ => OPEN, |
| 62 | + }; |
| 63 | + let index = (y * 7) + (x / 8); |
| 64 | + let offset = 56 - 8 * (x % 8); |
| 65 | + area[index] |= acre << offset; |
| 66 | + } |
| 67 | + } |
| 68 | + |
| 69 | + Key { area } |
| 70 | +} |
| 71 | + |
| 72 | +/// Compute 10 generations. |
| 73 | +pub fn part1(input: &Key) -> u32 { |
| 74 | + let mut area = input.area; |
| 75 | + let mut rows = [0; 364]; |
| 76 | + |
| 77 | + for _ in 0..10 { |
| 78 | + step(&mut area, &mut rows); |
| 79 | + } |
| 80 | + |
| 81 | + resource_value(&area) |
| 82 | +} |
| 83 | + |
| 84 | +/// Compute generations until a cycle is detected. |
| 85 | +pub fn part2(input: &Key) -> u32 { |
| 86 | + let mut area = input.area; |
| 87 | + let mut rows = [0; 364]; |
| 88 | + let mut seen = FastMap::with_capacity(1_000); |
| 89 | + |
| 90 | + for minute in 1.. { |
| 91 | + step(&mut area, &mut rows); |
| 92 | + |
| 93 | + if let Some(previous) = seen.insert(Key { area }, minute) { |
| 94 | + // Find the index of the state after 1 billion repetitions. |
| 95 | + let offset = 1_000_000_000 - previous; |
| 96 | + let cycle_width = minute - previous; |
| 97 | + let remainder = offset % cycle_width; |
| 98 | + let target = previous + remainder; |
| 99 | + |
| 100 | + let (result, _) = seen.iter().find(|(_, &i)| i == target).unwrap(); |
| 101 | + return resource_value(&result.area); |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + unreachable!() |
| 106 | +} |
| 107 | + |
| 108 | +fn step(area: &mut [u64], rows: &mut [u64]) { |
| 109 | + // Compute the horizontal sum of each column with its immediate neighbors. |
| 110 | + for y in 0..50 { |
| 111 | + // Shadow slices at correct starting offset for convenience. We pad `rows` on the top and |
| 112 | + // bottom then shift index by 7 to avoid having to check for edge conditions. |
| 113 | + let area = &area[7 * y..]; |
| 114 | + let rows = &mut rows[7 * (y + 1)..]; |
| 115 | + |
| 116 | + rows[0] = horizontal_sum(0, area[0], area[1]); |
| 117 | + rows[1] = horizontal_sum(area[0], area[1], area[2]); |
| 118 | + rows[2] = horizontal_sum(area[1], area[2], area[3]); |
| 119 | + rows[3] = horizontal_sum(area[2], area[3], area[4]); |
| 120 | + rows[4] = horizontal_sum(area[3], area[4], area[5]); |
| 121 | + rows[5] = horizontal_sum(area[4], area[5], area[6]); |
| 122 | + rows[6] = horizontal_sum(area[5], area[6], 0); |
| 123 | + |
| 124 | + // The grid is 50 wide so the last 6 bytes in each row are unused and must be set to zero. |
| 125 | + rows[6] &= EDGE; |
| 126 | + } |
| 127 | + |
| 128 | + for i in 0..350 { |
| 129 | + // Sum of all adjacent trees and lumberyards, not including center acre. |
| 130 | + let acre = area[i]; |
| 131 | + let sum = rows[i] + rows[i + 7] + rows[i + 14] - acre; |
| 132 | + |
| 133 | + // Add 13 so that any values 3 and higher overflow into high nibble. |
| 134 | + let mut to_tree = (sum & LOWER) + THIRTEENS; |
| 135 | + // Clear low nibble as this is irrelevant. |
| 136 | + to_tree &= UPPER; |
| 137 | + // To become a tree, we must be open space. |
| 138 | + to_tree &= !(acre | (acre << 4)); |
| 139 | + // Shift result back to low nibble. |
| 140 | + to_tree >>= 4; |
| 141 | + |
| 142 | + // Check for any values 3 or higher. |
| 143 | + let mut to_lumberyard = ((sum >> 4) & LOWER) + THIRTEENS; |
| 144 | + // Clear low nibble. |
| 145 | + to_lumberyard &= UPPER; |
| 146 | + // To become a lumberyard, we must already be a tree. |
| 147 | + to_lumberyard &= acre << 4; |
| 148 | + // Spread result to both high and low nibble. We will later XOR this to flip correct bits. |
| 149 | + to_lumberyard |= to_lumberyard >> 4; |
| 150 | + |
| 151 | + // We must be a lumberyard. |
| 152 | + let mut to_open = acre & UPPER; |
| 153 | + // Check for at least one adjacent tree. |
| 154 | + to_open &= (sum & LOWER) + FIFTEENS; |
| 155 | + // Check for at least one adjacent lumberyard. |
| 156 | + to_open &= ((sum >> 4) & LOWER) + FIFTEENS; |
| 157 | + // Flip bit as we will later XOR. |
| 158 | + to_open ^= acre & UPPER; |
| 159 | + |
| 160 | + // Flip relevant bits to transition to next state. |
| 161 | + area[i] = acre ^ (to_tree | to_lumberyard | to_open); |
| 162 | + } |
| 163 | +} |
| 164 | + |
| 165 | +/// Convenience method that also takes correct byte from left and right neighbors. |
| 166 | +#[inline] |
| 167 | +fn horizontal_sum(left: u64, middle: u64, right: u64) -> u64 { |
| 168 | + (left << 56) + (middle >> 8) + middle + (middle << 8) + (right >> 56) |
| 169 | +} |
| 170 | + |
| 171 | +/// Each tree or lumberyard is represented by a single bit. |
| 172 | +fn resource_value(area: &[u64]) -> u32 { |
| 173 | + let trees: u32 = area.iter().map(|n| (n & LOWER).count_ones()).sum(); |
| 174 | + let lumberyards: u32 = area.iter().map(|n| (n & UPPER).count_ones()).sum(); |
| 175 | + trees * lumberyards |
| 176 | +} |
0 commit comments