aoc/year2018/
day12.rs

1//! # Subterranean Sustainability
2//!
3//! The problem is a one-dimensional version of
4//! [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life).
5//!
6//! We use a vector to store which pots are occupied and which are empty in each generation.
7//! When calculating the next step, we truncate the bit vector on the left and right.
8//! This makes it easier to compare generations in part two.
9//!
10//! The trick for part two is that the plants will eventually stabilize into a stable pattern
11//! (similar to a [glider](https://en.wikipedia.org/wiki/Glider_(Conway%27s_Game_of_Life)))
12//! that moves by the same amount each generation. Once two subsequent generations are the same,
13//! except for the starting position, we extrapolate 50 billion generations into the future.
14use std::iter::repeat_n;
15use std::mem::swap;
16
17type Input = (i64, i64);
18
19pub fn parse(input: &str) -> Input {
20    // Parse initial state.
21    let (prefix, suffix) = input.split_once("\n\n").unwrap();
22    let mut pots = Pots::from(&prefix.as_bytes()[15..]);
23
24    // Parse rules into a table with all possible 2⁵=32 patterns.
25    let mut rules = [0; 32];
26    for line in suffix.lines().map(str::as_bytes) {
27        if line[9] == b'#' {
28            let binary = (0..5).fold(0, |acc, i| (acc << 1) | usize::from(line[i] == b'#'));
29            rules[binary] = 1;
30        }
31    }
32
33    // Part one - Simulate the first 20 steps.
34    for _ in 0..20 {
35        pots.step(&rules);
36    }
37    let part_one = pots.sum();
38
39    // Part two - Only simulate until the generation repeats.
40    for steps in 20.. {
41        let prev_pos = pots.pos;
42        pots.step(&rules);
43        if pots.state == pots.prev_state {
44            // Generation has repeated - extrapolate to 50 billion steps.
45            pots.pos += (pots.pos - prev_pos) * (50_000_000_000 - steps - 1);
46            break;
47        }
48    }
49
50    let part_two = pots.sum();
51    (part_one, part_two)
52}
53
54pub fn part1(input: &Input) -> i64 {
55    input.0
56}
57
58pub fn part2(input: &Input) -> i64 {
59    input.1
60}
61
62struct Pots {
63    /// Vector representing the pots. 1 means there is a plant in the pot, 0 means there isn't.
64    state: Vec<u8>,
65    /// A copy of the vector `state` before [`Self::step`] was called.
66    prev_state: Vec<u8>,
67    /// The id of the pot at the beginning of the bit vector `state`.
68    pos: i64,
69}
70
71impl Pots {
72    /// Parses the initial state into a bit vector.
73    fn from(initial_state: &[u8]) -> Self {
74        let state: Vec<_> = initial_state.iter().map(|&b| u8::from(b == b'#')).collect();
75        Self { state, prev_state: Vec::new(), pos: 0 }
76    }
77
78    /// Applies the given rules to the pots and updates [`Self::state`]. A copy of the state before
79    /// this method was called is left in [`Self::prev_state`].
80    fn step(&mut self, rules: &[u8; 32]) {
81        // Prepare new state.
82        swap(&mut self.state, &mut self.prev_state);
83        self.state.clear();
84
85        let start = self.prev_state.iter().position(|&b| b == 1).unwrap();
86        let end = self.prev_state.iter().rposition(|&b| b == 1).unwrap();
87        let mut mask = 0;
88
89        // Apply rules and build up new state.
90        // Pad zeros onto the end to make handling next state easier.
91        for b in self.prev_state[start..=end].iter().copied().chain(repeat_n(0, 4)) {
92            mask = ((mask << 1) | b as usize) & 0b11111;
93            self.state.push(rules[mask]);
94        }
95
96        // Update start position.
97        self.pos += start as i64 - 2;
98    }
99
100    /// Returns the sum of the numbers of all pots containing plants.
101    fn sum(&self) -> i64 {
102        self.state.iter().enumerate().map(|(i, &s)| (self.pos + i as i64) * s as i64).sum()
103    }
104}