Part 1: Presents Under the Trees
I’m running a couple of days behind on this one, but the Christmas tree farm waits for no one. After tumbling through a ventilation duct - because apparently that’s how we navigate the North Pole now - I’ve landed in a cavern full of Christmas trees and panicking elves.
Their problem? Presents. Specifically, oddly-shaped presents that need to fit into the regions beneath each tree. It’s like Tetris, except the pieces are weird, there’s no gravity, and instead of clearing lines you’re just trying not to disappoint young elves on Christmas morning. No pressure.
The input comes in two parts: first, a catalogue of present shapes, then a list of tree regions with the presents that need to fit in each:
| |
That 4x4: 0 0 0 0 2 0 means “here’s a 4-by-4 region, and you need to fit zero of shape 0, zero of shape 1, zero of shape 2, zero of shape 3, two of shape 4, and zero of shape 5.” The presents can be rotated and flipped, and while the # parts can’t overlap, the . parts are just empty space in the shape’s bounding box - other presents can happily occupy those cells.
Parsing the Input
I’m using numpy here, it provides multi-dimensional arrays with convenient rotation and flip operations, which is nothing too crazy but not something I want to implement myself for this challenge (it’s probably a lot more efficient than what I would throw together too). The parsing once again is a load of list comprehension gymnastics, but it gets the job done:
| |
This gives me a list of boolean numpy arrays representing each present shape (True where there’s a #), and a list of tuples containing an empty area (as a boolean array of the right dimensions) plus the count of each shape type needed.
Orientations: Flip and Rotate
Before diving into the fitting algorithm, I need to think about all the ways a present can be oriented. Each shape can be flipped horizontally, and each flip can be rotated 0, 90, 180, or 270 degrees. That’s potentially 8 orientations per present - but some shapes have symmetries that make certain orientations identical.
| |
The deduplication here is a small win in what I think will be a slog of a computation - no point exploring the same orientation twice. A square-ish symmetrical shape might only have 1 or 2 unique orientations, while an L-shaped piece might have all 8. Every reduction is a win.
Finding Valid Placements
Where can a present actually go? Given an area (which might already have some presents placed in it), I need to find all positions where a given present orientation fits without overlapping anything.
| |
The overlap check is rather elegant thanks to numpy’s boolean operations. If I slice out the region where the present would go and bitwise-AND it with the present shape, any True values indicate a collision. If the result is all False, the placement is valid.
| |
The generator yields new areas with the present “placed” (bitwise-OR’d into the array). The area is copied to ensure that different branches of the search get independent instances of state without interference.
The Recursive Fit
The algorithm is conceptually simple: pick a present from the pile, try every valid placement in every valid orientation, and recursively attempt to fit the remaining presents. If we ever empty the pile, success! If we exhaust all options, backtrack.
| |
There’s a nice early termination check in there - if the total area of remaining presents exceeds the remaining empty space, there’s no point continuing down that branch. It’s not going to magically fit.
The search is depth-first, and crucially it returns True as soon as it finds any valid arrangement. I don’t need the optimal packing, I don’t need all solutions - just confirmation that one exists.
| |
That nested list comprehension expands the count list into actual present shapes - if we need three of shape 4, we get three copies of that shape in our tofit list.
Performance Thoughts
I’ll be honest: this runs for about 1.5 minutes on my machine. That’s… not fast. But it does finish, which counts for something! The explosion of trying every present in every orientation at every position is quite heavy.
There’s probably room for improvement here. I was thinking of caching combinations of presents that are known to fit into a given area, effectively building “composite” shapes out of the presents. I could also potentially prune earlier by identifying isolated regions that can’t possibly fit any remaining presents. But at 1.5 minutes, it was done before I finished thinking about optimisations let alone implement them.
Sometimes the best optimisation is just… seeing if it’s even necessary!
Part 2: To Be Continued
Part 2 remains unsolved for now. It appears to suggest I have 2 remaining stars to collect before I can access this one, I assume the two days I have yet to complete Part 2. Will revisit this if it unlocks once I get around to those.
As always, full code is available at github.com/lordmoocow/aoc25.
Day 12 stumped me a first but I got a solution to Part 1 with a bit of force, not sure if that will stand up for the next part. Part 2 awaits once I figureo out the rest.