From 4408075c947e723baee4a48d46f26d8f63534aac Mon Sep 17 00:00:00 2001 From: Paul-Henri Froidmont Date: Thu, 21 Dec 2023 13:01:01 +0100 Subject: [PATCH] Day 20 --- input/day20 | 58 ++++++++++++++++++++ src/day20.scala | 140 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 input/day20 create mode 100644 src/day20.scala diff --git a/input/day20 b/input/day20 new file mode 100644 index 0000000..1f87334 --- /dev/null +++ b/input/day20 @@ -0,0 +1,58 @@ +%dj -> fj, jn +%xz -> cm +%fn -> rj +%fv -> nt, zp +%ls -> ph, cf +%rk -> zp, tp +&jn -> km, cr, vz +%nh -> ph, ls +%tx -> gb +%xg -> dv, zp +%tp -> gx +&zp -> kj, kz, gx, fv, lv, tp +&gq -> rx +%fj -> sl, jn +%cr -> vz, jn +%rt -> fn, mf +%kj -> tt +%tk -> mg, ph +%xt -> jn, gh +%qx -> bx +%lv -> sx +%nz -> dp, ph +%sx -> kj, zp +%dd -> bf +%gb -> jp +%bj -> ph, nn +%sk -> mf +%bx -> tx, mf +%mt -> xg, zp +%vz -> hf +%vx -> mf, sk +%tt -> mt, zp +%br -> jn, fk +&xj -> gq +%mg -> ph, ps +%nt -> zp, rk +&qs -> gq +%rj -> qx, mf +%bf -> vx, mf +&kz -> gq +%fk -> jn, gk +%dv -> zp +%dp -> ph +&mf -> gb, tx, xj, dd, qx, rt, fn +&ph -> nn, xz, tk, ps, qs +%ps -> xz +&km -> gq +broadcaster -> fv, cr, rt, tk +%gk -> jn, xt +%cf -> ph, nz +%tl -> jn, br +%cm -> bj, ph +%nn -> nh +%jp -> mf, dd +%gh -> jn, dj +%hf -> tl, jn +%sl -> jn +%gx -> lv diff --git a/src/day20.scala b/src/day20.scala new file mode 100644 index 0000000..3bbd4da --- /dev/null +++ b/src/day20.scala @@ -0,0 +1,140 @@ +package aoc +package day20 + +import scala.annotation.tailrec +import scala.collection.mutable +import scala.util.boundary + +val dayNumber = "20" + +@main def part1: Unit = + println(part1(loadInput(dayNumber))) + +@main def part2: Unit = + println(part2(loadInput(dayNumber))) + +enum Pulse: + case High, Low + +enum Module: + case FlipFlop(var on: Boolean = false) + case Conjuction(var seenPulses: Map[String, Pulse] = Map.empty) + case Broadcast + case Sink(var lowCount: Long = 0, var highCount: Long = 0) +object Module: + def parseAll(str: String): Map[String, (Module, List[String])] = + val modules = str + .split('\n') + .map { + case s"broadcaster -> $targets" => + Module.Broadcast.toString -> (Module.Broadcast -> targets + .split(',') + .map(_.trim) + .toList) + case s"%$name -> $targets" => + name -> (Module.FlipFlop() -> targets.split(',').map(_.trim).toList) + case s"&$name -> $targets" => + name -> (Module.Conjuction() -> targets.split(',').map(_.trim).toList) + } + .toMap + + modules.foreach { + case (name, (module: Module.Conjuction, _)) => + module.seenPulses = modules.collect { + case (sourceName, (_, targets)) if targets.contains(name) => + (sourceName -> Pulse.Low) + } + case _ => () + } + val sinks = modules.values + .flatMap(_._2) + .toList + .distinct + .filterNot(modules.contains) + .map(_ -> (Module.Sink(), List.empty)) + modules ++ sinks + +def processPulse( + source: String, + pulse: Pulse, + moduleName: String, + modules: Map[String, (Module, List[String])] +): List[(String, Pulse, String)] = + val (module, targets) = modules(moduleName) + module match + case Module.Broadcast => + targets.map((moduleName, pulse, _)) + case module: Module.Sink => + if pulse == Pulse.Low then module.lowCount += 1 + else if pulse == Pulse.High then module.highCount += 1 + List.empty + case module @ Module.FlipFlop(true) if pulse == Pulse.Low => + module.on = false + targets.map((moduleName, Pulse.Low, _)) + case module @ Module.FlipFlop(false) if pulse == Pulse.Low => + module.on = true + targets.map((moduleName, Pulse.High, _)) + case Module.FlipFlop(_) => List.empty + case module: Module.Conjuction => + module.seenPulses = module.seenPulses.updated(source, pulse) + if module.seenPulses.values.forall(_ == Pulse.High) then + targets.map((moduleName, Pulse.Low, _)) + else targets.map((moduleName, Pulse.High, _)) + +def part1(input: String): String = + val modules = Module.parseAll(input) + var lowCount = 0L + var highCount = 0L + + def pushButton = + val queue = mutable.Queue(("Button", Pulse.Low, Module.Broadcast.toString)) + while (queue.nonEmpty) + val (source, pulse, moduleName) = queue.dequeue + pulse match + case Pulse.High => highCount += 1 + case Pulse.Low => lowCount += 1 + queue.enqueueAll(processPulse(source, pulse, moduleName, modules)) + + for _ <- 1 to 1000 do pushButton + + (lowCount * highCount).toString + +def part2(input: String): String = + + def findModuleSources( + name: String, + modules: Map[String, (Module, List[String])] + ): List[String] = + modules.collect { + case (sourceName, (_, targets)) if targets.contains(name) => sourceName + }.toList + + def countButtonPushesUntilHighPulseFor(targetModuleName: String): Long = + val modules = Module.parseAll(input) + + boundary: + for pushes <- 1L to Int.MaxValue do + val queue = + mutable.Queue(("Button", Pulse.Low, Module.Broadcast.toString)) + while (queue.nonEmpty) + val (source, pulse, moduleName) = queue.dequeue + if source == targetModuleName && pulse == Pulse.High then + boundary.break(pushes) + queue.enqueueAll(processPulse(source, pulse, moduleName, modules)) + throw RuntimeException( + s"Module $targetModuleName never receives a high pulse" + ) + + @tailrec def gcd(a: Long, b: Long): Long = + if (b == 0) a.abs else gcd(b, a % b) + def lcm(a: Long, b: Long) = + (a * b).abs / gcd(a, b) + + val modules = Module.parseAll(input) + val rxSource = findModuleSources("rx", modules).head + val sourceConjunctions = findModuleSources(rxSource, modules) + + sourceConjunctions + .map(countButtonPushesUntilHighPulseFor) + .reduce(lcm) + .toString