AoC 2020 D20: Jurassic Jigsaw
| Problem statement | Source code | Tags: Image processingGeometry
← Previous Back to AoC Index Next →
This has to be the HARDEST Advent of Code problem I've ever done. It took me more than 24 hours over multiple days to finish it, with at least 3 rewrites.
Part 1
Part 1 in itself is simple. We can find pairs of matching edges (including reversed), which represent adjacent tiles. Each tile can have up to 4 neighbors. The corner tiles are the ones with only 2 neighbors, so we just need to find those and multiply their IDs.
Each tile has 8 possible edges: 4 edges, and their reverses. A 180-degree rotation swaps edges like this:
Let's define the canonical order to be left-to-right for top and bottom edges, and top-to-bottom for left and right edges. Call these edges T, R, B, and L. Their reverses are T', R', B', and L'. Then we can describe a 180-degree rotation as: T => B', R => L', B => T', L => R'. (Read: the original top edge is now the bottom edge reversed, etc.)
We just need to build a mapping from edge content (as a string) to the list of (tile ID, edge ID) pairs, such as:
Note that each pairing occurs twice, in opposite directions, such as T'/B' and T/B. We'll leave deduplication later. It is guaranteed that each edge matches at most one other edge; otherwise we have unresolvable ambiguity (or at least the problem would be much harder because now we may have to consider multiple configurations and backtrack).
Then, we can build a mapping from tile ID to the set of neighboring tile IDs, with the edge pairs.
We just need to find the tiles with only 2 neighbors and multiply their IDs.
Part 2
Now onto the real business: actually assembling the image. If the pieces are already correctly oriented, we should expect this:
Where all alignments are T to B and L to R. But we don't, so the goal is to rotate and flip the tiles such that all alignments are correct.
There are 5 possible transformations:
There are a lot of ways to combine these transformations, but ultimately there are only 8 unique orientations for each tile (once you choose the "top" edge to be one of the 8 directions, the rest are determined). This means that fixing one edge automatically fixes all other edges as well.
The final grid should look like this:
First I need to pick a starting AA tile, whose orientation is completely arbitrary, since its orientation determines the orientation of the entire image, which can be rotated/flipped however. The requirements are: (1) it must be a corner tile, and (2) it must border its two neighbors on the right and bottom (so that we can build the image left-to-right, top-to-bottom)—equivalently, its left and top edges must be unmatched edges. This can be achieved with just a rotation. For example, if the current unmatched edges are B and L:
This corresponds to a 90-degree counterclockwise rotation:
(No need to worry about flipping here; all we want is to get the unmatched edges to the top and left.)
Once we have AA, we immediately know how to orient AB and BA. For AB: (1) its left edge must match AA's right edge, and (2) its top edge must be unmatched. Let's suppose currently we have { AA: { AB: [('R', "B'")] } }, which looks like this:
We first get the top edge right. If there exists a top neighbor, then we need the top edge to align with that; otherwise, we need it to be unmatched. In this case, we want it unmatched, so we need to rotate 90 degrees counterclockwise:
Now we have (R, R) for AA-AB. To get to (R, L), we need to flip left-right:
Now AB is correctly oriented. We can repeat this process for all tiles in the first row (using the tile to the left to determine orientation), then for all subsequent rows (using the tile above to determine orientation).
There are two ways to know when to flip. We can either detect (B, T') alignments and flip left-right, or we can check the left neighbor like we did above. The latter is simpler because it works for the first row as well. As for the first column, we need the left to be unmatched, and the top edge to match the tile above.
To implement one rotation, we need to update two things: (1) the edge mapping, and (2) the tile image itself. The tile image update can be done with NumPy's rot90 and fliplr/flipud functions. For the edge mapping, we can use the transformations defined earlier. For example, a 90-degree clockwise rotation can be implemented like this:
The actual alignment algorithm can probably be greatly simplified, but it was hacked together after many attempts, so I left it as is.
Finally to test the monster, we have an array:
We just rotate and flip this array in all 8 orientations, and for each orientation, we slide it over the entire image and check for matches. If we find a match, we mark those positions as O in the image. At the end, we count the number of # remaining in the image.