Skip to main content

aoc/year2020/
day20.rs

1//! # Jurassic Jigsaw
2//!
3//! At first this seems like a daunting problem. However, a little analysis shows that the input
4//! has some nice properties that make solving this more tractable.
5//!
6//! * Tile edges match with at most one other tile.
7//! * The forward and reverse tile edges form two distinct sets of 312 values with no overlap.
8//!
9//! Tiles can be flipped and rotated for a total of 8 possible permutations each. When parsing
10//! the tiles we store all 8 edge possibilities to enable assembling the jigsaw in part two. For
11//! performance we avoid transforming the inner 8x8 pixels until we have determined the
12//! layout of the grid.
13//!
14//! ## Part One
15//!
16//! First we calculate the frequency of each edge, both forward and backward as tiles can be in
17//! any orientation. As there are only 2¹⁰ or 1024 possible edge values we can use an array instead
18//! of a hash table for speed, converting the edges into a binary number to index the array.
19//!
20//! This results in 96 values that occur once and 528 values that occur twice. Then for every tile
21//! we sum the frequency of each edge. Corner tiles will have two edges that only occur once, not
22//! matching with any other tile, for a total of 1 + 1 + 2 + 2 = 6.
23//!
24//! Other edge tiles have a total of 1 + 2 + 2 + 2 = 7 and inner tiles a total of 2 + 2 + 2 + 2 = 8.
25//!
26//! ## Part Two
27//!
28//! First we arbitrarily pick any corner tile that is oriented so that its unique edges are facing
29//! top and left. Then we proceed row by row, looking up the next tile to the right. Each time
30//! we find a tile we remove it from the remaining tiles, so that looking up a tile is always a
31//! very fast constant time `O(1)` operation.
32//!
33//! The complete picture is stored as an array of `u128` values as the tiles form a square 12 wide,
34//! for a total of 12 × 8 = 96 pixels. As we add each tile, we convert its pixels into a `u8` binary
35//! number and left shift to add to the existing pixels.
36//!
37//! When finding the monsters we make some further assumptions about the input:
38//!
39//! * The monsters will all be oriented the same way.
40//! * Monsters will not overlap with each other.
41//!
42//! For speed the monster bit patterns are rotated and flipped instead of the image, then stored
43//! in hardcoded arrays. The search ends as soon as we find monsters in any orientation.
44use std::array::from_fn;
45
46use crate::util::parse::*;
47
48pub struct Tile {
49    id: u64,
50    top: [usize; 8],
51    left: [usize; 8],
52    bottom: [usize; 8],
53    right: [usize; 8],
54    pixels: [[u8; 10]; 10],
55}
56
57impl Tile {
58    // O = Original
59    // H = Flip horizontal
60    // V = Flip vertical
61    // R = Rotate clockwise 90 degrees
62    // Sequence: [O, H, V, HV, R, RH, RV, RHV]
63    const COEFFICIENTS: [[i32; 6]; 8] = [
64        [1, 0, 1, 0, 1, 1],
65        [-1, 0, 8, 0, 1, 1],
66        [1, 0, 1, 0, -1, 8],
67        [-1, 0, 8, 0, -1, 8],
68        [0, 1, 1, -1, 0, 8],
69        [0, 1, 1, 1, 0, 1],
70        [0, -1, 8, -1, 0, 8],
71        [0, -1, 8, 1, 0, 1],
72    ];
73
74    fn from(chunk: &[&str]) -> Self {
75        let id = chunk[0][5..9].unsigned();
76
77        let pixels: [[_; 10]; 10] = from_fn(|i| chunk[i + 1].as_bytes().try_into().unwrap());
78
79        // The ASCII code for "#" 35 is odd and the code for "." 46 is even
80        // so we can convert to a 1 or 0 bit using bitwise AND with 1.
81        let binary = |row: usize, col: usize| usize::from(pixels[row][col] & 1);
82        let (t, l, b, r) = (0..10).fold((0, 0, 0, 0), |(t, l, b, r), i| {
83            (
84                (t << 1) | binary(0, i),
85                (l << 1) | binary(i, 0),
86                (b << 1) | binary(9, i),
87                (r << 1) | binary(i, 9),
88            )
89        });
90
91        let reverse = |edge: usize| edge.reverse_bits() >> 54;
92        let rt = reverse(t);
93        let rl = reverse(l);
94        let rb = reverse(b);
95        let rr = reverse(r);
96
97        // Same transform sequence as coefficients:
98        // [O, H, V, HV, R, RH, RV, RHV]
99        let top = [t, rt, b, rb, rl, l, rr, r];
100        let left = [l, r, rl, rr, b, t, rb, rt];
101        let bottom = [b, rb, t, rt, rr, r, rl, l];
102        let right = [r, l, rr, rl, t, b, rt, rb];
103
104        Self { id, top, left, bottom, right, pixels }
105    }
106
107    // Coefficients allow us to reuse the loop logic for each of the 8 possible permutations.
108    fn transform(&self, image: &mut [u128], permutation: usize) {
109        let [a, b, c, d, e, f] = Self::COEFFICIENTS[permutation];
110
111        for row in 0..8 {
112            let acc = (0..8).fold(0, |acc, col| {
113                let x = a * col + b * row + c;
114                let y = d * col + e * row + f;
115                let b = self.pixels[y as usize][x as usize];
116                (acc << 1) | (b & 1)
117            });
118
119            image[row as usize] = (image[row as usize] << 8) | u128::from(acc);
120        }
121    }
122}
123
124pub fn parse(input: &str) -> Vec<Tile> {
125    let lines: Vec<_> = input.lines().collect();
126    lines.chunks(12).map(Tile::from).collect()
127}
128
129pub fn part1(input: &[Tile]) -> u64 {
130    let mut freq = [0; 1024];
131
132    for edge in input.iter().flat_map(|t| t.top) {
133        freq[edge] += 1;
134    }
135
136    // Corner tiles have two edges matching no other tile. Any orientation will do, pick the first.
137    input
138        .iter()
139        .filter(|t| freq[t.top[0]] + freq[t.left[0]] + freq[t.bottom[0]] + freq[t.right[0]] == 6)
140        .map(|t| t.id)
141        .product()
142}
143
144pub fn part2(input: &[Tile]) -> u32 {
145    // Store mapping of tile edges to tile index in order to allow
146    // constant time lookup by edge when assembling the jigsaw.
147    let mut edge_to_tile = [[0; 2]; 1024];
148    let mut freq = [0; 1024];
149    let mut placed = [false; 1024];
150
151    for (i, tile) in input.iter().enumerate() {
152        for edge in tile.top {
153            edge_to_tile[edge][freq[edge]] = i;
154            freq[edge] += 1;
155        }
156    }
157
158    let mut next_top = input
159        .iter()
160        .flat_map(|tile| tile.top.iter().zip(&tile.left))
161        .find_map(|(&top, &left)| (freq[top] == 1 && freq[left] == 1).then_some(top))
162        .unwrap();
163    freq[next_top] += 1;
164
165    let mut find_matching_tile = |edge| {
166        let [first, second] = edge_to_tile[edge];
167        let next = if placed[first] { second } else { first };
168        placed[next] = true;
169        &input[next]
170    };
171
172    // Assemble the image.
173    let mut image = [0; 96];
174    let mut index = 0;
175
176    while freq[next_top] == 2 {
177        let tile = find_matching_tile(next_top);
178        let permutation = tile.top.iter().position(|&top| top == next_top).unwrap();
179        tile.transform(&mut image[index..], permutation);
180        next_top = tile.bottom[permutation];
181
182        let mut next_left = tile.right[permutation];
183
184        while freq[next_left] == 2 {
185            let tile = find_matching_tile(next_left);
186            let permutation = tile.left.iter().position(|&left| left == next_left).unwrap();
187            tile.transform(&mut image[index..], permutation);
188            next_left = tile.right[permutation];
189        }
190
191        index += 8;
192    }
193
194    // Common search logic.
195    let sea = image.iter().map(|n| n.count_ones()).sum();
196    let find = |monster: &mut [u128], width: usize, height: usize| {
197        let mut rough = sea;
198
199        for _ in 0..(96 - width + 1) {
200            for window in image.windows(height) {
201                if monster.iter().zip(window).all(|(&mask, &row)| mask & row == mask) {
202                    rough -= 15;
203                }
204            }
205            monster.iter_mut().for_each(|n| *n <<= 1);
206        }
207
208        (rough < sea).then_some(rough)
209    };
210
211    // Transform the monsters instead of the image.
212    // Hardcoded bit patterns for [O, H, V, HV].
213    let mut monsters = [
214        [0b00000000000000000010, 0b10000110000110000111, 0b01001001001001001000],
215        [0b01001001001001001000, 0b10000110000110000111, 0b00000000000000000010],
216        [0b01000000000000000000, 0b11100001100001100001, 0b00010010010010010010],
217        [0b00010010010010010010, 0b11100001100001100001, 0b01000000000000000000],
218    ];
219
220    if let Some(rough) = monsters.iter_mut().find_map(|monster| find(monster, 20, 3)) {
221        return rough;
222    }
223
224    // Hardcoded bit patterns for [R, RH, RV, RHV].
225    let mut monsters = [
226        [2, 4, 0, 0, 4, 2, 2, 4, 0, 0, 4, 2, 2, 4, 0, 0, 4, 2, 3, 2],
227        [2, 3, 2, 4, 0, 0, 4, 2, 2, 4, 0, 0, 4, 2, 2, 4, 0, 0, 4, 2],
228        [2, 1, 0, 0, 1, 2, 2, 1, 0, 0, 1, 2, 2, 1, 0, 0, 1, 2, 6, 2],
229        [2, 6, 2, 1, 0, 0, 1, 2, 2, 1, 0, 0, 1, 2, 2, 1, 0, 0, 1, 2],
230    ];
231
232    monsters.iter_mut().find_map(|monster| find(monster, 3, 20)).unwrap()
233}