Skip to content

Commit 521bc24

Browse files
committed
Solution Day 17, part two (Chronospatial Computer)
* be able to save and restore the internal state * make the register Ints Long * replace the A/2^x calculations with A shr x * implement part two's program explicitly Instead of interpreting the input, run the program represented by day 17's input directly as Kotlin (Java byte)code.
1 parent 70f8a7a commit 521bc24

File tree

2 files changed

+228
-33
lines changed

2 files changed

+228
-33
lines changed
Lines changed: 198 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,169 @@
11
package de.ronny_h.aoc.year24.day17
22

33
import de.ronny_h.aoc.AdventOfCode
4-
import kotlin.math.pow
5-
import kotlin.properties.Delegates
4+
import de.ronny_h.aoc.extensions.memoize
65

7-
fun main() = ChronospatialComputer().run("4,1,7,6,4,1,0,2,7", "TODO")
6+
fun main() = ChronospatialComputer().run("4,1,7,6,4,1,0,2,7", "164279024971453")
87

98
class ChronospatialComputer : AdventOfCode<String>(2024, 17) {
109
override fun part1(input: List<String>): String {
11-
return ThreeBitComputer().runProgram(input)
10+
return ThreeBitComputer(input).runProgram().joinToString(",")
1211
}
1312

14-
override fun part2(input: List<String>): String = "TODO"
13+
fun part2BruteForce(input: List<String>): String {
14+
val computer = ThreeBitComputer(input)
15+
val state = computer.save()
16+
var registerA = 0L
17+
while (computer.runProgram() != computer.program) {
18+
registerA++
19+
computer.restore(state, registerA)
20+
}
21+
return "$registerA"
22+
}
23+
24+
override fun part2(input: List<String>): String {
25+
val computer = ThreeBitComputer(input)
26+
val program = computer.program
27+
val binarySolution = findSmallestRegisterAValueToOutputTheProgram(program)
28+
val solution = binarySolution.toLong(2)
29+
30+
// validate the solution using the emulated 3-bit computer
31+
computer.restore(computer.save(), solution)
32+
val output = computer.runProgram()
33+
check(output == program)
34+
35+
println("binary solution: $binarySolution")
36+
println("as Long: $solution")
37+
return "$solution"
38+
}
39+
}
40+
41+
fun findSmallestRegisterAValueToOutputTheProgram(program: List<Int>): String {
42+
val programValues = program.toSet() // the numbers to generate
43+
44+
// there are 0..1111111 possibilities to generate one number
45+
// (C is shifted right max 7 digits, only lowest 3 digits of remaining C count for output)
46+
val max = "1111111111".toInt(2)
47+
val resultByInput = mutableMapOf<Int, MutableList<Int>>()
48+
(0..max).map {
49+
it to runTheProgramOneIteration(it.toLong())
50+
}.forEach { (a, out) ->
51+
if (out in programValues) {
52+
resultByInput.getOrPut(out) { mutableListOf() }.add(a)
53+
}
54+
}
55+
val outProducing = programValues.sorted().associateWith { out ->
56+
val generatedBy = resultByInput.getValue(out)
57+
println("$out can be generated by $generatedBy ${generatedBy.size}")
58+
generatedBy.map { it.toString(2).padStart(3, '0') }
59+
}
60+
val outProducingSortedByLowest3Bits = outProducing.mapValues { (_, producedBy) ->
61+
// sort by lowest 3 bits because of (*)
62+
producedBy.sortedBy { it.substring(it.length - 3) }
63+
}
64+
65+
fun checkSolution(candidate: String): Boolean {
66+
val registerA = candidate.toLong(2)
67+
val result = runTheProgramsActualImplementation(registerA)
68+
return result == program
69+
}
70+
71+
fun findSolution(
72+
remainingProgram: List<Int>,
73+
solutionSoFar: String,
74+
): String {
75+
76+
data class RecursionState(
77+
val remainingProgram: List<Int>,
78+
val solutionSoFar: String,
79+
)
80+
81+
lateinit var findSolutionRec: (RecursionState) -> Pair<String, Boolean>
82+
83+
findSolutionRec = { state: RecursionState ->
84+
if (state.remainingProgram.isEmpty()) {
85+
// due to larger overlaps than the chosen ones there might be higher digits leading to a different output
86+
// -> check if the solution indeed is one
87+
state.solutionSoFar to checkSolution(state.solutionSoFar)
88+
} else {
89+
val p = state.remainingProgram.first()
90+
// (*) from outProducing[p] take that number whose digits higher than 3 matches the last bits of binarySolution and has minimal lowest 3 bits
91+
val possibleChoices = outProducingSortedByLowest3Bits.getValue(p)
92+
93+
var result: Pair<String, Boolean> = state.solutionSoFar to false
94+
for (c in possibleChoices) {
95+
if (c.length == 3) {
96+
// an exactly 3 bit long minimal value found -> take it
97+
val (solution, found) = findSolutionRec(
98+
RecursionState(
99+
state.remainingProgram.drop(1),
100+
state.solutionSoFar + c
101+
)
102+
)
103+
if (found) {
104+
result = solution to true
105+
break
106+
} else {
107+
continue
108+
}
109+
} else {
110+
// search a longer value with minimal lowest 3 bits (see (*)) and matching overlapping bits
111+
val higherBitsOfC = c.substring(0, c.length - 3)
112+
val startIndex = state.solutionSoFar.length - higherBitsOfC.length
113+
if (startIndex < 0) {
114+
// overlap exceeds the current number of bits in binarySolution
115+
continue
116+
}
117+
val overlappingBits = state.solutionSoFar.substring(startIndex)
118+
if (higherBitsOfC == overlappingBits) {
119+
// take the lowest 3 bits; the higher ones are matching
120+
val (solution, found) = findSolutionRec(
121+
RecursionState(
122+
state.remainingProgram.drop(1),
123+
state.solutionSoFar + c.substring(c.length - 3)
124+
)
125+
)
126+
if (found) {
127+
result = solution to true
128+
break
129+
} else {
130+
continue
131+
}
132+
}
133+
}
134+
}
135+
result
136+
}
137+
}.memoize()
138+
139+
return findSolutionRec(RecursionState(remainingProgram, solutionSoFar)).first
140+
}
141+
142+
println("program: $program")
143+
// result can only be minimal if the minimal solution is used for the highest bit -> start with that
144+
val highestThreeBits = outProducing.getValue(program.last()).first()
145+
check(highestThreeBits.length == 3)
146+
val binarySolution = findSolution(program.dropLast(1).reversed(), highestThreeBits)
147+
return binarySolution
148+
}
149+
150+
fun runTheProgramsActualImplementation(registerA: Long = 64854237L): List<Int> {
151+
var A = registerA
152+
val out = mutableListOf<Int>()
153+
do {
154+
out.add(runTheProgramOneIteration(A))
155+
A = A shr 3 // 6: 0,3 A = A shr 3
156+
} while (A != 0L) // 7: 3,0 A==0 ? end : goto 0
157+
return out
158+
}
159+
160+
fun runTheProgramOneIteration(A: Long): Int {
161+
var B = A % 8 // 0: 2,4 B = A % 8 -> B = lowest 3 bits of A
162+
B = B xor 1 // 1: 1,1 B = B xor 1 (001) -> invert lowest bit of B
163+
val C = A shr B.toIntChecked() // 2: 7,5 C = A shr B
164+
B = B xor 5 // 3: 1,5 B = B xor 5 (101) -> invert bit 0 and 2 of B
165+
B = B xor C // 4: 4,0 B = B xor C
166+
return (B % 8).toInt() // 5: 5,5 out B % 8 -> print lowest 3 bits of B
15167
}
16168

