-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
362 additions
and
0 deletions.
There are no files selected for viewing
162 changes: 162 additions & 0 deletions
162
src/main/kotlin/biz/koziolek/adventofcode/year2023/day20/day20.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
package biz.koziolek.adventofcode.year2023.day20 | ||
|
||
import biz.koziolek.adventofcode.findInput | ||
|
||
fun main() { | ||
val inputFile = findInput(object {}) | ||
val modules = parseModules(inputFile.bufferedReader().readLines()) | ||
println("low pulses * high pulses: ${countPulses(modules, buttonPresses = 1000)}") | ||
} | ||
|
||
sealed interface Module { | ||
val name: String | ||
val destinations: List<String> | ||
fun processPulse(pulse: Pulse): Pair<Module, Pulse.Level?> | ||
} | ||
|
||
data class BroadcasterModule(override val destinations: List<String>) : Module { | ||
override val name = "broadcaster" | ||
override fun processPulse(pulse: Pulse) = this to pulse.level | ||
} | ||
|
||
data class FlipFlopModule(override val name: String, | ||
override val destinations: List<String>, | ||
val isOn: Boolean = false) : Module { | ||
private fun flip() = copy(isOn = !isOn) | ||
|
||
override fun processPulse(pulse: Pulse): Pair<Module, Pulse.Level?> = | ||
when (pulse.level) { | ||
Pulse.Level.HIGH -> this to null | ||
Pulse.Level.LOW -> this.flip() to if (isOn) Pulse.Level.LOW else Pulse.Level.HIGH | ||
} | ||
} | ||
|
||
data class ConjunctionModule(override val name: String, | ||
val inputs: Set<String>, | ||
override val destinations: List<String>, | ||
val recentPulses: Map<String, Pulse.Level> = inputs.associateWith { Pulse.Level.LOW }) : Module { | ||
override fun processPulse(pulse: Pulse): Pair<Module, Pulse.Level?> { | ||
val newModule = this.update(pulse.from, pulse.level) | ||
|
||
return if (newModule.recentPulses.values.all { it == Pulse.Level.HIGH }) { | ||
newModule to Pulse.Level.LOW | ||
} else { | ||
newModule to Pulse.Level.HIGH | ||
} | ||
} | ||
|
||
private fun update(from: String, level: Pulse.Level): ConjunctionModule = | ||
copy(recentPulses = recentPulses + mapOf(from to level)) | ||
} | ||
|
||
data class UntypedModule(override val name: String, | ||
override val destinations: List<String>, | ||
val lastPulse: Pulse.Level = Pulse.Level.LOW) : Module { | ||
override fun processPulse(pulse: Pulse) = this to pulse.level | ||
} | ||
|
||
data class Pulse(val from: String, val to: String, val level: Level) { | ||
enum class Level { LOW, HIGH } | ||
|
||
override fun toString() = "$from -${level.name.lowercase()}-> $to" | ||
|
||
companion object { | ||
val BUTTON_PRESS = Pulse(from = "button", to = "broadcaster", level = Level.LOW) | ||
} | ||
} | ||
|
||
fun parseModules(lines: List<String>): Map<String, Module> { | ||
val inputToOutputMap = lines.associate { line -> | ||
val (namePart, destPart) = line.split("->") | ||
val nameAndType = namePart.trim() | ||
val destinations = destPart.split(',').map { it.trim() } | ||
nameAndType to destinations | ||
} | ||
|
||
return inputToOutputMap.map { (nameAndType, destinations) -> | ||
when { | ||
nameAndType == "broadcaster" -> | ||
BroadcasterModule(destinations) | ||
|
||
nameAndType.startsWith('%') -> | ||
FlipFlopModule( | ||
name = getName(nameAndType), | ||
destinations = destinations | ||
) | ||
|
||
nameAndType.startsWith('&') -> | ||
ConjunctionModule( | ||
name = getName(nameAndType), | ||
inputs = inputToOutputMap | ||
.filterValues { it.contains(getName(nameAndType)) } | ||
.keys | ||
.map { getName(it) } | ||
.toSet(), | ||
destinations = destinations | ||
) | ||
|
||
else -> | ||
UntypedModule(nameAndType, destinations) | ||
} | ||
}.associateBy { it.name } | ||
} | ||
|
||
private fun getName(nameAndType: String): String = | ||
when { | ||
nameAndType.startsWith('%') -> nameAndType.drop(1) | ||
nameAndType.startsWith('&') -> nameAndType.drop(1) | ||
else -> nameAndType | ||
} | ||
|
||
fun generatePulses(modules: Map<String, Module>, initialPulse: Pulse): Sequence<Pair<Pulse, Map<String, Module>>> = | ||
sequence { | ||
val pulsesToProcess = mutableListOf(initialPulse) | ||
var currentModules = modules | ||
|
||
while (pulsesToProcess.isNotEmpty()) { | ||
val pulse = pulsesToProcess.removeFirst() | ||
val module = currentModules[pulse.to] | ||
|
||
if (module != null) { | ||
val (newModule, nextLevel) = module.processPulse(pulse) | ||
currentModules = currentModules + (newModule.name to newModule) | ||
|
||
if (nextLevel != null) { | ||
pulsesToProcess.addAll(newModule.destinations.map { | ||
Pulse( | ||
from = newModule.name, | ||
to = it, | ||
level = nextLevel | ||
) | ||
}) | ||
} | ||
} | ||
|
||
yield(pulse to currentModules) | ||
} | ||
} | ||
|
||
fun Sequence<Pair<Pulse, Map<String, Module>>>.getLatestModulesAndAllPulses(): Pair<Map<String, Module>, List<Pulse>> = | ||
fold(Pair(emptyMap(), emptyList())) { (_, pulses), (pulse, newModules) -> | ||
newModules to (pulses + pulse) | ||
} | ||
|
||
fun countPulses(modules: Map<String, Module>, buttonPresses: Int): Long { | ||
var currentModules = modules | ||
var lowCount = 0L | ||
var highCount = 0L | ||
|
||
for (i in 1..buttonPresses) { | ||
for ((pulse, newModules) in generatePulses(currentModules, Pulse.BUTTON_PRESS)) { | ||
currentModules = newModules | ||
|
||
if (pulse.level == Pulse.Level.LOW) { | ||
lowCount++ | ||
} else { | ||
highCount++ | ||
} | ||
} | ||
} | ||
|
||
return lowCount * highCount | ||
} |
58 changes: 58 additions & 0 deletions
58
src/main/resources/biz/koziolek/adventofcode/year2023/day20/input
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
%nr -> mr | ||
&sx -> zh | ||
%rk -> dc, bl | ||
%lx -> rs | ||
%hx -> bl | ||
%hp -> bj | ||
%dk -> mr, lf | ||
%hc -> xc | ||
%bj -> vv, rd | ||
&jt -> zh | ||
&bl -> ks, kn, dc, hc, zk | ||
&zh -> rx | ||
%sp -> hz, bl | ||
%rd -> vv, tp | ||
%cg -> dk | ||
%rg -> jl, pv | ||
%jl -> js | ||
%fb -> vv, zd | ||
%gv -> lx | ||
%lr -> vj, bl | ||
%vz -> hc, bl | ||
%kn -> bl, zk | ||
%rj -> mr, nr | ||
%cn -> pv, sb | ||
%rs -> vv, hp | ||
&mr -> qc, kb, gc, vl, bs, cg, lf | ||
%rb -> qj | ||
%sm -> bv, vv | ||
%dh -> rg | ||
%zk -> vz | ||
%qj -> xs, pv | ||
%ng -> ql, pv | ||
%vj -> bl, sp | ||
&kb -> zh | ||
%sb -> pv | ||
%vl -> mr, cz | ||
%dc -> lr | ||
%xc -> rk, bl | ||
%cz -> cg, mr | ||
%hz -> bl, hx | ||
%xs -> pv, cn | ||
%js -> ng | ||
%cb -> mr, nc | ||
%qb -> vv | ||
%gc -> qc | ||
%bv -> qb, vv | ||
broadcaster -> kn, fb, ln, vl | ||
%bs -> cb | ||
%lf -> gc | ||
%nc -> mr, rj | ||
%ln -> pv, dh | ||
%qc -> bs | ||
&vv -> zd, jt, fb, hp, gv, lx | ||
&ks -> zh | ||
%ql -> rb | ||
%tp -> sm, vv | ||
&pv -> sx, dh, jl, ln, js, rb, ql | ||
%zd -> gv |
142 changes: 142 additions & 0 deletions
142
src/test/kotlin/biz/koziolek/adventofcode/year2023/day20/Day20Test.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
package biz.koziolek.adventofcode.year2023.day20 | ||
|
||
import biz.koziolek.adventofcode.findInput | ||
import org.junit.jupiter.api.Assertions.* | ||
import org.junit.jupiter.api.Tag | ||
import org.junit.jupiter.api.Test | ||
|
||
@Tag("2023") | ||
internal class Day20Test { | ||
|
||
private val sampleInput1 = """ | ||
broadcaster -> a, b, c | ||
%a -> b | ||
%b -> c | ||
%c -> inv | ||
&inv -> a | ||
""".trimIndent().split("\n") | ||
|
||
private val sampleInput2 = """ | ||
broadcaster -> a | ||
%a -> inv, con | ||
&inv -> b | ||
%b -> con | ||
&con -> output | ||
""".trimIndent().split("\n") | ||
|
||
@Test | ||
fun testParse() { | ||
val modules1 = parseModules(sampleInput1) | ||
assertEquals( | ||
mapOf( | ||
"broadcaster" to BroadcasterModule(destinations = listOf("a", "b", "c")), | ||
"a" to FlipFlopModule(name = "a", destinations = listOf("b")), | ||
"b" to FlipFlopModule(name = "b", destinations = listOf("c")), | ||
"c" to FlipFlopModule(name = "c", destinations = listOf("inv")), | ||
"inv" to ConjunctionModule(name = "inv", inputs = setOf("c"), destinations = listOf("a")), | ||
), | ||
modules1 | ||
) | ||
|
||
val modules2 = parseModules(sampleInput2) | ||
assertEquals( | ||
mapOf( | ||
"broadcaster" to BroadcasterModule(destinations = listOf("a")), | ||
"a" to FlipFlopModule(name = "a", destinations = listOf("inv", "con")), | ||
"inv" to ConjunctionModule(name = "inv", inputs = setOf("a"), destinations = listOf("b")), | ||
"b" to FlipFlopModule(name = "b", destinations = listOf("con")), | ||
"con" to ConjunctionModule(name = "con", inputs = setOf("a", "b"), destinations = listOf("output")), | ||
), | ||
modules2 | ||
) | ||
} | ||
|
||
@Test | ||
fun testGeneratePulses() { | ||
val modules1 = parseModules(sampleInput1) | ||
val (_, pulses1) = generatePulses(modules1, initialPulse = Pulse.BUTTON_PRESS) | ||
.take(12) | ||
.getLatestModulesAndAllPulses() | ||
assertEquals( | ||
""" | ||
button -low-> broadcaster | ||
broadcaster -low-> a | ||
broadcaster -low-> b | ||
broadcaster -low-> c | ||
a -high-> b | ||
b -high-> c | ||
c -high-> inv | ||
inv -low-> a | ||
a -low-> b | ||
b -low-> c | ||
c -low-> inv | ||
inv -high-> a | ||
""".trimIndent(), | ||
pulses1.joinToString("\n") { it.toString() } | ||
) | ||
|
||
val modules2 = parseModules(sampleInput2) | ||
val (modules2b, pulses2) = generatePulses(modules2, initialPulse = Pulse.BUTTON_PRESS) | ||
.getLatestModulesAndAllPulses() | ||
assertEquals( | ||
""" | ||
button -low-> broadcaster | ||
broadcaster -low-> a | ||
a -high-> inv | ||
a -high-> con | ||
inv -low-> b | ||
con -high-> output | ||
b -high-> con | ||
con -low-> output | ||
""".trimIndent(), | ||
pulses2.joinToString("\n") { it.toString() } | ||
) | ||
|
||
val (modules2c, pulses2b) = generatePulses(modules2b, initialPulse = Pulse.BUTTON_PRESS) | ||
.getLatestModulesAndAllPulses() | ||
assertEquals( | ||
""" | ||
button -low-> broadcaster | ||
broadcaster -low-> a | ||
a -low-> inv | ||
a -low-> con | ||
inv -high-> b | ||
con -high-> output | ||
""".trimIndent(), | ||
pulses2b.joinToString("\n") { it.toString() } | ||
) | ||
|
||
val (_, pulses2c) = generatePulses(modules2c, initialPulse = Pulse.BUTTON_PRESS) | ||
.getLatestModulesAndAllPulses() | ||
assertEquals( | ||
""" | ||
button -low-> broadcaster | ||
broadcaster -low-> a | ||
a -high-> inv | ||
a -high-> con | ||
inv -low-> b | ||
con -low-> output | ||
b -low-> con | ||
con -high-> output | ||
""".trimIndent(), | ||
pulses2c.joinToString("\n") { it.toString() } | ||
) | ||
} | ||
|
||
@Test | ||
fun testSampleAnswer1() { | ||
val modules1 = parseModules(sampleInput1) | ||
assertEquals(32000000, countPulses(modules1, buttonPresses = 1000)) | ||
|
||
val modules2 = parseModules(sampleInput2) | ||
assertEquals(11687500, countPulses(modules2, buttonPresses = 1000)) | ||
} | ||
|
||
@Test | ||
@Tag("answer") | ||
fun testAnswer1() { | ||
val input = findInput(object {}).bufferedReader().readLines() | ||
val modules = parseModules(input) | ||
assertEquals(777666211, countPulses(modules, buttonPresses = 1000)) | ||
} | ||
} |