Skip to content

Commit 786108b

Browse files
committed
Year 2018 Day 18
1 parent 1472208 commit 786108b

File tree

7 files changed

+190
-0
lines changed

7 files changed

+190
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ Performance is reasonable even on older hardware, for example a 2011 MacBook Pro
254254
| 15 | [Beverage Bandits](https://adventofcode.com/2018/day/15) | [Source](src/year2018/day15.rs) | 584 |
255255
| 16 | [Chronal Classification](https://adventofcode.com/2018/day/16) | [Source](src/year2018/day16.rs) | 36 |
256256
| 17 | [Reservoir Research ](https://adventofcode.com/2018/day/17) | [Source](src/year2018/day17.rs) | 145 |
257+
| 18 | [Settlers of The North Pole](https://adventofcode.com/2018/day/18) | [Source](src/year2018/day18.rs) | 387 |
257258

258259
## 2017
259260

benches/benchmark.rs

+1
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ mod year2018 {
141141
benchmark!(year2018, day15);
142142
benchmark!(year2018, day16);
143143
benchmark!(year2018, day17);
144+
benchmark!(year2018, day18);
144145
}
145146

146147
mod year2019 {

src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ pub mod year2018 {
129129
pub mod day15;
130130
pub mod day16;
131131
pub mod day17;
132+
pub mod day18;
132133
}
133134

134135
/// # Rescue Santa from deep space with a solar system voyage.

src/main.rs

+1
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ fn year2018() -> Vec<Solution> {
197197
solution!(year2018, day15),
198198
solution!(year2018, day16),
199199
solution!(year2018, day17),
200+
solution!(year2018, day18),
200201
]
201202
}
202203

src/year2018/day18.rs

+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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+
}

tests/test.rs

+1
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ mod year2018 {
130130
mod day15_test;
131131
mod day16_test;
132132
mod day17_test;
133+
mod day18_test;
133134
}
134135

135136
mod year2019 {

tests/year2018/day18_test.rs

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#[test]
2+
fn part1_test() {
3+
// No example data
4+
}
5+
6+
#[test]
7+
fn part2_test() {
8+
// No example data
9+
}

0 commit comments

Comments
 (0)