17169
/**
@@ -38,50 +190,51 @@ class ChronospatialComputer : AdventOfCode<String>(2024, 17) {
38190
* 6 bdv division of A and 2^<combo operand>, truncated result in B
39191
* 7 cdv division of A and 2^<combo operand>, truncated result in C
40192
*/
41-
private class ThreeBitComputer {
193+
class ThreeBitComputer(input: List<String>) {
42194

43195
private val instructionStep = 2
44196

45-
private lateinit var program: List<Int>
46-
private var registerA by Delegates.notNull<Int>()
47-
private var registerB by Delegates.notNull<Int>()
48-
private var registerC by Delegates.notNull<Int>()
197+
val program = input.readProgram()
198+
private var registerA = input.readRegister('A')
199+
private var registerB = input.readRegister('B')
200+
private var registerC = input.readRegister('C')
49201
private var instructionPointer = 0
50202

51203
private val output = mutableListOf<Int>()
52204

53-
fun init(input: List<String>) {
54-
program = input.readProgram()
55-
registerA = input.readRegister('A')
56-
registerB = input.readRegister('B')
57-
registerC = input.readRegister('C')
58-
instructionPointer = 0
59-
output.clear()
60-
}
61-
62205
private val instructions: List<(Int) -> Unit> = listOf(
63-
{ op -> registerA = (registerA / 2.0.pow(combo(op))).toInt(); next() }, // 0 adv
64-
{ op -> registerB = (registerB xor op) ; next() }, // 1 bxl
65-
{ op -> registerB = (combo(op) % 8) and 7 ; next() }, // 2 bst; 7=111 -> take only lowest 3 bits
66-
{ op -> if (registerA == 0) next() else instructionPointer = op }, // 3 jnz
206+
{ op -> registerA = (registerA shr combo(op).toIntChecked()); next() }, // 0 adv; A/2^x = A shr x
207+
{ op -> registerB = (registerB xor op.toLong()) ; next() }, // 1 bxl
208+
{ op -> registerB = (combo(op) % 8) ; next() }, // 2 bst; 7=111 -> take only lowest 3 bits
209+
{ op -> if (registerA == 0L) next() else instructionPointer = op }, // 3 jnz
67210
{ _ -> registerB = (registerB xor registerC) ; next() }, // 4 bxc
68-
{ op -> output.add((combo(op) % 8) and 7) ; next() }, // 5 out
69-
{ op -> registerB = (registerA / 2.0.pow(combo(op))).toInt(); next() }, // 6 bdv
70-
{ op -> registerC = (registerA / 2.0.pow(combo(op))).toInt(); next() }, // 7 cdv
211+
{ op -> output.add((combo(op) % 8).toInt()) ; next() }, // 5 out
212+
{ op -> registerB = (registerA shr combo(op).toIntChecked()); next() }, // 6 bdv
213+
{ op -> registerC = (registerA shr combo(op).toIntChecked()); next() }, // 7 cdv
71214
)
72215

73-
fun runProgram(input: List<String>): String {
74-
init(input)
216+
fun runProgram(): List<Int> {
75217
while (instructionPointer < program.size) {
76218
val instruction = program[instructionPointer]
77219
val op = program[instructionPointer + 1]
78220
instructions[instruction].invoke(op)
79221
}
80-
return output.joinToString(",")
222+
return output
223+
}
224+
225+
fun save() = State(registerA, registerB, registerC, instructionPointer)
226+
fun restore(state: State, registerAOverride: Long) {
227+
this.registerA = registerAOverride
228+
this.registerB = state.registerB
229+
this.registerC = state.registerC
230+
this.instructionPointer = state.instructionPointer
231+
this.output.clear()
81232
}
82233

83234
private fun combo(op: Int) = when (op) {
84-
in 1..3 -> op
235+
1 -> 1L
236+
2 -> 2L
237+
3 -> 3L
85238
4 -> registerA
86239
5 -> registerB
87240
6 -> registerC
@@ -95,11 +248,25 @@ private class ThreeBitComputer {
95248
private fun List<String>.readRegister(register: Char) =
96249
first { it.startsWith("Register $register: ") }
97250
.substringAfter("Register $register: ")
98-
.toInt()
251+
.toLong()
99252

100253
private fun List<String>.readProgram() =
101254
first { it.startsWith("Program: ") }
102255
.substringAfter("Program: ")
103256
.split(",")
104257
.map(String::toInt)
258+
259+
data class State(
260+
val registerA: Long,
261+
val registerB: Long,
262+
val registerC: Long,
263+
val instructionPointer: Int,
264+
)
265+
}
266+
267+
private fun Long.toIntChecked(): Int {
268+
if (this > Int.MAX_VALUE) {
269+
throw IllegalArgumentException("$this exceeds Int range")
270+
}
271+
return this.toInt()
105272
}

src/test/kotlin/de/ronny_h/aoc/year24/day17/ChronospatialComputerTest.kt

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import io.kotest.core.spec.style.StringSpec
55
import io.kotest.matchers.shouldBe
66

77
class ChronospatialComputerTest : StringSpec({
8-
val input = """
8+
val input1 = """
99
Register A: 729
1010
Register B: 0
1111
Register C: 0
@@ -14,6 +14,34 @@ class ChronospatialComputerTest : StringSpec({
1414
""".asList()
1515

1616
"part 1: The program's output" {
17-
ChronospatialComputer().part1(input) shouldBe "4,6,3,5,6,3,5,2,1,0"
17+
ChronospatialComputer().part1(input1) shouldBe "4,6,3,5,6,3,5,2,1,0"
18+
}
19+
20+
val input2 = """
21+
Register A: 2024
22+
Register B: 0
23+
Register C: 0
24+
25+
Program: 0,3,5,4,3,0
26+
""".asList()
27+
28+
"part 2: The value of A so that the program outputs itself can be found brute force for a small program" {
29+
ChronospatialComputer().part2BruteForce(input2) shouldBe "117440"
30+
}
31+
32+
"part 2: For a larger program a different approach is needed" {
33+
val program = listOf(2,4,1,1,7,5,1,5,4,0,5,5,0,3,3,0)
34+
val registerABinary = findSmallestRegisterAValueToOutputTheProgram(program)
35+
val registerA = registerABinary.toLong(2)
36+
37+
val input = """
38+
Register A: $registerA
39+
Register B: 0
40+
Register C: 0
41+
42+
Program: ${program.joinToString(",")}
43+
""".asList()
44+
45+
ThreeBitComputer(input).runProgram() shouldBe program
1846
}
1947
})

0 commit comments

Comments
 (0)