Skip to content

Commit 5eecbb6

Browse files
authored
Manually solve, rather than generic Z3 (#273)
since it's a simple system of equations, solving the equations by hand was faster.
1 parent 29781bc commit 5eecbb6

File tree

2 files changed

+37
-92
lines changed

2 files changed

+37
-92
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ as the value which passed the given day/phase combination
159159

160160
#### 2024
161161
* Day 11 The fact that the order of stones didn't actually matter helped to allow us to just keep a list of how many we had.
162-
* Day 13 Being able to solve the basic with shortest path while needing math for part two is always fun re-learning linear algebra.
162+
* Day 13 Being able to solve the basic with shortest path while needing math for part two is always fun re-learning linear algebra. And refactoring always speeds things up.
163163

164164
## Takeaways
165165

Lines changed: 36 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,121 +1,66 @@
11
package me.peckb.aoc._2024.calendar.day13
22

3-
import com.microsoft.z3.BitVecNum
4-
import com.microsoft.z3.BitVecSort
5-
import com.microsoft.z3.Context
6-
import com.microsoft.z3.Expr
7-
import com.microsoft.z3.Status
8-
import me.peckb.aoc._2024.calendar.day13.ClawGameDijkstra.GameNode
93
import javax.inject.Inject
104
import me.peckb.aoc.generators.InputGenerator.InputGeneratorFactory
11-
import me.peckb.aoc.pathing.GenericIntDijkstra
5+
import kotlin.math.roundToLong
126

137
class Day13 @Inject constructor(
148
private val generatorFactory: InputGeneratorFactory,
159
) {
1610
fun partOne(filename: String) = generatorFactory.forFile(filename).read { input ->
1711
input.chunked(4)
18-
.mapNotNull { (a, b, p, _) ->
19-
val aSpeed = parse(a.split("A: ").last()).let{ (x, y) -> Speed(x, y) }
20-
val bSpeed = parse(b.split("B: ").last()).let{ (x, y) -> Speed(x, y) }
21-
val prize = parse(p.split(": ").last()).let{ (x, y) -> GameNode(x, y) }
22-
23-
val game = ClawGameDijkstra(aSpeed, bSpeed, prize)
24-
25-
val solutions = game.solve(GameNode(0, 0).withGame(game), prize.withGame(game))
26-
27-
solutions[prize]
28-
}
12+
.mapNotNull { (a, b, p, _) -> solve(a, b, p, 0) }
2913
.sum()
3014
}
3115

3216
fun partTwo(filename: String) = generatorFactory.forFile(filename).read { input ->
3317
input.chunked(4)
34-
.mapNotNull { (a, b, p, _) ->
35-
val aSpeed = parse(a.split("A: ").last()).let{ (x, y) -> Speed(x, y) }
36-
val bSpeed = parse(b.split("B: ").last()).let{ (x, y) -> Speed(x, y) }
37-
val prize = parse(p.split(": ").last()).let{ (x, y) -> x + BUFFER to y + BUFFER }
38-
39-
Context().use { ctx ->
40-
val solver = ctx.mkSolver()
41-
42-
val longType = ctx.mkBitVecSort(64)
43-
fun variableOf(name: String) = ctx.mkConst(name, longType)
44-
fun valueOf(value: Long) = ctx.mkNumeral(value, longType) as BitVecNum
45-
operator fun Expr<BitVecSort>.times(t: Expr<BitVecSort>) = ctx.mkBVMul(this, t)
46-
operator fun Expr<BitVecSort>.plus(t: Expr<BitVecSort>) = ctx.mkBVAdd(this, t)
47-
infix fun Expr<BitVecSort>.equalTo(t: Expr<BitVecSort>) = ctx.mkEq(this, t)
48-
infix fun Expr<BitVecSort>.greaterThan(t: Expr<BitVecSort>) = ctx.mkBVSGT(this, t)
49-
infix fun Expr<BitVecSort>.lessThan(t: Expr<BitVecSort>) = ctx.mkBVSLT(this, t)
50-
51-
val zero = valueOf(0)
52-
val max = valueOf(500_000_000_000)
53-
val aPress = variableOf("x")
54-
val bPress = variableOf("y")
55-
val aXSpeed = valueOf(aSpeed.x.toLong())
56-
val aYSpeed = valueOf(aSpeed.y.toLong())
57-
val bXSpeed = valueOf(bSpeed.x.toLong())
58-
val bYSpeed = valueOf(bSpeed.y.toLong())
59-
val prizeX = valueOf(prize.first)
60-
val prizeY = valueOf(prize.second)
61-
62-
solver.add(aPress greaterThan zero)
63-
solver.add(bPress greaterThan zero)
64-
solver.add(aPress lessThan max)
65-
solver.add(bPress lessThan max)
66-
solver.add((aXSpeed * aPress) + (bXSpeed * bPress) equalTo prizeX)
67-
solver.add((aYSpeed * aPress) + (bYSpeed * bPress) equalTo prizeY)
68-
69-
val status = solver.check()
70-
71-
if (status == Status.SATISFIABLE) {
72-
val model = solver.model
73-
val aButtonCost = (model.evaluate(aPress, true) as BitVecNum).long * A_COST
74-
val bButtonCost = (model.evaluate(bPress, true) as BitVecNum).long * B_COST
75-
76-
aButtonCost + bButtonCost
77-
} else {
78-
null
79-
}
80-
}
81-
}
18+
.mapNotNull { (a, b, p, _) -> solve(a, b, p, BUFFER) }
8219
.sum()
8320
}
8421

85-
private fun parse(data: String): Pair<Int, Int> {
86-
return data.split(", ").map{ it.drop(2).toInt() }.let { (x, y) -> x to y }
22+
private fun parse(data: String): Pair<Long, Long> {
23+
return data.split(", ").map{ it.drop(2).toLong() }.let { (x, y) -> x to y }
8724
}
8825

89-
companion object {
90-
private const val BUFFER = 10000000000000
91-
private const val A_COST = 3
92-
private const val B_COST = 1
93-
}
94-
}
26+
private fun solve(a: String, b: String, p: String, buffer: Long): Long? {
27+
val aSpeed = parse(a.split("A: ").last()).let{ (x, y) -> Speed(x, y) }
28+
val bSpeed = parse(b.split("B: ").last()).let{ (x, y) -> Speed(x, y) }
29+
val prize = parse(p.split(": ").last()).let{ (x, y) -> x + buffer to y + buffer }
9530

96-
class ClawGameDijkstra(val speedA: Speed, val speedB: Speed, val destination: GameNode) : GenericIntDijkstra<GameNode>() {
97-
98-
data class GameNode(val x: Int, val y: Int) : DijkstraNode<GameNode> {
99-
lateinit var game: ClawGameDijkstra
100-
101-
fun withGame(game: ClawGameDijkstra) = apply { this.game = game }
31+
return findCost(aSpeed, bSpeed, prize)
32+
}
10233

103-
override fun neighbors(): Map<GameNode, Int> {
104-
return mutableMapOf<GameNode, Int>().apply {
105-
if (x + game.speedA.x <= game.destination.x && y + game.speedA.y <= game.destination.y) {
106-
put(GameNode(x + game.speedA.x, y + game.speedA.y).withGame(game), A_COST)
107-
}
108-
if (x + game.speedB.x <= game.destination.x && y + game.speedB.y <= game.destination.y) {
109-
put(GameNode(x + game.speedB.x, y + game.speedB.y).withGame(game), B_COST)
110-
}
111-
}
112-
}
34+
private fun findCost(aSpeed: Speed, bSpeed: Speed, prize: Pair<Long, Long>): Long? {
35+
val py = prize.second.toDouble()
36+
val px = prize.first.toDouble()
37+
val ax = aSpeed.x.toDouble()
38+
val ay = aSpeed.y.toDouble()
39+
val bx = bSpeed.x.toDouble()
40+
val by = bSpeed.y.toDouble()
41+
42+
// ax*a + bx*b = px (solve for a)
43+
// ... a = px/ax - bx/ax * b
44+
// ay*a + by*b = py (replace a with above)
45+
// ay(px/ax - bx-ax) + by*b = py (solve for b)
46+
// ... b = (py - ay*(px/ax)) / (by - (ay*bx)/ax)
47+
val b = ((py - ay * (px/ax)) / (by - (ay*bx)/ax)).roundToLong()
48+
// replace b in original a = ...
49+
val a = (px/ax - bx/ax * b).roundToLong()
50+
51+
// grab our new location
52+
val loc = (aSpeed.x * a) + (bSpeed.x * b) to (aSpeed.y * a) + (bSpeed.y * b)
53+
54+
// if we're there, the system is solved!
55+
return if (loc == prize) { a * A_COST + b * B_COST } else { null }
11356
}
11457

11558
companion object {
59+
private const val BUFFER = 10000000000000
60+
11661
private const val A_COST = 3
11762
private const val B_COST = 1
11863
}
11964
}
12065

121-
data class Speed(val x: Int, val y: Int)
66+
data class Speed(val x: Long, val y: Long)

0 commit comments

Comments
 (0)