Skip to content

Commit 8b527c3

Browse files
committed
adventofcode 2025 Day 12
In hindsight that 'check the tree has enough spaces' optimisation is obvious. GitOrigin-RevId: cfd049959bac722af671c8346e858e3989654001
1 parent dbc2b59 commit 8b527c3

File tree

6 files changed

+268
-3
lines changed

6 files changed

+268
-3
lines changed
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package com.willmolloy.adventofcode._2025
2+
3+
import com.willmolloy.adventofcode.common.Day
4+
import com.willmolloy.adventofcode.common.Input
5+
import com.willmolloy.adventofcode.common.extensions.debug
6+
import com.willmolloy.adventofcode.common.grid.CharGrid
7+
import com.willmolloy.adventofcode.common.grid.MutableCharGrid
8+
import com.willmolloy.adventofcode.common.grid.Point
9+
import java.util.concurrent.atomic.AtomicInteger
10+
11+
/** https://adventofcode.com/2025/day/12 */
12+
object Day12 : Day(2025, 12) {
13+
14+
private data class Present(val grid: CharGrid, val combinations: List<CharGrid>, val index: Int)
15+
16+
private data class Tree(val width: Int, val length: Int, val presentCounts: List<Int>)
17+
18+
private fun parsePresents(input: Input): List<Present> {
19+
var presentIndex = 0
20+
return input
21+
.readLines()
22+
.filter { it.contains('#') || it.contains('.') }
23+
.filter { it.isNotEmpty() }
24+
// they're all 3x3
25+
.chunked(3)
26+
.map {
27+
val grid = it.map { it.toCharArray() }.toTypedArray().let { CharGrid(it) }
28+
Present(grid, grid.combinations(), presentIndex++)
29+
}
30+
}
31+
32+
private fun parseTrees(input: Input): List<Tree> {
33+
return input
34+
.readLines()
35+
.filter { it.contains('x') }
36+
.map { line ->
37+
val match = Regex("(\\d+)x(\\d+): (.*)").matchEntire(line)!!
38+
val (width, length, counts) = match.destructured
39+
40+
val presentCounts = counts.split(" ").map { it.toInt() }
41+
Tree(width.toInt(), length.toInt(), presentCounts)
42+
}
43+
}
44+
45+
override fun part1(input: Input): Any {
46+
val presents = parsePresents(input)
47+
val trees = parseTrees(input)
48+
49+
val solveCounter = AtomicInteger()
50+
51+
fun solve(tree: Tree): Boolean {
52+
"Solving ${solveCounter.incrementAndGet()}/${trees.size}: $tree".debug()
53+
54+
// first ensure the grid has enough spaces
55+
val treeSpaces = tree.length * tree.width
56+
val totalPresentSpaces =
57+
presents.sumOf { present ->
58+
val presentSpaces = present.grid.count { present.grid[it] == '#' }
59+
presentSpaces * tree.presentCounts[present.index]
60+
}
61+
if (totalPresentSpaces > treeSpaces) {
62+
return false
63+
}
64+
65+
val treeGrid = CharGrid(Array(tree.length) { CharArray(tree.width) { '.' } }).toMutable()
66+
var presentId = 'A' // debugging
67+
68+
// attempt to place the presents - recursively - it's like the sudoku solver problem
69+
fun dfs(presentCounts: MutableList<Int>): Boolean {
70+
// treeGrid.debug()
71+
// presentCounts.debug()
72+
73+
for (present in presents) {
74+
if (presentCounts[present.index] == 0) {
75+
continue
76+
}
77+
78+
for (presentGrid in present.combinations) {
79+
80+
for (x in 0..tree.width - presentGrid.width()) {
81+
for (y in 0..tree.length - presentGrid.height()) {
82+
val offset = Point(x, y)
83+
84+
if (tryPlacePresent(treeGrid, presentGrid, offset, presentId)) {
85+
// "Placed: ${present.id} as $presentId".debug()
86+
presentCounts[present.index]--
87+
presentId++
88+
if (dfs(presentCounts)) {
89+
return true
90+
} else {
91+
// back track
92+
removePresent(treeGrid, presentGrid, offset)
93+
presentCounts[present.index]++
94+
presentId--
95+
}
96+
}
97+
}
98+
}
99+
}
100+
}
101+
102+
return presentCounts.all { it == 0 }
103+
}
104+
105+
return dfs(tree.presentCounts.toMutableList())
106+
}
107+
108+
return trees.parallelStream().filter { solve(it) }.count()
109+
}
110+
111+
private fun tryPlacePresent(
112+
treeGrid: MutableCharGrid,
113+
presentGrid: CharGrid,
114+
offset: Point,
115+
presentId: Char,
116+
): Boolean {
117+
val placed = mutableListOf<Point>()
118+
119+
for (presentLoc in presentGrid) {
120+
// TODO precompute this? i.e. really just need the List<Point> which contain '#'
121+
if (presentGrid[presentLoc] != '#') {
122+
continue
123+
}
124+
125+
val treeLoc = presentLoc.move(offset)
126+
if (treeGrid[treeLoc] == '.') {
127+
treeGrid[treeLoc] = presentId
128+
placed.add(treeLoc)
129+
} else {
130+
// need to clear any partially placed present
131+
placed.forEach { treeGrid[it] = '.' }
132+
return false
133+
}
134+
}
135+
136+
return true
137+
}
138+
139+
private fun removePresent(treeGrid: MutableCharGrid, presentGrid: CharGrid, offset: Point) {
140+
for (presentLoc in presentGrid) {
141+
if (presentGrid[presentLoc] != '#') {
142+
continue
143+
}
144+
145+
val treeLoc = presentLoc.move(offset)
146+
if (treeGrid[treeLoc] != '.') {
147+
treeGrid[treeLoc] = '.'
148+
}
149+
}
150+
}
151+
152+
// TODO move these into Grid class?
153+
private fun CharGrid.combinations(): List<CharGrid> {
154+
val rotate90 = this.rotate90()
155+
val rotate180 = rotate90.rotate90()
156+
val rotate270 = rotate180.rotate90()
157+
val rotations = listOf(this, rotate90, rotate180, rotate270)
158+
return rotations.flatMap { listOf(it, it.flip()) }.distinct()
159+
}
160+
161+
private fun CharGrid.rotate90(): CharGrid {
162+
val rotated = Array(width()) { CharArray(height()) }
163+
164+
// transpose
165+
for (row in 0 until height()) {
166+
for (col in 0 until width()) {
167+
rotated[col][row] = this[row, col]
168+
}
169+
}
170+
171+
// reverse each row
172+
for (chars in rotated) {
173+
chars.reverse()
174+
}
175+
176+
return CharGrid(rotated)
177+
}
178+
179+
private fun CharGrid.flip(): CharGrid {
180+
val flipped = Array(height()) { CharArray(width()) }
181+
182+
// copy
183+
for (row in 0 until height()) {
184+
for (col in 0 until width()) {
185+
flipped[row][col] = this[row, col]
186+
}
187+
}
188+
189+
// reverse each row
190+
for (chars in flipped) {
191+
chars.reverse()
192+
}
193+
194+
return CharGrid(flipped)
195+
}
196+
197+
override fun part2(input: Input): Any {
198+
return 2222222222222222
199+
}
200+
}

