aoc/year2017/
day21.rs

1//! # Fractal Art
2//!
3//! The image size starts at 3x3, growing exponentially to 18x18 after 5 generations and 2187x2187
4//! after 18 generations. The first insight to solving efficiently is realizing that we don't need
5//! to compute the entire image, instead only the *count* of each pattern is needed. Multiplying
6//! the count of each pattern by the number of set bits in each pattern gives the result.
7//!
8//! The second insight is that after 3 generations, the 9x9 image can be split into nine 3x3
9//! images that are independent of each other and the enhancement cycle can start over.
10//! Interestingly most of the 3x3 patterns in the input are not needed, only the starting 3x3
11//! pattern and the six 2x2 to 3x3 patterns.
12//!
13//! Adding a few extra made up rules:
14//!
15//! ```none
16//!     ##/#. => ###/#.#/###
17//!     .#/.# => .#./###/.#.
18//!     ../.. => #.#/.#./#.#
19//! ```
20//!
21//! then using the example:
22//!
23//! ```none
24//!     .#.    #..#    ##.##.    ###|.#.|##.
25//!     ..# => .... => #..#.. => #.#|###|#..
26//!     ###    ....    ......    ###|.#.|...
27//!            #..#    ##.##.    ---+---+---
28//!                    #..#..    .#.|##.|##.
29//!                    ......    ###|#..|#..
30//!                              .#.|...|...
31//!                              ---+---+---
32//!                              ##.|##.|#.#
33//!                              #..|#..|.#.
34//!                              ...|...|#.#
35//! ```
36//!
37//! Splitting the 9x9 grid results in:
38//!
39//! ```none
40//!
41//!     1 x ###    2 x .#.    5 x ##.    1 x #.#
42//!         # #        ###        #..        .#.
43//!         ###        .#.        ...        #.#
44//! ```
45//!
46//! The enhancement cycle can start again with each 3x3 image. This means that we only need to
47//! calculate 2 generations for the starting image and each 2x2 to 3x3 rule.
48struct Pattern {
49    three: u32,
50    four: u32,
51    six: u32,
52    nine: [usize; 9],
53}
54
55pub fn parse(input: &str) -> Vec<u32> {
56    // 2⁴ = 16 possible 2x2 patterns
57    let mut pattern_lookup = [0; 16];
58    let mut two_to_three = [[0; 9]; 16];
59    // 2⁹ = 512 possible 3x3 patterns
60    let mut three_to_four = [[0; 16]; 512];
61
62    // Starting pattern .#./..#/### => 010/001/111 => b010001111 => 143
63    let mut todo = vec![143];
64
65    for line in input.lines().map(str::as_bytes) {
66        // The ASCII code for "#" 35 is odd and the code for "." 46 is even
67        // so we can convert to a 1 or 0 bit using bitwise AND with 1.
68        let bit = |i: usize| line[i] & 1;
69
70        if line.len() == 20 {
71            // 2x2 to 3x3
72            let indices = [0, 1, 3, 4];
73            let from = indices.map(bit);
74
75            let indices = [9, 10, 11, 13, 14, 15, 17, 18, 19];
76            let value = indices.map(bit);
77
78            let pattern = todo.len();
79            todo.push(to_index(&value));
80
81            for key in two_by_two_permutations(from) {
82                two_to_three[key] = value;
83                pattern_lookup[key] = pattern;
84            }
85        } else {
86            // 3x3 to 4x4
87            let indices = [0, 1, 2, 4, 5, 6, 8, 9, 10];
88            let from = indices.map(bit);
89
90            let indices = [15, 16, 17, 18, 20, 21, 22, 23, 25, 26, 27, 28, 30, 31, 32, 33];
91            let value = indices.map(bit);
92
93            for key in three_by_three_permutations(from) {
94                three_to_four[key] = value;
95            }
96        }
97    }
98
99    let patterns: Vec<_> = todo
100        .iter()
101        .map(|&index| {
102            // Lookup 4x4 pattern then map to 6x6
103            let four = three_to_four[index];
104            let mut six = [0; 36];
105
106            for (src, dst) in [(0, 0), (2, 3), (8, 18), (10, 21)] {
107                let index = to_index(&[four[src], four[src + 1], four[src + 4], four[src + 5]]);
108                let replacement = two_to_three[index];
109                six[dst..dst + 3].copy_from_slice(&replacement[0..3]);
110                six[dst + 6..dst + 9].copy_from_slice(&replacement[3..6]);
111                six[dst + 12..dst + 15].copy_from_slice(&replacement[6..9]);
112            }
113
114            // Map 6x6 pattern to nine 3x3 patterns.
115            let nine = [0, 2, 4, 12, 14, 16, 24, 26, 28].map(|i| {
116                let index = to_index(&[six[i], six[i + 1], six[i + 6], six[i + 7]]);
117                pattern_lookup[index]
118            });
119
120            let three = index.count_ones();
121            let four = four.iter().sum::<u8>() as u32;
122            let six = six.iter().sum::<u8>() as u32;
123
124            Pattern { three, four, six, nine }
125        })
126        .collect();
127
128    let mut current = vec![0; patterns.len()];
129    let mut result = Vec::new();
130
131    // Begin with single starting pattern
132    current[0] = 1;
133
134    // Calculate generations 0 to 20 inclusive.
135    for _ in 0..7 {
136        let mut three = 0;
137        let mut four = 0;
138        let mut six = 0;
139        let mut next = vec![0; patterns.len()];
140
141        for (count, pattern) in current.iter().zip(patterns.iter()) {
142            three += count * pattern.three;
143            four += count * pattern.four;
144            six += count * pattern.six;
145            // Each 6x6 grid splits into nine 3x3 grids.
146            pattern.nine.iter().for_each(|&i| next[i] += count);
147        }
148
149        result.push(three);
150        result.push(four);
151        result.push(six);
152        current = next;
153    }
154
155    result
156}
157
158pub fn part1(input: &[u32]) -> u32 {
159    input[5]
160}
161
162pub fn part2(input: &[u32]) -> u32 {
163    input[18]
164}
165
166/// Generate an array of the 8 possible transformations possible from rotating and flipping
167/// the 2x2 input.
168fn two_by_two_permutations(mut a: [u8; 4]) -> [usize; 8] {
169    let mut indices = [0; 8];
170
171    for (i, index) in indices.iter_mut().enumerate() {
172        // Convert pattern to binary to use as lookup index.
173        *index = to_index(&a);
174        // Rotate clockwise
175        // 0 1 => 2 0
176        // 2 3    3 1
177        a = [a[2], a[0], a[3], a[1]];
178        // Flip vertical
179        // 0 1 => 2 3
180        // 2 3    0 1
181        if i == 3 {
182            a = [a[2], a[3], a[0], a[1]];
183        }
184    }
185
186    indices
187}
188
189/// Generate an array of the 8 possible transformations possible from rotating and flipping
190/// the 3x3 input.
191fn three_by_three_permutations(mut a: [u8; 9]) -> [usize; 8] {
192    let mut indices = [0; 8];
193
194    for (i, index) in indices.iter_mut().enumerate() {
195        // Convert pattern to binary to use as lookup index.
196        *index = to_index(&a);
197        // Rotate clockwise
198        // 0 1 2 => 6 3 0
199        // 3 4 5    7 4 1
200        // 6 7 8    8 5 2
201        a = [a[6], a[3], a[0], a[7], a[4], a[1], a[8], a[5], a[2]];
202        // Flip vertical
203        // 0 1 2 => 6 7 8
204        // 3 4 5    3 4 5
205        // 6 7 8    0 1 2
206        if i == 3 {
207            a = [a[6], a[7], a[8], a[3], a[4], a[5], a[0], a[1], a[2]];
208        }
209    }
210
211    indices
212}
213
214/// Convert a pattern slice of ones and zeroes to a binary number.
215fn to_index(a: &[u8]) -> usize {
216    a.iter().fold(0, |acc, &n| (acc << 1) | n as usize)
217}