Skip to content

Commit c21e558

Browse files
committed
Solution 2025-10 part 2 (Factory)
Use the Z3 theorem prover from Microsoft Research to solve an under-determined system of linear equations.
1 parent 0405819 commit c21e558

File tree

6 files changed

+156
-94
lines changed

6 files changed

+156
-94
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ repositories {
1010
dependencies {
1111
implementation(libs.kotlin.logging)
1212
implementation(libs.kotlinx.coroutines.core)
13+
implementation(libs.z3.turnkey)
1314
runtimeOnly(libs.logback.classic)
1415

1516
testImplementation(libs.kotest.assertions.table)

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ kotlinx-coroutines = "1.10.2"
66
kover = "0.9.3"
77
logback = "1.5.21"
88
system-lambda = "1.2.1"
9+
z3-turnkey = "4.14.1"
910

1011
[libraries]
1112
kotest-assertions-table = { module = "io.kotest:kotest-assertions-table", version.ref = "kotest" }
@@ -14,6 +15,7 @@ kotlin-logging = { module = "io.github.oshai:kotlin-logging-jvm", version.ref =
1415
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
1516
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
1617
system-lambda = { module = "com.github.stefanbirkner:system-lambda", version.ref = "system-lambda" }
18+
z3-turnkey = { module = "tools.aqua:z3-turnkey", version.ref = "z3-turnkey" }
1719

1820
[plugins]
1921
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }

src/main/kotlin/de/ronny_h/aoc/extensions/StringUtils.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,6 @@ infix fun String.isAnagramOf(other: String): Boolean {
1212
it.value.size == otherChars[it.key]?.size
1313
}
1414
}
15+
16+
fun String.substringBetween(startDelimiter: String, endDelimiter: String) =
17+
substringAfter(startDelimiter).substringBefore(endDelimiter)

src/main/kotlin/de/ronny_h/aoc/year2025/day10/Day10.kt

Lines changed: 0 additions & 88 deletions
This file was deleted.
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package de.ronny_h.aoc.year2025.day10
2+
3+
import com.microsoft.z3.*
4+
import de.ronny_h.aoc.AdventOfCode
5+
import de.ronny_h.aoc.extensions.memoize
6+
import de.ronny_h.aoc.extensions.substringBetween
7+
import de.ronny_h.aoc.year2025.day10.LightState.OFF
8+
import de.ronny_h.aoc.year2025.day10.LightState.ON
9+
10+
fun main() = Factory().run(481, 20142)
11+
12+
class Factory : AdventOfCode<Int>(2025, 10) {
13+
override fun part1(input: List<String>): Int = input
14+
.parseMachineDescriptions().sumOf {
15+
Machine(it).configureIndicatorLights()
16+
}
17+
18+
override fun part2(input: List<String>): Int = input
19+
.parseMachineDescriptions().sumOf {
20+
Machine(it).configureJoltages()
21+
}
22+
}
23+
24+
// [.##.] (3) (1,3) (2) (2,3) (0,2) (0,1) {3,5,4,7}
25+
fun List<String>.parseMachineDescriptions(): List<MachineDescription> =
26+
map { line ->
27+
MachineDescription(
28+
indicatorLights = line.substringBefore("] (").drop(1)
29+
.map { if (it == '#') ON else OFF },
30+
wiringSchematics = line.substringBetween("] ", " {")
31+
.split(" ").map { it.drop(1).dropLast(1).split(",").map(String::toInt) },
32+
joltage = line.substringAfter(") {").dropLast(1).split(",").map(String::toInt),
33+
)
34+
}
35+
36+
37+
data class MachineDescription(
38+
val indicatorLights: List<LightState>,
39+
val wiringSchematics: List<List<Int>>,
40+
val joltage: List<Int>
41+
)
42+
43+
enum class LightState {
44+
OFF, ON;
45+
46+
fun toggle() = when (this) {
47+
OFF -> ON
48+
ON -> OFF
49+
}
50+
}
51+
52+
class Machine(private val description: MachineDescription, private val maxPressCount: Int = 7) {
53+
private val buttons = description.wiringSchematics
54+
55+
fun configureIndicatorLights(): Int {
56+
val indicatorLightsOff = List(description.indicatorLights.size) { OFF }
57+
val target = description.indicatorLights
58+
59+
data class RecursiveState(val state: List<LightState>, val buttonsToPress: List<Int>, val pressCount: Int)
60+
61+
lateinit var pressButtons: (RecursiveState) -> Int
62+
pressButtons = { s ->
63+
if (s.pressCount == maxPressCount) {
64+
maxPressCount
65+
} else {
66+
val newState = s.state.toMutableList()
67+
s.buttonsToPress.forEach { newState[it] = newState[it].toggle() }
68+
if (newState.contentEquals(target)) {
69+
s.pressCount
70+
} else {
71+
buttons.minOf { pressButtons(RecursiveState(newState.toList(), it, s.pressCount + 1)) }
72+
}
73+
}
74+
}
75+
76+
val pressButtonsMemoized = pressButtons.memoize()
77+
78+
return buttons.minOf { pressButtonsMemoized(RecursiveState(indicatorLightsOff, it, 1)) }
79+
}
80+
81+
/*
82+
Create equations for joltages as sum of button presses and equal to the required target joltage of the machine.
83+
Then Optimize for min of total presses.
84+
Example:
85+
x0 x1 x2 x3 x4 x5
86+
(3) (1,3) (2) (2,3) (0,2) (0,1)
87+
0: x4 x5 = 3
88+
1: x1 x5 = 5
89+
2: x2 x3 x4 = 4
90+
3: x0 x1 x3 = 7
91+
92+
Use the Z3 Theorem Prover to solve the equations.
93+
- the C/C++ implementation: https://github.com/Z3Prover/z3/
94+
- packaged for Java by https://github.com/tudo-aqua/z3-turnkey
95+
*/
96+
fun configureJoltages(): Int = with(Context()) {
97+
operator fun <R : ArithSort> ArithExpr<R>.plus(other: ArithExpr<R>) = mkAdd(this, other)
98+
fun Int.int() = mkInt(this)
99+
100+
val optimization = mkOptimize()
101+
val buttonPresses = buttons.indices.map { mkIntConst("x$it") }
102+
103+
// number of button presses may not be negative
104+
for (presses in buttonPresses) {
105+
optimization.Add(mkGe(presses, 0.int()))
106+
}
107+
108+
// we want to minimize the total number of button presses
109+
val totalPresses = buttonPresses.fold<IntExpr, ArithExpr<IntSort>>(0.int()) { acc, n ->
110+
acc + n
111+
}
112+
optimization.MkMinimize(totalPresses)
113+
114+
// create the actual system of linear equations
115+
val targetJoltages = description.joltage
116+
val joltageExpressions = MutableList<ArithExpr<IntSort>>(targetJoltages.size) { 0.int() }
117+
for ((button, presses) in buttons.zip(buttonPresses)) {
118+
for (i in button) {
119+
joltageExpressions[i] = joltageExpressions[i] + presses
120+
}
121+
}
122+
123+
for ((joltage, targetJoltage) in joltageExpressions.zip(targetJoltages)) {
124+
optimization.Add(mkEq(joltage, targetJoltage.int()))
125+
}
126+
127+
check(optimization.Check() == Status.SATISFIABLE)
128+
val minimalTotalPresses = optimization.model.evaluate(totalPresses, false)
129+
check(minimalTotalPresses is IntNum)
130+
return minimalTotalPresses.int
131+
}
132+
}
133+
134+
fun <T> MutableList<T>.contentEquals(other: List<T>): Boolean {
135+
if (size != other.size) return false
136+
forEachIndexed { i, element -> if (other[i] != element) return false }
137+
return true
138+
}

src/test/kotlin/de/ronny_h/aoc/year2025/day10/Day10Test.kt renamed to src/test/kotlin/de/ronny_h/aoc/year2025/day10/FactoryTest.kt

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ import de.ronny_h.aoc.year2025.day10.LightState.ON
66
import io.kotest.core.spec.style.StringSpec
77
import io.kotest.matchers.shouldBe
88

9-
class Day10Test : StringSpec({
9+
class FactoryTest : StringSpec({
1010

1111
val input = """
1212
[.##.] (3) (1,3) (2) (2,3) (0,2) (0,1) {3,5,4,7}
1313
[...#.] (0,2,3,4) (2,3) (0,4) (0,1,2) (1,2,3,4) {7,5,12,7,2}
1414
[.###.#] (0,1,2,3,4) (0,3,4) (0,1,2,4,5) (1,2) {10,11,11,5,10,5}
1515
""".asList()
1616

17-
"parse" {
17+
"machine descriptions can be parsed" {
1818
"""
1919
[.##.] (3) (1,3) {3,5,4}
2020
[.#] (0,2,3,4) {7,5}
@@ -24,7 +24,7 @@ class Day10Test : StringSpec({
2424
)
2525
}
2626

27-
"contentEquals" {
27+
"contentEquals checks equality of a MutableList and a List" {
2828
mutableListOf(1, 2).contentEquals(listOf(1, 2)) shouldBe true
2929
mutableListOf(1, 2).contentEquals(listOf(1, 2, 3)) shouldBe false
3030
mutableListOf(1, 2).contentEquals(listOf(1, 3)) shouldBe false
@@ -37,12 +37,18 @@ class Day10Test : StringSpec({
3737
Machine(descriptions[2], 5).configureIndicatorLights() shouldBe 2
3838
}
3939

40+
"configureJoltages" {
41+
val descriptions = input.parseMachineDescriptions()
42+
Machine(descriptions[0]).configureJoltages() shouldBe 10
43+
Machine(descriptions[1]).configureJoltages() shouldBe 12
44+
Machine(descriptions[2]).configureJoltages() shouldBe 11
45+
}
46+
4047
"part 1" {
41-
Day10().part1(input) shouldBe 7
48+
Factory().part1(input) shouldBe 7
4249
}
4350

4451
"part 2" {
45-
val input = listOf("")
46-
Day10().part2(input) shouldBe 0
52+
Factory().part2(input) shouldBe 33
4753
}
4854
})

0 commit comments

Comments
 (0)