aoc/year2020/
day20.rs

1//! # Jurassic Jigsaw
2//!
3//! At first this seems like a daunting problem. However a little anaylsis shows that the input
4//! has some nice properties that makes 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//!
15//! ## Part One
16//!
17//! First we calculate the frequency of each edge, both forwards and backwards as tiles can be in
18//! any orientation. As there only 2¹⁰ or 1024 possible edge values we can use an array intead of a
19//! hashtable for speed, converting the edges into a binary number to index the array.
20//!
21//! This results in 96 values that occur once and 528 values that occur twice. Then for every tile
22//! we sum the frequency of each edge. Corner tiles will have two edges that only occur once, not
23//! matching with any other tile, for a total of 1 + 1 + 2 + 2 = 6.
24//!
25//! Other edge tiles have a total of 1 + 2 + 2 + 2 = 7 and inner tiles a total of 2 + 2 + 2 + 2 = 8.
26//!
27//! ## Part Two
28//!
29//! First we arbitrarily pick any corner tile that is oriented so that its unique edges are facing
30//! top and left. Then we proceed row by row, looking up the next tile to the right. Each time
31//! we find a tile we remove it from the remaining tiles, so that looking up a tile is always a
32//! very fast constant time `O(1)` operation.
33//!
34//! The complete picture is stored as an array of `u128` values as the tiles form a square 12 wide,
35//! for a total of 12 * 8 = 96 pixels. As we add each tile, we convert its pixels into a `u8` binary
36//! number and left shift to add to the existing pixels.
37//!
38//! When finding the monsters we make some further assumptions about the input:
39//!
40//! * The monsters will all be oriented the same way
41//! * Monsters will not overlap with each other
42//!
43//! For speed the monster bit patterns are rotated and flipped instead of the image, then stored
44//! in hardcoded arrays. The search ends as soon as we find monsters in any orientation.
45use crate::util::parse::*;
46use std::array::from_fn;
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]) -> Tile {
75        let id = (&chunk[0][5..9]).unsigned();
76
77        let pixels: [[u8; 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| (pixels[row][col] & 1) as usize;
82        let mut t = 0;
83        let mut l = 0;
84        let mut b = 0;
85        let mut r = 0;
86
87        for i in 0..10 {
88            t = (t << 1) | binary(0, i);
89            l = (l << 1) | binary(i, 0);
90            b = (b << 1) | binary(9, i);
91            r = (r << 1) | binary(i, 9);
92        }
93
94        let reverse = |edge: usize| edge.reverse_bits() >> 54;
95        let rt = reverse(t);
96        let rl = reverse(l);
97        let rb = reverse(b);
98        let rr = reverse(r);
99
100        // Same transform sequence as coefficients:
101        // [O, H, V, HV, R, RH, RV, RHV]
102        let top = [t, rt, b, rb, rl, l, rr, r];
103        let left = [l, r, rl, rr, b, t, rb, rt];
104        let bottom = [b, rb, t, rt, rr, r, rl, l];
105        let right = [r, l, rr, rl, t, b, rt, rb];
106
107        Tile { id, top, left, bottom, right, pixels }
108    }
109
110    // Coefficients allow us to reuse the loop logic for each of the 8 possible permutations.
111    fn transform(&self, image: &mut [u128], permutation: usize) {
112        let [a, b, c, d, e, f] = Self::COEFFICIENTS[permutation];
113
114        for row in 0..8 {
115            let mut acc = 0;
116
117            for col in 0..8 {
118                let x = a * col + b * row + c;
119                let y = d * col + e * row + f;
120                let b = self.pixels[y as usize][x as usize];
121                acc = (acc << 1) | (b & 1);
122            }
123
124            image[row as usize] = (image[row as usize] << 8) | (acc as u128);
125        }
126    }
127}
128
129pub fn parse(input: &str) -> Vec<Tile> {
130    let lines: Vec<_> = input.lines().collect();
131    lines.chunks(12).map(Tile::from).collect()
132}
133
134pub fn part1(input: &[Tile]) -> u64 {
135    let mut frequency = [0; 1024];
136    let mut result = 1;
137
138    for tile in input {
139        for edge in tile.top {
140            frequency[edge] += 1;
141        }
142    }
143
144    for tile in input {
145        // Any orientation will do, pick the first.
146        let total = frequency[tile.top[0]]
147            + frequency[tile.left[0]]
148            + frequency[tile.bottom[0]]
149            + frequency[tile.right[0]];
150        if total == 6 {
151            result *= tile.id;
152        }
153    }
154
155    result
156}
157
158pub fn part2(input: &[Tile]) -> u32 {
159    // Store mapping of tile edges to tile index in order to allow
160    // constant time lookup by edge when assembling the jigsaw.
161    let mut edge_to_tile = [[0; 2]; 1024];
162    let mut frequency = [0; 1024];
163    let mut placed = [false; 1024];
164
165    for (i, tile) in input.iter().enumerate() {
166        for edge in tile.top {
167            edge_to_tile[edge][frequency[edge]] = i;
168            frequency[edge] += 1;
169        }
170    }
171
172    let mut find_arbitrary_corner = || {
173        for tile in input {
174            for j in 0..8 {
175                if frequency[tile.top[j]] == 1 && frequency[tile.left[j]] == 1 {
176                    frequency[tile.top[j]] += 1;
177                    return tile.top[j];
178                }
179            }
180        }
181        unreachable!();
182    };
183    let mut find_matching_tile = |edge: usize| {
184        let [first, second] = edge_to_tile[edge];
185        let next = if placed[first] { second } else { first };
186        placed[next] = true;
187        &input[next]
188    };
189
190    // Assemble the image
191    let mut next_top = find_arbitrary_corner();
192    let mut image = [0; 96];
193    let mut index = 0;
194
195    while frequency[next_top] == 2 {
196        let tile = find_matching_tile(next_top);
197        let permutation = (0..8).position(|i| tile.top[i] == next_top).unwrap();
198        tile.transform(&mut image[index..], permutation);
199        next_top = tile.bottom[permutation];
200
201        let mut next_left = tile.right[permutation];
202
203        while frequency[next_left] == 2 {
204            let tile = find_matching_tile(next_left);
205            let permutation = (0..8).position(|i| tile.left[i] == next_left).unwrap();
206            tile.transform(&mut image[index..], permutation);
207            next_left = tile.right[permutation];
208        }
209
210        index += 8;
211    }
212
213    // Common search logic
214    let sea: u32 = image.iter().map(|n| n.count_ones()).sum();
215    let find = |monster: &mut [u128], width: usize, height: usize| {
216        let mut rough = sea;
217
218        for _ in 0..(96 - width + 1) {
219            for window in image.windows(height) {
220                if monster.iter().enumerate().all(|(i, &n)| n & window[i] == n) {
221                    rough -= 15;
222                }
223            }
224            monster.iter_mut().for_each(|n| *n <<= 1);
225        }
226
227        (rough < sea).then_some(rough)
228    };
229
230    // Transform the monsters instead of the image.
231    // Hardcoded bit patterns for [O, H, V, HV].
232    let mut monsters = [
233        [0b00000000000000000010, 0b10000110000110000111, 0b01001001001001001000],
234        [0b01001001001001001000, 0b10000110000110000111, 0b00000000000000000010],
235        [0b01000000000000000000, 0b11100001100001100001, 0b00010010010010010010],
236        [0b00010010010010010010, 0b11100001100001100001, 0b01000000000000000000],
237    ];
238
239    for monster in &mut monsters {
240        if let Some(rough) = find(monster, 20, 3) {
241            return rough;
242        }
243    }
244
245    // Hardcoded bit patterns [R, RH, RV, RHV].
246    let mut monsters = [
247        [2, 4, 0, 0, 4, 2, 2, 4, 0, 0, 4, 2, 2, 4, 0, 0, 4, 2, 3, 2],
248        [2, 3, 2, 4, 0, 0, 4, 2, 2, 4, 0, 0, 4, 2, 2, 4, 0, 0, 4, 2],
249        [2, 1, 0, 0, 1, 2, 2, 1, 0, 0, 1, 2, 2, 1, 0, 0, 1, 2, 6, 2],
250        [2, 6, 2, 1, 0, 0, 1, 2, 2, 1, 0, 0, 1, 2, 2, 1, 0, 0, 1, 2],
251    ];
252
253    for monster in &mut monsters {
254        if let Some(rough) = find(monster, 3, 20) {
255            return rough;
256        }
257    }
258
259    unreachable!()
260}