Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ as the value which passed the given day/phase combination
* Day 03
* Regex shining today! Sorting by the match location made the single list of all matches easy to scan through.
* Day 12
* Not a day where I had the best solution; but a solution nonetheless. Slow scan of each field's border; ain't much, but it's honest work.
* Not a day where I had the best solution; but a solution nonetheless. Slow scan of each field's border; ain't much, but it's honest work.
* Day 24
* I had to pen + paper the finding of the wires; while renaming my output to be what they actually are (i.e. `x01 XOR y01 -> sum1`). But then coming up with the actual rules to mimic my manual process was fun. And in the process learned a lot about binary addition at the logic gate level.

### Interesting approaches:

Expand Down
283 changes: 283 additions & 0 deletions src/main/kotlin/me/peckb/aoc/_2024/calendar/day24/Day24.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
package me.peckb.aoc._2024.calendar.day24

import javax.inject.Inject
import me.peckb.aoc.generators.InputGenerator.InputGeneratorFactory
import java.util.LinkedList

class Day24 @Inject constructor(
private val generatorFactory: InputGeneratorFactory,
) {
fun partOne(filename: String) = generatorFactory.forFile(filename).read { input ->
var readingBase = true

val data = mutableMapOf<String, Lazy<Int>>()
val zValues = mutableListOf<String>()

input.forEach input@{ line ->
if (line.isEmpty()) {
readingBase = false
return@input
}
if (readingBase) {
line.split(": ").let { (key, value) -> data[key] = lazy { value.toInt() } }
} else {
line.split(" ").let { (first, operations, second, _, result) ->
if (result.startsWith('z')) { zValues.add(result) }

val op = findOperation(operations)
data[result] = lazy { op(data[first]!!.value, data[second]!!.value) }
}
}
}

zValues.sortedDescending()
.map { data[it]!!.value }
.joinToString("")
.toLong(2)
}

fun partTwo(filename: String) = generatorFactory.forFile(filename).read { input ->
var readingBase = true
val data = mutableMapOf<String, () -> Int>()
val xValues = mutableListOf<String>()
val yValues = mutableListOf<String>()
val zValues = mutableListOf<String>()
val wirings = mutableMapOf<String, Wiring>()

input.forEach input@{ line ->
if (line.isEmpty()) {
readingBase = false
return@input
}
if (readingBase) {
line.split(": ").let { (key, value) ->
if (key.startsWith('x')) { xValues.add(key) }
if (key.startsWith('y')) { yValues.add(key) }
data[key] = { value.toInt() }
}
} else {
line.split(" ").let { (a, operation, b, _, result) ->
if (result.startsWith('z')) { zValues.add(result) }

val op = findOperation(operation)

wirings[result] = Wiring(a, operation, b, result)

data[result] = { op(data[a]!!(), data[b]!!()) }
}
}
}

val swaps = mutableMapOf<String, String>()

fun swap(swap: Pair<String, String>) {
val oldA = wirings[swap.first]!!
val oldB = wirings[swap.second]!!

val newB = oldB.copy(result = oldA.result)
val newA = oldA.copy(result = oldB.result)

wirings[swap.first] = newB
wirings[swap.second] = newA

val opA = findOperation(newA.op)
val opB = findOperation(newB.op)

data[newA.result] = { opA(data[newA.a]!!(), data[newA.b]!!()) }
data[newB.result] = { opB(data[newB.a]!!(), data[newB.b]!!()) }

swaps[swap.first] = swap.second
swaps[swap.second] = swap.first
}

val correctWirings = mutableMapOf<String, List<Wiring>>()

(0 .. 45).forEach loop@{ n ->
val previousPreviousKey = "z${(n-2).toString().padStart(2, '0')}"
val previousKey = "z${(n-1).toString().padStart(2, '0')}"
val key = "z${n.toString().padStart(2, '0')}"
when (n) {
0 -> checkZero(wirings)?.also { swap(it) }
1 -> checkOne(wirings)?.also { swap(it) }
in (2..44) -> checkDefault(
wirings = wirings,
key = n to key,
previousWirings = n - 1 to previousKey,
previousPreviousWirings = n - 2 to previousPreviousKey,
correctWirings = correctWirings
)?.also { swap(it) }
// don't need to check the last - the error won't be there.
}
correctWirings[key] = getMyWirings(key, wirings)
}

if (swaps.size != 8) { throw IllegalStateException("Did not find all the swaps") }

val binaryX = xValues.sortedDescending().map { data[it]!!() }.joinToString("")
val binaryY = yValues.sortedDescending().map { data[it]!!() }.joinToString("")
val expectedResult = (binaryX.toLong(2) + binaryY.toLong(2)).toString(2)
val actualResult = zValues.sortedDescending().map { data[it]!!() }.joinToString("")

if (actualResult != expectedResult) { throw IllegalStateException("We found eight swaps, but didn't get the right result") }

swaps.keys.sorted().joinToString(",")
}

private fun findOperation(op: String) : (Int, Int) -> Int {
return when (op) {
"AND" -> Int::and
"OR" -> Int::or
"XOR" -> Int::xor
else -> throw IllegalArgumentException("Unknown operation $op")
}
}

private fun checkZero(wirings: MutableMap<String, Wiring>) : Pair<String, String>? {
val z00Wiring = wirings["z00"]!!
val expectedInput = setOf("x00", "y00")
return if (z00Wiring.op != "XOR" || expectedInput != z00Wiring.input()) {
// find `x00 XOR y00 = ???`
val itemToSwapTo = wirings.entries.first { (_, w) ->
val (a, op, b, _) = w
op == "XOR" && expectedInput == setOf(a, b)
}
return "z00" to itemToSwapTo.key
} else {
null
}
}

private fun checkOne(wirings: MutableMap<String, Wiring>): Pair<String, String>? {
val z01Wiring = wirings["z01"]!!

// aaa XOR bbb = z01
// y01 XOR x01 = bbb
// x00 AND y00 = aaa
val aaaInput = setOf("x00", "y00")
val bbbInput = setOf("x01", "y01")

val aaa = wirings.entries.first { (_, w) -> w.op == "AND" && aaaInput == setOf(w.a, w.b) }
val bbb = wirings.entries.first { (_, w) -> w.op == "XOR" && bbbInput == setOf(w.a, w.b) }

val correctInput = setOf(aaa.key, bbb.key)
if (z01Wiring.input() == correctInput) {
return null
} else {
// is there a `aaa.key AND bbb.key` which we need to swap to z01?
val maybeZSwap = wirings.entries.find { (_, w) -> w.op == "XOR" && setOf(w.a, w.b) == setOf(aaa.key, bbb.key) }
if (maybeZSwap != null) {
return "z01" to maybeZSwap.key
} else {
// our "z01" was correct - so the input needs swapping
return if (!z01Wiring.input().contains(aaa.key) && !z01Wiring.input().contains(bbb.key)) {
// full disjointSet - this should not happen in the input
throw IllegalStateException("Input has full disjoint set!")
} else {
// one of the items is missing
if (aaa.key in z01Wiring.input()) {
// bbb needs swap
bbb.key to z01Wiring.input().minus(aaa.key).first()
} else {
// aaa needs swap
aaa.key to z01Wiring.input().minus(bbb.key).first()
}
}
}
}
}

fun checkDefault(
wirings: MutableMap<String, Wiring>,
key: Pair<Int, String>,
previousWirings: Pair<Int, String>,
previousPreviousWirings: Pair<Int, String>,
correctWirings: Map<String, List<Wiring>>,
): Pair<String, String>? {
val myWirings = getMyWirings(key.second, wirings)

val currentWirings = myWirings.minus(correctWirings[previousWirings.second]!!)
val previousNewWirings = correctWirings[previousWirings.second]!!.minus(correctWirings[previousPreviousWirings.second]!!)

// the items we need ...
// for zN
// sum(N-1) AND carryChain(N-1) = carryAfter(N-1)
// y(N-1) AND x(N-1) = carry(N-1)
// carryAfter(N-1) OR carry(N-1) = carryChainN
// yN XOR yN = sumN
// sumN XOR carryChainN = zN

// sum(N-1)
val x1 = previousWirings.first.let { "x${it.toString().padStart(2, '0')}" }
val y1 = previousWirings.first.let { "y${it.toString().padStart(2, '0')}" }
val sumN1 = previousNewWirings.first { w -> w.op == "XOR" && setOf(w.a, w.b) == setOf(x1, y1) }

// carry(N-1)
val carryN1 = wirings.entries.first { (_, w) -> w.op == "AND" && setOf(w.a, w.b) == setOf(x1, y1) }.value

// sumN
val x = key.first.let { "x${it.toString().padStart(2, '0')}" }
val y = key.first.let { "y${it.toString().padStart(2, '0')}" }
val sumN = wirings.entries.first { (_, w) -> w.op == "XOR" && setOf(w.a, w.b) == setOf(x, y) }.value

// carryChain(N-1)
val carryChainN1 = previousNewWirings.find { w -> w.op == "OR" } ?: previousNewWirings.first { w -> w.op == "AND" }
// carryAfter(N-1)
val carryAfterN1 = wirings.entries.find { (_, w) -> w.op == "AND" && setOf(w.a, w.b) == setOf(sumN1.result, carryChainN1.result) }?.value
if (carryAfterN1 == null) {
// doesn't happen on input
throw IllegalArgumentException("Something Wrong with $carryChainN1 or $sumN1")
}

// carryChainN
val carryChainN = wirings.entries.find { (_, w) -> w.op == "OR" && setOf(w.a, w.b) == setOf(carryAfterN1.result, carryN1.result) }?.value
if (carryChainN == null) {
// doesn't happen on input
throw IllegalArgumentException("Something Wrong with $carryAfterN1 or $carryN1")
}

// zN
val zN = wirings.entries.find { (_, w) -> w.op == "XOR" && setOf(w.a, w.b) == setOf(sumN.result, carryChainN.result) }?.value
if (zN == null) {
val correctZN = wirings[key.second]!!
return if (carryChainN.result in correctZN.input()) {
// something bad with sumN
sumN.result to correctZN.input().minus(carryChainN.result).first()
} else {
// something bad with carryChainN
carryChainN.result to correctZN.input().minus(sumN.result).first()
}
}

if (setOf(carryN1, sumN, carryAfterN1, carryChainN, zN) != currentWirings.toSet()) {
// if we got this far - we need to swap out zN values
val toSwapWith = currentWirings.first { it.result == key.second }
return toSwapWith.result to zN.result
}

return null
}

private fun getMyWirings(key: String, wirings: MutableMap<String, Wiring>): MutableList<Wiring> {
val myWirings = mutableListOf<Wiring>()
val toCheck = LinkedList<String>()
toCheck.add(key)
while(toCheck.isNotEmpty()) {
val wiring = wirings[toCheck.poll()]!!
val (a, op, b, r) = wiring

myWirings.add(wiring)

val aIsInput = a.startsWith('x') || a.startsWith('y')
val bIsInput = b.startsWith('x') || b.startsWith('y')

if (!aIsInput) { toCheck.add(a) }
if (!bIsInput) { toCheck.add(b) }
}

return myWirings
}
}

