diff --git a/src/main/kotlin/no/rodland/advent/Misc.kt b/src/main/kotlin/no/rodland/advent/Misc.kt index f04431f1..4b9d6205 100644 --- a/src/main/kotlin/no/rodland/advent/Misc.kt +++ b/src/main/kotlin/no/rodland/advent/Misc.kt @@ -1,4 +1,5 @@ import no.rodland.advent.Pos +import java.math.BigInteger fun String.readFile(): List { @@ -20,6 +21,12 @@ fun String.readFileAsString(): String { } } +fun lcm(n1: BigInteger, n2: BigInteger): BigInteger { + // https://no.wikipedia.org/wiki/Minste_felles_multiplum + // lcm = (n1 * n2) / gcd + return (n1 * n2) / n1.gcd(n2) +} + fun String.readFileAsInt(): List { return readFile().map { it.toInt() } } diff --git a/src/main/kotlin/no/rodland/advent_2023/Day20.kt b/src/main/kotlin/no/rodland/advent_2023/Day20.kt index be3399eb..c001073a 100644 --- a/src/main/kotlin/no/rodland/advent_2023/Day20.kt +++ b/src/main/kotlin/no/rodland/advent_2023/Day20.kt @@ -1,41 +1,172 @@ package no.rodland.advent_2023 +import lcm import no.rodland.advent.Day +import java.math.BigInteger // template generated: 20/12/2023 // Fredrik Rødland 2023 class Day20(val input: List) : Day> { - private val parsed = input.parse() + val parsed = input.parse() override fun partOne(): Long { - return 2 + parsed.values.forEach { it.reset() } + repeat(1000) { sendPulse("button", Pulse.LOW) } + val high = parsed.values.sumOf { it.high } + val low = parsed.values.sumOf { it.low } + return high * low.toLong() } + override fun partTwo(): Long { - return 2 + parsed.values.forEach { it.reset() } + // rx gets a low pulse when nextToLast sends a low pulse + // nextToLast sends a low pulse when all beforeNextToLast are high + val nextToLast = parsed.values.single { it.destinations.contains("rx") } + val beforeNextToLast = parsed.values.filter { it.destinations.contains(nextToLast.id) }.map { it.id } + val periods = beforeNextToLast.associateWith { -1L }.toMutableMap() + + var count = 0L + while (true) { + val result = sendPulse("button", Pulse.LOW, periods, ++count) + if (result != 0L) { + return result + } + } + } + + private fun checkPart2(source: String, periods: MutableMap, pulse: Pulse, count: Long): Boolean { + if (source in periods) { + if (pulse == Pulse.HIGH && periods[source]!! == -1L) { + periods[source] = count + } + if (periods.values.all { it != -1L }) { + println("all found: $periods") + return true + } + } + return false + } + + + private fun sendPulse(mod: String, pulse: Pulse, periods: MutableMap = mutableMapOf(), count: Long = 0L): Long { + val deque = ArrayDeque() + deque.addAll(parsed[mod]!!.pulse("button", pulse).map { QueueElement(mod, it.second, it.first) }) + while (deque.isNotEmpty()) { + deque.removeFirst().let { (source, pulse, destination) -> + if (checkPart2(source, periods, pulse, count)) return periods.values.map { it.toBigInteger() }.fold(BigInteger.ONE) { acc, i -> lcm(acc, i) }.toLong() + val dest = parsed[destination]!! + val addToQueue = dest.pulseWithCount(source, pulse) + deque.addAll(addToQueue.map { QueueElement(destination, it.second, it.first) }) + } + } + return 0L + } + + + data class QueueElement(val source: String, val pulse: Pulse, val destination: String) + enum class Pulse { HIGH, LOW } + + sealed class Mod(val id: String, val destinations: List) { + var high = 0 + var low = 0 + override fun toString(): String { + return "$id, $low, $high" + } + + fun reset() { + high = 0 + low = 0 + resetSub() + } + + fun pulseWithCount(source: String, pulse: Pulse): List> { + if (pulse == Pulse.HIGH) { + high++ + } else { + low++ + } + return pulse(source, pulse) + } + + abstract fun resetSub() + abstract fun pulse(source: String, pulse: Pulse): List> + } + + class FlipFlop(id: String, destinations: List) : Mod(id, destinations) { + private var memory = false + override fun resetSub() { + memory = false + } + + override fun pulse(source: String, pulse: Pulse): List> { + return if (pulse == Pulse.LOW) { + val pulseToSend = if (!memory) Pulse.HIGH else Pulse.LOW + memory = !memory + destinations.map { it to pulseToSend } + } else { + emptyList() + } + + } + } + + data class Conjunction(val iD: String, val dest: List, val sources: List = emptyList()) : Mod(iD, dest) { + private val memory = sources.associateWith { Pulse.LOW }.toMutableMap() + override fun resetSub() { + memory.replaceAll { _, _ -> Pulse.LOW } + } + + override fun pulse(source: String, pulse: Pulse): List> { + memory[source] = pulse + val pulseToSend = if (memory.values.all { it == Pulse.HIGH }) { + Pulse.LOW + } else { + Pulse.HIGH + } + return destinations.map { it to pulseToSend } + } + } + + class Forwarder(iD: String, destinations: List) : Mod(iD, destinations) { + override fun resetSub() {} + + override fun pulse(source: String, pulse: Pulse): List> = destinations.map { it to pulse } + } + + class NA(iD: String) : Mod(iD, emptyList()) { + override fun resetSub() {} + override fun pulse(source: String, pulse: Pulse): List> = emptyList() } - sealed class Mod(val id: String, val destinations: List) - class FlipFlop(id: String, destinations: List) : Mod(id, destinations) - class Broadcast(destinations: List) : Mod("broadcaster", destinations) - class Conjunction(id: String, destinations: List) : Mod(id, destinations) override fun List.parse(): Map { - return associate { line -> - println(line) + val mods = map { line -> val (id, list) = line.split(" -> ") - val destinations = list.split(";", " ").filterNot { it.isEmpty() } + val destinations = list.split(";", " ", ",").filterNot { it.isEmpty() } + val mod = when { id.startsWith("&") -> Conjunction(id.drop(1), destinations) id.startsWith("%") -> FlipFlop(id.drop(1), destinations) - id == "broadcaster" -> Broadcast(destinations) + id == "broadcaster" -> Forwarder("broadcaster", destinations) else -> error("unknown mod: $id") } - mod.id to mod + mod + } + val na = mods.flatMap { it.destinations }.distinct().filterNot { it in mods.map { it.id } }.map { + NA(it) } + return (mods + na + Forwarder("button", listOf("broadcaster"))).map { mod -> + if (mod is Conjunction) { + mod.copy(sources = mods.filter { mod.id in it.destinations }.map { it.id }) + } else { + mod + } + }.associateBy { it.id } } + override val day = "20".toInt() } diff --git a/src/test/kotlin/no/rodland/advent_2023/Day20Test.kt b/src/test/kotlin/no/rodland/advent_2023/Day20Test.kt index cac94801..4e690c6c 100644 --- a/src/test/kotlin/no/rodland/advent_2023/Day20Test.kt +++ b/src/test/kotlin/no/rodland/advent_2023/Day20Test.kt @@ -16,11 +16,11 @@ internal class Day20Test { private val test20 = "2023/input_20_test.txt".readFile() private val test20_2 = "2023/input_20_test_2.txt".readFile() - private val resultTestOne = 2L + private val resultTestOne = 32000000L private val resultTestTwo = 2L - private val resultTestTwo_2 = 2L - private val resultOne = 2L - private val resultTwo = 2L + private val resultTestTwo_2 = 11687500L + private val resultOne = 819397964L + private val resultTwo = 252667369442479L val test = defaultTestSuiteParseOnInit( Day20(data20), @@ -31,6 +31,8 @@ internal class Day20Test { resultTwo, { Day20(data20) }, { Day20(test20) }, + numTestPart1 = 20, + numTestPart2 = 4 ) @Nested @@ -62,6 +64,7 @@ internal class Day20Test { fun `20,1,test`() { report(test.testPart1) } + @Test fun `20,1,test,2`() { report(test.testPart1.copy(function = { Day20(test20_2).partOne() }, expected = resultTestTwo_2)) @@ -75,10 +78,10 @@ internal class Day20Test { @Nested inner class `Part 2` { - @Test - fun `20,2,test`() { - report(test.testPart2) - } +// @Test +// fun `20,2,test`() { +// report(test.testPart2) +// } @Test fun `20,2,live,1`() {