Skip to content

Commit 465897e

Browse files
committed
adventofcode 2025 Day 10 part 2 attempt
Reading that BFS/A* etc. isn't going to work. The search space is too large. Need to treat as "linear programming" problem and use something called "Z3 Solver" which I haven't learnt. GitOrigin-RevId: d553622a21e8436d78216be1e4393f7178c21772
1 parent 6c8f709 commit 465897e

File tree

3 files changed

+108
-48
lines changed

3 files changed

+108
-48
lines changed

advent-of-code/src/main/kotlin/com/willmolloy/adventofcode/_2025/Day10.kt

Lines changed: 90 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,92 +3,146 @@ package com.willmolloy.adventofcode._2025
33
import com.willmolloy.adventofcode.common.Day
44
import com.willmolloy.adventofcode.common.Input
55
import com.willmolloy.adventofcode.common.extensions.debug
6+
import java.util.BitSet
67
import java.util.concurrent.atomic.AtomicInteger
78

89
/** https://adventofcode.com/2025/day/10 */
910
object Day10 : Day(2025, 10) {
1011

11-
// lights = BITSET
12-
private class Machine(val lights: Int, val buttons: Array<IntArray>, val joltage: IntArray){
12+
private class Machine(
13+
val lights: BooleanArray,
14+
val buttons: Array<IntArray>,
15+
val joltage: IntArray,
16+
) {
1317
override fun toString(): String {
14-
return "Machine(lights=${lights}, buttons=${buttons.contentDeepToString()}, joltage=${joltage.contentToString()})"
18+
return "Machine(lights=${lights.contentToString()}, buttons=${buttons.contentDeepToString()}, joltage=${joltage.contentToString()})"
1519
}
1620
}
1721

18-
fun Int.toggleBit(bit: Int): Int = this xor (1 shl bit)
22+
private fun parse(input: Input): List<Machine> =
23+
input.readLines().map { line ->
24+
val match = Regex("\\[(.*)] \\((.*)\\) \\{(.*)}").matchEntire(line)!!
25+
val (lights, buttons, joltage) = match.destructured
26+
27+
val lightsTyped = lights.toCharArray().map { it != '.' }.toBooleanArray()
28+
val buttonsTyped =
29+
buttons.split(") (").map { it.split(",").map { it.toInt() }.toIntArray() }.toTypedArray()
30+
val joltageTyped = joltage.split(",").map { it.toInt() }.toIntArray()
31+
Machine(lightsTyped, buttonsTyped, joltageTyped)
32+
}
33+
34+
fun BooleanArray.toBitSet(): BitSet {
35+
val bitSet = BitSet()
36+
for ((i, b) in this.withIndex()) {
37+
if (b) {
38+
bitSet.flip(i)
39+
}
40+
}
41+
return bitSet
42+
}
1943

2044
override fun part1(input: Input): Any {
2145
val machines = parse(input)
22-
23-
val solve = AtomicInteger(0)
46+
val solveCount = AtomicInteger()
2447

2548
fun solve(machine: Machine): Long {
26-
"Solving ${solve.incrementAndGet()}/${machines.size}: $machine".debug()
49+
"Solving ${solveCount.incrementAndGet()}/${machines.size}: $machine".debug()
50+
51+
// represent the graph node as a bitset; otherwise get OOM
52+
val lightsBitSet = machine.lights.toBitSet()
2753

2854
// BFS for shorted path
29-
val queue = ArrayDeque<Int>()
55+
val queue = ArrayDeque<BitSet>()
3056

31-
// initially all off
32-
queue.add(0)
57+
// initially all lights off
58+
val root = BitSet()
59+
queue.add(root)
3360

34-
val visited = mutableSetOf<Int>()
61+
val visited = mutableSetOf<BitSet>()
3562

3663
var depth = 0L
3764
while (!queue.isEmpty()) {
65+
"Depth: $depth, breadth: ${queue.size}".debug()
3866

39-
val levelSize = queue.size
40-
for (i in 0 until levelSize) {
67+
(0..<queue.size).forEach { _ ->
4168
val node = queue.removeFirst()
4269

43-
if (node == machine.lights) {
70+
if (node == lightsBitSet) {
4471
return depth
4572
}
4673

47-
for (button in machine.buttons.withIndex()) {
48-
var adjNode = node
49-
for (b in button.value) {
50-
adjNode = adjNode.toggleBit(b)
51-
}
74+
for (button in machine.buttons) {
75+
val adjNode = node.clone() as BitSet
76+
77+
// TODO possible to represent button as BitSet then flip all at once?
78+
button.forEach { adjNode.flip(it) }
5279

5380
if (visited.add(adjNode)) {
5481
queue.add(adjNode)
5582
}
5683
}
57-
5884
}
5985
depth++
6086
}
6187

62-
error("Unsolved")
88+
error("Failed to solve: $machine")
6389
}
6490

6591
return machines.sumOf { solve(it) }
6692
}
6793

6894
override fun part2(input: Input): Any {
69-
return 2222222222222222
70-
}
95+
val machines = parse(input)
96+
val solveCount = AtomicInteger()
7197

72-
private fun parse(input: Input): List<Machine> = input.readLines()
73-
.mapNotNull { line ->
74-
val match = Regex("\\[(.*)](.*)\\{(.*)}").matchEntire(line) ?: return@mapNotNull null
98+
fun solve(machine: Machine): Long {
99+
"Solving ${solveCount.incrementAndGet()}/${machines.size}: $machine".debug()
75100

76-
val (lights, buttons, joltage) = match.destructured
101+
// TODO possible to represent the joltage as bitset too?
102+
// TODO or is there a more efficient way to traverse the graph?
103+
// TODO the subreddit mentions Z3...
77104

78-
val lightsTyped = lights.toCharArray().map { it != '.' }.toBooleanArray()
105+
// BFS for shorted path
106+
val queue = ArrayDeque<IntArray>()
79107

80-
val buttonsTyped = buttons.trim().removePrefix("(").removeSuffix(")").split(") (")
81-
.map { it.split(",").map { it.toInt() }.toIntArray() }.toTypedArray()
108+
// initially all joltage zero
109+
val root = IntArray(machine.joltage.size) { 0 }
110+
queue.add(root)
82111

83-
val joltageTyped = joltage.split(",").map { it.toInt() }.toIntArray()
112+
val visited = mutableSetOf<String>()
113+
114+
var depth = 0L
115+
while (!queue.isEmpty()) {
116+
"Depth: $depth, breadth: ${queue.size}".debug()
117+
118+
(0..<queue.size).forEach { _ ->
119+
val node = queue.removeFirst()
120+
121+
if (node.contentEquals(machine.joltage)) {
122+
return depth
123+
}
124+
125+
for (button in machine.buttons) {
126+
val adjNode = node.clone()
84127

85-
var lightsBits = 0
86-
for ((i, b) in lightsTyped.withIndex()) {
87-
if (b) {
88-
lightsBits = lightsBits.toggleBit(i)
128+
button.forEach { adjNode[it]++ }
129+
130+
// prune nodes which exceed the joltage
131+
if (adjNode.withIndex().any { adjNode[it.index] > machine.joltage[it.index] }) {
132+
continue
133+
}
134+
135+
if (visited.add(adjNode.contentToString())) {
136+
queue.add(adjNode)
137+
}
138+
}
89139
}
140+
depth++
90141
}
91142

92-
Machine(lightsBits, buttonsTyped, joltageTyped)
143+
error("Failed to solve: $machine")
93144
}
145+
146+
return machines.sumOf { solve(it) }
147+
}
94148
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package com.willmolloy.adventofcode._2025
22

33
import com.willmolloy.adventofcode.common.DayTest
4+
import org.junit.jupiter.api.Assumptions.abort
45

56
object Day10Test : DayTest(Day10) {
67

78
override fun part1() = Answer(7, 425)
89

9-
override fun part2() = Answer(2222222222222222, 2222222222222222)
10+
override fun part2() = Answer({ 33 }, { abort() })
1011
}

advent-of-code/src/test/kotlin/com/willmolloy/adventofcode/common/DayTest.kt

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package com.willmolloy.adventofcode.common
33
import com.google.common.truth.Truth.assertThat
44
import org.junit.jupiter.api.Assumptions.abort
55
import org.junit.jupiter.api.MethodOrderer
6-
import org.junit.jupiter.api.Order
76
import org.junit.jupiter.api.Test
87
import org.junit.jupiter.api.TestMethodOrder
98

@@ -12,33 +11,37 @@ import org.junit.jupiter.api.TestMethodOrder
1211
*
1312
* @author <a href=https://willmolloy.com>Will Molloy</a>
1413
*/
15-
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
14+
@TestMethodOrder(MethodOrderer.MethodName::class)
1615
abstract class DayTest(private val day: Day) {
1716
private val exampleInput: FileInput = FileInput.example(day)
1817
private val realInput: FileInput? = FileInput.real(day)
1918

20-
@Order(1)
2119
@Test
2220
fun part1_exampleInput() {
23-
assertThat(day.part1(exampleInput)).isEqualTo(part1().example)
21+
val expected = part1().example.invoke()
22+
val actual = day.part1(exampleInput)
23+
assertThat(actual).isEqualTo(expected)
2424
}
2525

26-
@Order(3)
2726
@Test
2827
fun part1_realInput() {
29-
assertThat(day.part1(realInput ?: abort())).isEqualTo(part1().real)
28+
val expected = part1().real.invoke()
29+
val actual = day.part1(realInput ?: abort())
30+
assertThat(actual).isEqualTo(expected)
3031
}
3132

32-
@Order(2)
3333
@Test
3434
fun part2_exampleInput() {
35-
assertThat(day.part2(exampleInput)).isEqualTo(part2().example)
35+
val expected = part2().example.invoke()
36+
val actual = day.part2(exampleInput)
37+
assertThat(actual).isEqualTo(expected)
3638
}
3739

38-
@Order(4)
3940
@Test
4041
fun part2_realInput() {
41-
assertThat(day.part2(realInput ?: abort())).isEqualTo(part2().real)
42+
val expected = part2().real.invoke()
43+
val actual = day.part2(realInput ?: abort())
44+
assertThat(actual).isEqualTo(expected)
4245
}
4346

4447
protected abstract fun part1(): Answer
@@ -51,5 +54,7 @@ abstract class DayTest(private val day: Day) {
5154
* @param example expected example answer
5255
* @param real expected real answer
5356
*/
54-
data class Answer(val example: Any, val real: Any)
57+
data class Answer(val example: () -> Any, val real: () -> Any) {
58+
constructor(example: Any, real: Any) : this({ example }, { real })
59+
}
5560
}

0 commit comments

Comments
 (0)