diff --git a/README.md b/README.md index bd51cb4a..8e536fc1 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/src/main/kotlin/me/peckb/aoc/_2024/calendar/day24/Day24.kt b/src/main/kotlin/me/peckb/aoc/_2024/calendar/day24/Day24.kt new file mode 100644 index 00000000..26f7b1b1 --- /dev/null +++ b/src/main/kotlin/me/peckb/aoc/_2024/calendar/day24/Day24.kt @@ -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>() + val zValues = mutableListOf() + + 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 Int>() + val xValues = mutableListOf() + val yValues = mutableListOf() + val zValues = mutableListOf() + val wirings = mutableMapOf() + + 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() + + fun swap(swap: Pair) { + 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>() + + (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) : Pair? { + 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): Pair? { + 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, + key: Pair, + previousWirings: Pair, + previousPreviousWirings: Pair, + correctWirings: Map>, + ): Pair? { + 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): MutableList { + val myWirings = mutableListOf() + val toCheck = LinkedList() + 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) +} \ No newline at end of file diff --git a/src/main/kotlin/me/peckb/aoc/_2024/calendar/day24/README.md b/src/main/kotlin/me/peckb/aoc/_2024/calendar/day24/README.md new file mode 100644 index 00000000..68a43848 --- /dev/null +++ b/src/main/kotlin/me/peckb/aoc/_2024/calendar/day24/README.md @@ -0,0 +1 @@ +## [Day 24: Crossed Wires](https://adventofcode.com/2024/day/24) \ No newline at end of file diff --git a/src/test/kotlin/me/peckb/aoc/_2024/TestDayComponent.kt b/src/test/kotlin/me/peckb/aoc/_2024/TestDayComponent.kt index ec833387..96f411e8 100644 --- a/src/test/kotlin/me/peckb/aoc/_2024/TestDayComponent.kt +++ b/src/test/kotlin/me/peckb/aoc/_2024/TestDayComponent.kt @@ -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 @@ -54,4 +55,5 @@ internal interface TestDayComponent : DayComponent { fun inject(day21Test: Day21Test) fun inject(day22Test: Day22Test) fun inject(day23Test: Day23Test) + fun inject(day24Test: Day24Test) } diff --git a/src/test/kotlin/me/peckb/aoc/_2024/calendar/day24/Day24Test.kt b/src/test/kotlin/me/peckb/aoc/_2024/calendar/day24/Day24Test.kt new file mode 100644 index 00000000..da37a82e --- /dev/null +++ b/src/test/kotlin/me/peckb/aoc/_2024/calendar/day24/Day24Test.kt @@ -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" + } +}