Skip to content

Commit

Permalink
Solution for 2023-12-20, part 1
Browse files Browse the repository at this point in the history
  • Loading branch information
pkoziol committed Dec 20, 2023
1 parent 7b2adf9 commit ba94689
Show file tree
Hide file tree
Showing 3 changed files with 362 additions and 0 deletions.
162 changes: 162 additions & 0 deletions src/main/kotlin/biz/koziolek/adventofcode/year2023/day20/day20.kt
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 src/main/resources/biz/koziolek/adventofcode/year2023/day20/input
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 src/test/kotlin/biz/koziolek/adventofcode/year2023/day20/Day20Test.kt
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))
}
}

0 comments on commit ba94689

Please sign in to comment.