You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/2025/puzzles/day12.md
+224Lines changed: 224 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,10 +2,234 @@ import Solver from "../../../../../website/src/components/Solver.js"
2
2
3
3
# Day 12: Christmas Tree Farm
4
4
5
+
by [@natsukagami](https://github.com/natsukagami)
6
+
5
7
## Puzzle description
6
8
7
9
https://adventofcode.com/2025/day/12
8
10
11
+
## Inspecting the input
12
+
13
+
Today's problem gives us various shapes contained in a 3x3 box, and asks us whether we can place them in a larger MxN space.
14
+
The number of shapes of each kind is given.
15
+
16
+
This is eerily reminiscent of the [Bin Packing problem](https://en.wikipedia.org/wiki/Bin_packing_problem), therefore we should
17
+
expect to figure out some patterns in the input, as the general problem is likely to be untractable.
18
+
19
+
We are given the following 6 shapes:
20
+
21
+
```
22
+
0:
23
+
###
24
+
##.
25
+
##.
26
+
27
+
1:
28
+
###
29
+
##.
30
+
.##
31
+
32
+
2:
33
+
.##
34
+
###
35
+
##.
36
+
37
+
3:
38
+
##.
39
+
###
40
+
##.
41
+
42
+
4:
43
+
###
44
+
#..
45
+
###
46
+
47
+
5:
48
+
###
49
+
.#.
50
+
###
51
+
```
52
+
Note that they can be rotated and flipped, so the total number of shapes is actually about 72.
53
+
54
+
The input asks for spaces of size ~2000 (at 40-50 cells for each edge), with the total amount of shapes hovering around a few hundreds, so simply backtracking is not possible (but we tried anyway!).
55
+
56
+
However, we can weed out some special cases:
57
+
- If the space given is spacious enough to fit each shape in its own 3x3 box (i.e. without fitting them together), we can trivially place them:
valtotalCount= requirements.values.sum // total number of required shapes
61
+
(maxRows /3) * (maxCols /3) >= totalCount
62
+
```
63
+
- If the space given is _not enough to fit the total number of cells occupied by the shapes_, then no matter how we fit them, we cannot fit all the shapes into the given space:
Fortunately, **all of our input queries fall into one of these categories!** Therefore, we avoid endless search and grab our last star of the year. Cheers!
82
+
83
+
## Bonus: Can Z3 solve it?
84
+
85
+
I have always heard that these NP-complete problems can sometimes be quickly solved by a general purpose SMT solving library, such as [Z3](https://github.com/Z3Prover/z3).
86
+
In a nutshell: SMT solvers like Z3 allow us to find solutions to inequalities (called _constraints_ in SMT terms) with multiple variables. So, if we manage to encode the problem as a set of arithmetic inequalities, then perhaps Z3 would give us the answer?
87
+
88
+
To use Z3 from Scala, I opted for the [ScalaZ3](https://github.com/epfl-lara/ScalaZ3) wrapper made by EPFL's LARA lab.
89
+
Unfortunately the library seems to not be able on Maven, so I compiled it myself and manually include it as an unmanaged JAR.
90
+
It was painless to compile however (`sbt +package` as the README says does the job), and including it is a single directive with the `scala` command-line as a build tool:
91
+
92
+
```scala
93
+
//>usingjar./scalaz3_3-4.8.14.jar
94
+
```
95
+
96
+
In the code, we can set up a Z3 context as follows, preparing to build our constraints.
97
+
98
+
```scala 3
99
+
importz3.scala.*
100
+
valctx=newZ3Context("MODEL"->true) // this allows us to receive a specific solution later on
101
+
vali= ctx.mkIntSort() // declare an int-like type in the SMT context
102
+
// some constants that we shall mention later
103
+
valzero= ctx.mkInt(0, i)
104
+
valone= ctx.mkInt(1, i)
105
+
106
+
// as we will build up the complex constraints one-by-one, mutable collections make them easier to work with
107
+
valconstraints= mutable.ListBuffer[Z3AST]()
108
+
```
109
+
110
+
Now, how do we encode our problem as a bunch of arithmetic inequalities? From a glance, here are the requirements that we have
111
+
to encode:
112
+
- For shape `i`, we have to use `count(i)` amount of shapes.
113
+
- The shapes should be laid on a `N x M` space.
114
+
- Layered shapes should not overlap.
115
+
116
+
A simple way to encode the _choice_ of putting a shape at a specific position, is to turn them into a *binary variable*.
117
+
For shape `i` and a location for the top-left corner `(x, y)`, we would put `i` at this location if the variable `v(i, x, y)`
118
+
holds true.
119
+
As we shall be working with Z3's integers (we will see why later), we will add the following constraints to our list:
`Z3AST` is the type of a constraint in Z3: it is a syntax node of the larger constraint expression.
179
+
180
+
```scala 3
181
+
// in the setupShape function, in our vars for expression
182
+
for i <-0 until s.rows
183
+
j <-0 until s.cols
184
+
if shape.isFilled(i, j)
185
+
do
186
+
contributors(ti + i)(tj + j) += v
187
+
```
188
+
189
+
Once all the contributors have been found, we simply require that _at most one_ of the contributors can actually be filled.
190
+
In terms of arithmetic, we can require that the _sum_ of the contributors is at most 1, so at most one of the contributors can be one.
191
+
```scala 3
192
+
constraints ++=
193
+
for i <-0 until maxRows
194
+
j <-0 until maxCols
195
+
cs = contributors(i)(j) // contributors to cell (i, j)
196
+
if!cs.isEmpty
197
+
yield ctx.mkLE( // <=
198
+
ctx.mkAdd(cs.toSeq*), // sum of all the contributors
199
+
one
200
+
)
201
+
```
202
+
203
+
And that's it! We just have to summon the solver, and asks whether it can find a solution.
204
+
205
+
```scala 3
206
+
valsolver= ctx.mkSolver()
207
+
solver.assertCnstr(
208
+
ctx.mkAnd(constraints.toSeq*), // require all our constraints to be true
209
+
)
210
+
solver.check().get // will fail if Z3 time outs before finding a solution
211
+
```
212
+
213
+
Also, we can ask Z3 to find one specific solution for us:
214
+
```scala 3
215
+
solver
216
+
.checkAndGetAllModels()
217
+
.nextOption()
218
+
.map: model =>
219
+
valtoFill=
220
+
variables // the Seq of all variables for all shapes
221
+
.filter:
222
+
caseVariable(shape, ti, tj, v) =>
223
+
model.evalAs[Int](v) ==1
224
+
// we can now draw shapes in toFill down!
225
+
```
226
+
227
+
So, how does this fare?
228
+
229
+
Well, Z3 can solve the example... (that the trivial code cannot)
230
+
231
+
But no luck for any tests in the actual input :( So yes, it was fun, but we still created tens of thousands of variables, and solvers still cannot deal with that much yet.
232
+
9
233
## Solutions from the community
10
234
-[Solution](https://github.com/rmarbeck/advent2025/blob/main/day12/src/main/scala/Solution.scala) by [Raphaël Marbeck](https://github.com/rmarbeck)
0 commit comments