1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
//! # Jurassic Jigsaw
//!
//! At first this seems like a daunting problem. However a little anaylsis shows that the input
//! has some nice properties that makes solving this more tractable.
//!
//! * Tile edges match with at most one other tile
//! * The forward and reverse tile edges form two distinct sets of 312 values with no overlap.
//!
//! Tiles can be flipped and rotated for a total of 8 possible permutations each. When parsing
//! the tiles we store all 8 edge possibilities to enable assembling the jigsaw in part two. For
//! performance we avoid transforming the inner 8x8 pixels until we have determined the
//! layout of the grid.

//!
//! ## Part One
//!
//! First we calculate the frequency of each edge, both forwards and backwards as tiles can be in
//! any orientation. As there only 2¹⁰ or 1024 possible edge values we can use an array intead of a
//! hashtable for speed, converting the edges into a binary number to index the array.
//!
//! This results in 96 values that occur once and 528 values that occur twice. Then for every tile
//! we sum the frequency of each edge. Corner tiles will have two edges that only occur once, not
//! matching with any other tile, for a total of 1 + 1 + 2 + 2 = 6.
//!
//! Other edge tiles have a total of 1 + 2 + 2 + 2 = 7 and inner tiles a total of 2 + 2 + 2 + 2 = 8.
//!
//! ## Part Two
//!
//! First we arbitrarily pick any corner tile that is oriented so that its unique edges are facing
//! top and left. Then we proceed row by row, looking up the next tile to the right. Each time
//! we find a tile we remove it from the remaining tiles, so that looking up a tile is always a
//! very fast constant time `O(1)` operation.
//!
//! The complete picture is stored as an array of `u128` values as the tiles form a square 12 wide,
//! for a total of 12 * 8 = 96 pixels. As we add each tile, we convert its pixels into a `u8` binary
//! number and left shift to add to the existing pixels.
//!
//! When finding the monsters we make some further assumptions about the input:
//!
//! * The monsters will all be oriented the same way
//! * Monsters will not overlap with each other
//!
//! For speed the monster bit patterns are rotated and flipped instead of the image, then stored
//! in hardcoded arrays. The search ends as soon as we find monsters in any orientation.
use crate::util::parse::*;
use std::array::from_fn;

pub struct Tile {
    id: u64,
    top: [usize; 8],
    left: [usize; 8],
    bottom: [usize; 8],
    right: [usize; 8],
    pixels: [[u8; 10]; 10],
}

impl Tile {
    // O = Original
    // H = Flip horizontal
    // V = Flip vertical
    // R = Rotate clockwise 90 degrees
    // Sequence: [O, H, V, HV, R, RH, RV, RHV]
    const COEFFICIENTS: [[i32; 6]; 8] = [
        [1, 0, 1, 0, 1, 1],
        [-1, 0, 8, 0, 1, 1],
        [1, 0, 1, 0, -1, 8],
        [-1, 0, 8, 0, -1, 8],
        [0, 1, 1, -1, 0, 8],
        [0, 1, 1, 1, 0, 1],
        [0, -1, 8, -1, 0, 8],
        [0, -1, 8, 1, 0, 1],
    ];

    fn from(chunk: &[&str]) -> Tile {
        let id = (&chunk[0][5..9]).unsigned();

        let pixels: [[u8; 10]; 10] = from_fn(|i| chunk[i + 1].as_bytes().try_into().unwrap());

        // The ASCII code for "#" 35 is odd and the code for "." 46 is even
        // so we can convert to a 1 or 0 bit using bitwise AND with 1.
        let binary = |row: usize, col: usize| (pixels[row][col] & 1) as usize;
        let mut t = 0;
        let mut l = 0;
        let mut b = 0;
        let mut r = 0;

        for i in 0..10 {
            t = (t << 1) | binary(0, i);
            l = (l << 1) | binary(i, 0);
            b = (b << 1) | binary(9, i);
            r = (r << 1) | binary(i, 9);
        }

        let reverse = |edge: usize| edge.reverse_bits() >> 54;
        let rt = reverse(t);
        let rl = reverse(l);
        let rb = reverse(b);
        let rr = reverse(r);

        // Same transform sequence as coefficients:
        // [O, H, V, HV, R, RH, RV, RHV]
        let top = [t, rt, b, rb, rl, l, rr, r];
        let left = [l, r, rl, rr, b, t, rb, rt];
        let bottom = [b, rb, t, rt, rr, r, rl, l];
        let right = [r, l, rr, rl, t, b, rt, rb];

        Tile { id, top, left, bottom, right, pixels }
    }