data class Wiring(val a: String, val op: String, val b: String, val result: String) {
fun input() = setOf(a, b)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
## [Day 24: Crossed Wires](https://adventofcode.com/2024/day/24)
2 changes: 2 additions & 0 deletions src/test/kotlin/me/peckb/aoc/_2024/TestDayComponent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import me.peckb.aoc._2024.calendar.day20.Day20Test
import me.peckb.aoc._2024.calendar.day21.Day21Test
import me.peckb.aoc._2024.calendar.day22.Day22Test
import me.peckb.aoc._2024.calendar.day23.Day23Test
import me.peckb.aoc._2024.calendar.day24.Day24Test
import javax.inject.Singleton
import me.peckb.aoc.DayComponent
import me.peckb.aoc.InputModule
Expand Down Expand Up @@ -54,4 +55,5 @@ internal interface TestDayComponent : DayComponent {
fun inject(day21Test: Day21Test)
fun inject(day22Test: Day22Test)
fun inject(day23Test: Day23Test)
fun inject(day24Test: Day24Test)
}
32 changes: 32 additions & 0 deletions src/test/kotlin/me/peckb/aoc/_2024/calendar/day24/Day24Test.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package me.peckb.aoc._2024.calendar.day24

import javax.inject.Inject

import me.peckb.aoc._2024.DaggerTestDayComponent
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

internal class Day24Test {
@Inject
lateinit var day24: Day24

@BeforeEach
fun setup() {
DaggerTestDayComponent.create().inject(this)
}

@Test
fun testDay24PartOne() {
assertEquals(55544677167336, day24.partOne(DAY_24))
}

@Test
fun testDay24PartTwo() {
assertEquals("gsd,kth,qnf,tbt,vpm,z12,z26,z32", day24.partTwo(DAY_24))
}

companion object {
private const val DAY_24: String = "advent-of-code-input/2024/day24.input"
}
}
Loading