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    let lines: Vec<_> = input.lines().map(str::as_bytes).collect();
21
22    // Parse initial state
23    let initial_state = &lines[0][15..];
24    let mut pots = Pots::from(initial_state);
25
26    // Parse rules into a table with all possible 2⁵=32 patterns
27    let mut rules = [0; 32];
28    for line in &lines[2..] {
29        if line[9] == b'#' {
30            let binary = (0..5).fold(0, |acc, i| (acc << 1) | usize::from(line[i] == b'#'));
31            rules[binary] = 1;
32        }
33    }
34
35    // Part 1 - Simulate the first 20 steps
36    for _ in 0..20 {
37        pots.step(&rules);
38    }
39    let part_one = pots.sum();
40
41    // Part 2 - Only simulate until the generation repeats
42    let mut prev_pos;
43    for steps in 20.. {
44        prev_pos = pots.pos;
45        pots.step(&rules);
46        if pots.state == pots.prev_state {
47            // Generation has repeated - extrapolate to 50 billion steps
48            pots.pos += (pots.pos - prev_pos) * (50_000_000_000 - steps - 1);
49            break;
50        }
51    }
52
53    let part_two = pots.sum();
54    (part_one, part_two)
55}
56
57pub fn part1(input: &Input) -> i64 {
58    input.0
59}
60
61pub fn part2(input: &Input) -> i64 {
62    input.1
63}
64
65struct Pots {
66    /// Vector representing the pots. 1 means there is a plant in the pot, 0 means there isn't.
67    state: Vec<u8>,
68    /// A copy of the vector `state` before [`Self::step`] was called.
69    prev_state: Vec<u8>,
70    /// The id of the pot at the beginning of the bit vector `state`.
71    pos: i64,
72}
73
74impl Pots {
75    /// Parses the initial state into a bit vector
76    fn from(initial_state: &[u8]) -> Self {
77        let state: Vec<_> = initial_state.iter().map(|&b| u8::from(b == b'#')).collect();
78        Self { state, prev_state: Vec::new(), pos: 0 }
79    }
80
81    /// Applies the given rules to the pots and updates [`Self::state`]. A copy of the state before
82    /// this method was called is left in [`Self::prev_state`].
83    fn step(&mut self, rules: &[u8; 32]) {
84        // Prepare new state
85        swap(&mut self.state, &mut self.prev_state);
86        self.state.clear();
87
88        let start = self.prev_state.iter().position(|&b| b == 1).unwrap();
89        let end = self.prev_state.iter().rposition(|&b| b == 1).unwrap();
90        let mut mask = 0;
91
92        // Apply rules and built up new state.
93        // Pad zeros onto the end to make handling next state easier.
94        for b in self.prev_state[start..=end].iter().copied().chain(repeat_n(0, 4)) {
95            mask = ((mask << 1) | b as usize) & 0b11111;
96            self.state.push(rules[mask]);
97        }
98
99        // Update start position.
100        self.pos += start as i64 - 2;
101    }
102
103    /// Returns the sum of the numbers of all pots containing plants
104    fn sum(&self) -> i64 {
105        self.state.iter().enumerate().map(|(i, &s)| (self.pos + i as i64) * s as i64).sum()
106    }
107}