advent-of-code/src/main/kotlin/com/willmolloy/adventofcode/common/extensions/Collections.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ fun <T> List<List<T>>.transpose(): List<List<T>> {
44
val rows = size
55
val cols = if (isEmpty()) 0 else first().size
66

7-
val transpose = MutableList(cols) { MutableList<T?>(rows) { null } }
7+
val transposed = MutableList(cols) { MutableList<T?>(rows) { null } }
88

99
for (row in 0 until rows) {
1010
for (col in 0 until cols) {
11-
transpose[col][row] = this[row][col]
11+
transposed[col][row] = this[row][col]
1212
}
1313
}
1414

1515
@Suppress("UNCHECKED_CAST")
16-
return transpose as List<List<T>>
16+
return transposed as List<List<T>>
1717
}
1818

1919
fun <T> Iterable<T>.pairs(): List<Pair<T, T>> {

advent-of-code/src/main/kotlin/com/willmolloy/adventofcode/common/grid/CharGrid.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,21 @@ open class CharGrid(private val array: Array<CharArray>) : Grid<Char, CharGrid,
3434
@Throws(ArrayIndexOutOfBoundsException::class)
3535
operator fun get(point: Point): Char = array[point.y.toInt()][point.x.toInt()]
3636

37+
@Throws(ArrayIndexOutOfBoundsException::class)
38+
operator fun get(row: Int, col: Int): Char = array[row][col]
39+
3740
override fun toString(): String = array.joinToString("\n") { String(it) }
41+
42+
override fun equals(other: Any?): Boolean {
43+
if (this === other) return true
44+
if (javaClass != other?.javaClass) return false
45+
46+
other as CharGrid
47+
48+
return array.contentDeepEquals(other.array)
49+
}
50+
51+
override fun hashCode(): Int {
52+
return array.contentDeepHashCode()
53+
}
3854
}

advent-of-code/src/main/kotlin/com/willmolloy/adventofcode/common/grid/Point.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ data class Point(val x: Long, val y: Long) {
2222
return dx * dx + dy * dy
2323
}
2424

25+
fun move(p: Point): Point = Point(x + p.x, y + p.y)
26+
27+
fun move(dx: Int, dy: Int): Point = Point(x + dx, y + dy)
28+
2529
/**
2630
* Move the point.
2731
*
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
0:
2+
###
3+
##.
4+
##.
5+
6+
1:
7+
###
8+
##.
9+
.##
10+
11+
2:
12+
.##
13+
###
14+
##.
15+
16+
3:
17+
##.
18+
###
19+
##.
20+
21+
4:
22+
###
23+
#..
24+
###
25+
26+
5:
27+
###
28+
.#.
29+
###
30+
31+
4x4: 0 0 0 0 2 0
32+
12x5: 1 0 1 0 2 2
33+
12x5: 1 0 1 0 3 2
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.willmolloy.adventofcode._2025
2+
3+
import com.willmolloy.adventofcode.common.DayTest
4+
import org.junit.jupiter.api.Assumptions.abort
5+
6+
object Day12Test : DayTest(Day12) {
7+
8+
// today the example is harder - save build minutes
9+
override fun part1() = Answer({ abort() }, { 490 })
10+
11+
override fun part2() = Answer(2222222222222222, 2222222222222222)
12+
}

0 commit comments

Comments
 (0)