Advent of Code 2024 - Day 24, in Kotlin - Crossed Wires
Kotlin solutions to parts 1 and 2 of Advent of Code 2024, Day 24: 'Crossed Wires'
Puzzles like this remind me of the computer architecture classes I took in college. While I don’t really have much call to know the inner workings of processors, gates, and transistors these days, I did enjoy it at the time. Today, we’ll solve part 1 with code and part 2 with our eyes and brains. I had help with the visualization of part 2 from this comment by /u/burnt_heatshield on Reddit.
If you’d rather just view the code, my GitHub Repository is here.
Puzzle Input
Today we’ll take our input
in as a List<String>
and parse them into wires
and gates
. Since the system we’re simulating changes over time, we’ll make both of these structures mutable.
class Day24(input: List<String>) {
private val wires: MutableMap<String, Int> = parseWires(input)
private val gates: MutableList<Gate> = parseGates(input)
}
Parsing the wires is done by taking input while it isNotEmpty
. Meaning, once we hit the empty row separating wires an gates, we stop. We associate
the two parts of the wire together, which gives us a Map<String,Int>
, and we make that mutable via toMutableMap
.
// In Day24
private fun parseWires(input: List<String>): MutableMap<String, Int> =
input
.takeWhile { it.isNotEmpty() }
.associate { it.substringBefore(":") to it.last().digitToInt() }
.toMutableMap()
Our gate information is stored in a class called Gate
. It takes four arguments, the left
and right
input wires, the op
(operation) to perform, and the out
wire name. We’ll parse it via an of
function in the companion object
, as usual.
// In Day24
private data class Gate(
val left: String,
val right: String,
val op: String,
val out: String
) {
companion object {
fun of(input: String): Gate =
input.split(" ").let { Gate(it[0], it[2], it[1], it[4]) }
}
}
Parsing the gates is similar to the wires except we’re skipping over non-empty lines and then dropping the first empty line, in order to get to the gates part of the input.
// In Day24
private fun parseGates(input: List<String>): MutableList<Gate> =
input
.dropWhile { it.isNotEmpty() }
.drop(1)
.map { Gate.of(it) }
.toMutableList()
⭐ Day 24, Part 1
The puzzle text can be found here.
Since not all of the gates will be ready to perform an action at the start of the puzzle, we need a way of figuring out which ones are ready. We could have made gates
immutable and kept track of gates we’ve already used in a set, but I went with this because it seemed more fun.
Basically, we call findAndRemoveReady
on our list of gates and it finds and removes any gate whose input wires both have values present in the wires
map.
// In Day24
private fun MutableList<Gate>.findAndRemoveReady(): List<Gate> =
filter {
it.left in wires && it.right in wires
}.also { removeAll(it) }
Next, we can simulate
the entire circuit.
// In Day24
private fun simulate() {
while (gates.isNotEmpty()) {
gates
.findAndRemoveReady()
.forEach { (left, right, op, out) ->
wires[out] = when (op) {
"AND" -> wires.getValue(left) and wires.getValue(right)
"OR" -> wires.getValue(left) or wires.getValue(right)
"XOR" -> wires.getValue(left) xor wires.getValue(right)
else -> throw IllegalArgumentException("Invalid op: $op")
}
}
}
}
We loop through our logic until we run out of gates
to execute. For each gate we find, we look at the op
and perform the action required against the left
and right
input wires, storing the result in the out
wire.
Now we can solve part 1.
// In Day24
fun solvePart1(): Long {
simulate()
return wires
.filter { it.key.startsWith("z") }
.entries
.sortedByDescending { it.key }
.map { it.value }
.joinToString("")
.toLong(2)
}
We simulate
the circuit, and then find all of the z
wires, sort them highest to lowest, join all of the values together and then convert the String
representing a binary number of 1’s and 0’s to a Long
, specifying the string we have is in binary.
Star earned! Onward!
⭐ Day 24, Part 2
The puzzle text can be found here.
While I’m sure there is an algorithmic solution to Part 2, I solved this like I solved Advent of Code 2024 Day 14 - by visual inspection. This time using GraphViz and manually working out which wires need to move where. Thankfully we know there are four pair of wires, so working this out came down to a matter of time, coffee, and focus. And a lot of mumbling to myself.
I found this tedious but effective for two reasons:
-
I had trouble creating the graph until I stumbled across this comment by /u/burnt_heatshield on Reddit who suggested linking the x, y, and z nodes together to get a nice ladder effect. Otherwise, the graph that GraphViz creates is unruly and hard to follow. This made most of the errors stick out immediately when visualized. I also learned about subgraphs and this great online visualizer
-
Even though I could spot that an error was present visually, fixing it required a fair deal of pointing at my screen with multiple fingers and talking to myself. But again, that worked.
Here is the code I used to generate output that GraphViz can use.
// In Day24
fun solvePart2() {
val z = gates.filter { it.out.startsWith("z") }.map { it.out }.sorted().joinToString("->")
val x = z.replace('z', 'x')
val y = z.replace('z', 'y')
println(
"""
digraph G {
subgraph {
node [style=filled,color=green]
$z
}
subgraph {
node [style=filled,color=gray]
$x
}
subgraph {
node [style=filled,color=gray]
$y
}
subgraph {
node [style=filled,color=pink]
${gates.filter { gate -> gate.op == "AND" }.joinToString(" ") { gate -> gate.out }}
}
subgraph {
node [style=filled,color=yellow];
${gates.filter { gate -> gate.op == "OR" }.joinToString(" ") { gate -> gate.out }}
}
subgraph {
node [style=filled,color=lightblue];
${gates.filter { gate -> gate.op == "XOR" }.joinToString(" ") { gate -> gate.out }}
}
""".trimIndent()
)
gates.forEach { (left, right, _, out) ->
println(" $left -> $out")
println(" $right -> $out")
}
println("}")
}
When visualized, you can clearly make out that something isn’t right because the pattern suddenly changes:
Or it might be more subtle, which is why the types of operations have different colors:
Sorry these pictures aren’t larger - they have labels from my input and I don’t want to run afoul of the “no uploading your input” rule. Running your input through the code above and copying the resulting output through Graphviz will generate a graph specific to your output! I’ll leave it to you to work out the alterations that need to be made! :)
Star Earned! See you tomorrow for the last day of Advent of Code 2024!
Further Reading
- Index of All Solutions - All posts and solutions for 2024, in Kotlin.
- My Github repo - Solutions and tests for each day.
- Solution - Full code for day 24
- Advent of Code - Come join in and do these challenges yourself!