    // Coefficients allow us to reuse the loop logic for each of the 8 possible permutations.
    fn transform(&self, image: &mut [u128], permutation: usize) {
        let [a, b, c, d, e, f] = Self::COEFFICIENTS[permutation];

        for row in 0..8 {
            let mut acc = 0;

            for col in 0..8 {
                let x = a * col + b * row + c;
                let y = d * col + e * row + f;
                let b = self.pixels[y as usize][x as usize];
                acc = (acc << 1) | (b & 1);
            }

            image[row as usize] = (image[row as usize] << 8) | (acc as u128);
        }
    }
}

pub fn parse(input: &str) -> Vec<Tile> {
    let lines: Vec<_> = input.lines().collect();
    lines.chunks(12).map(Tile::from).collect()
}

pub fn part1(input: &[Tile]) -> u64 {
    let mut frequency = [0; 1024];
    let mut result = 1;

    for tile in input {
        for edge in tile.top {
            frequency[edge] += 1;
        }
    }

    for tile in input {
        // Any orientation will do, pick the first.
        let total = frequency[tile.top[0]]
            + frequency[tile.left[0]]
            + frequency[tile.bottom[0]]
            + frequency[tile.right[0]];
        if total == 6 {
            result *= tile.id;
        }
    }

    result
}

pub fn part2(input: &[Tile]) -> u32 {
    // Store mapping of tile edges to tile index in order to allow
    // constant time lookup by edge when assembling the jigsaw.
    let mut edge_to_tile = [[0; 2]; 1024];
    let mut frequency = [0; 1024];
    let mut placed = [false; 1024];

    for (i, tile) in input.iter().enumerate() {
        for edge in tile.top {
            edge_to_tile[edge][frequency[edge]] = i;
            frequency[edge] += 1;
        }
    }

    let mut find_arbitrary_corner = || {
        for tile in input {
            for j in 0..8 {
                if frequency[tile.top[j]] == 1 && frequency[tile.left[j]] == 1 {
                    frequency[tile.top[j]] += 1;
                    return tile.top[j];
                }
            }
        }
        unreachable!();
    };
    let mut find_matching_tile = |edge: usize| {
        let [first, second] = edge_to_tile[edge];
        let next = if placed[first] { second } else { first };
        placed[next] = true;
        &input[next]
    };

    // Assemble the image
    let mut next_top = find_arbitrary_corner();
    let mut image = [0; 96];
    let mut index = 0;

    while frequency[next_top] == 2 {
        let tile = find_matching_tile(next_top);
        let permutation = (0..8).position(|i| tile.top[i] == next_top).unwrap();
        tile.transform(&mut image[index..], permutation);
        next_top = tile.bottom[permutation];

        let mut next_left = tile.right[permutation];

        while frequency[next_left] == 2 {
            let tile = find_matching_tile(next_left);
            let permutation = (0..8).position(|i| tile.left[i] == next_left).unwrap();
            tile.transform(&mut image[index..], permutation);
            next_left = tile.right[permutation];
        }

        index += 8;
    }

    // Common search logic
    let sea: u32 = image.iter().map(|n| n.count_ones()).sum();
    let find = |monster: &mut [u128], width: usize, height: usize| {
        let mut rough = sea;

        for _ in 0..(96 - width + 1) {
            for window in image.windows(height) {
                if monster.iter().enumerate().all(|(i, &n)| n & window[i] == n) {
                    rough -= 15;
                }
            }
            monster.iter_mut().for_each(|n| *n <<= 1);
        }

        (rough < sea).then_some(rough)
    };

    // Transform the monsters instead of the image.
    // Hardcoded bit patterns for [O, H, V, HV].
    let mut monsters = [
        [0b00000000000000000010, 0b10000110000110000111, 0b01001001001001001000],
        [0b01001001001001001000, 0b10000110000110000111, 0b00000000000000000010],
        [0b01000000000000000000, 0b11100001100001100001, 0b00010010010010010010],
        [0b00010010010010010010, 0b11100001100001100001, 0b01000000000000000000],
    ];

    for monster in &mut monsters {
        if let Some(rough) = find(monster, 20, 3) {
            return rough;
        }
    }

    // Hardcoded bit patterns [R, RH, RV, RHV].
    let mut monsters = [
        [2, 4, 0, 0, 4, 2, 2, 4, 0, 0, 4, 2, 2, 4, 0, 0, 4, 2, 3, 2],
        [2, 3, 2, 4, 0, 0, 4, 2, 2, 4, 0, 0, 4, 2, 2, 4, 0, 0, 4, 2],
        [2, 1, 0, 0, 1, 2, 2, 1, 0, 0, 1, 2, 2, 1, 0, 0, 1, 2, 6, 2],
        [2, 6, 2, 1, 0, 0, 1, 2, 2, 1, 0, 0, 1, 2, 2, 1, 0, 0, 1, 2],
    ];

    for monster in &mut monsters {
        if let Some(rough) = find(monster, 3, 20) {
            return rough;
        }
    }

    unreachable!()
}