|
1 | 1 | package me.peckb.aoc._2024.calendar.day13 |
2 | 2 |
|
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 |
9 | 3 | import javax.inject.Inject |
10 | 4 | import me.peckb.aoc.generators.InputGenerator.InputGeneratorFactory |
11 | | -import me.peckb.aoc.pathing.GenericIntDijkstra |
| 5 | +import kotlin.math.roundToLong |
12 | 6 |
|
13 | 7 | class Day13 @Inject constructor( |
14 | 8 | private val generatorFactory: InputGeneratorFactory, |
15 | 9 | ) { |
16 | 10 | fun partOne(filename: String) = generatorFactory.forFile(filename).read { input -> |
17 | 11 | 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) } |
29 | 13 | .sum() |
30 | 14 | } |
31 | 15 |
|
32 | 16 | fun partTwo(filename: String) = generatorFactory.forFile(filename).read { input -> |
33 | 17 | 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) } |
82 | 19 | .sum() |
83 | 20 | } |
84 | 21 |
|
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 } |
87 | 24 | } |
88 | 25 |
|
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 } |
95 | 30 |
|
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 | + } |
102 | 33 |
|
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 } |
113 | 56 | } |
114 | 57 |
|
115 | 58 | companion object { |
| 59 | + private const val BUFFER = 10000000000000 |
| 60 | + |
116 | 61 | private const val A_COST = 3 |
117 | 62 | private const val B_COST = 1 |
118 | 63 | } |
119 | 64 | } |
120 | 65 |
|
121 | | -data class Speed(val x: Int, val y: Int) |
| 66 | +data class Speed(val x: Long, val y: Long) |
0 commit comments