Skip to content

Commit 90300a5

Browse files
authored
Add code for days 20 and 24 (#511)
* Add code for day 20 * day 24
1 parent c93ec0e commit 90300a5

File tree

2 files changed

+378
-0
lines changed

2 files changed

+378
-0
lines changed

2023/src/day20.scala

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
package day20
2+
3+
import locations.Directory.currentDir
4+
import inputs.Input.loadFileSync
5+
import scala.annotation.tailrec
6+
import scala.collection.immutable.Queue
7+
8+
@main def part1: Unit =
9+
println(s"The solution is ${part1(loadInput())}")
10+
// println(s"The solution is ${part1(sample1)}")
11+
12+
@main def part2: Unit =
13+
println(s"The solution is ${part2(loadInput())}")
14+
// println(s"The solution is ${part2(sample1)}")
15+
16+
def loadInput(): String = loadFileSync(s"$currentDir/../input/day20")
17+
18+
val sample1 = """
19+
broadcaster -> a
20+
%a -> inv, con
21+
&inv -> b
22+
%b -> con
23+
&con -> output
24+
""".strip
25+
26+
type ModuleName = String
27+
28+
// Pulses are the messages of our primary state machine. They are either low
29+
// (false) or high (true) and travel from a source to a destination module
30+
31+
final case class Pulse(
32+
source: ModuleName,
33+
destination: ModuleName,
34+
level: Boolean,
35+
)
36+
37+
object Pulse:
38+
final val ButtonPress = Pulse("button", "broadcaster", false)
39+
40+
// The modules include pass-throughs which simply forward pulses, flip flips
41+
// which toggle state and emit when they receive a low pulse, and conjunctions
42+
// which emit a low signal when all inputs are high.
43+
44+
sealed trait Module:
45+
def name: ModuleName
46+
def destinations: Vector[ModuleName]
47+
// Generate pulses for all the destinations of this module
48+
def pulses(level: Boolean): Vector[Pulse] =
49+
destinations.map(Pulse(name, _, level))
50+
end Module
51+
52+
final case class PassThrough(
53+
name: ModuleName,
54+
destinations: Vector[ModuleName],
55+
) extends Module
56+
57+
final case class FlipFlop(
58+
name: ModuleName,
59+
destinations: Vector[ModuleName],
60+
state: Boolean,
61+
) extends Module
62+
63+
final case class Conjunction(
64+
name: ModuleName,
65+
destinations: Vector[ModuleName],
66+
// The source modules that most-recently sent a high pulse
67+
state: Set[ModuleName],
68+
) extends Module
69+
70+
// The machine comprises a collection of named modules and a map that gathers
71+
// which modules serve as sources for each module in the machine.
72+
73+
final case class Machine(
74+
modules: Map[ModuleName, Module],
75+
sources: Map[ModuleName, Set[ModuleName]]
76+
):
77+
inline def +(module: Module): Machine =
78+
copy(
79+
modules = modules.updated(module.name, module),
80+
sources = module.destinations.foldLeft(sources): (sources, destination) =>
81+
sources.updatedWith(destination):
82+
case None => Some(Set(module.name))
83+
case Some(values) => Some(values + module.name)
84+
)
85+
end Machine
86+
87+
object Machine:
88+
final val Initial = Machine(Map.empty, Map.empty)
89+
90+
// To parse the input we first parse all of the modules and then fold them
91+
// into a new machine
92+
93+
def parse(input: String): Machine =
94+
val modules = input.linesIterator.map:
95+
case s"%$name -> $targets" =>
96+
FlipFlop(name, targets.split(", ").toVector, false)
97+
case s"&$name -> $targets" =>
98+
Conjunction(name, targets.split(", ").toVector, Set.empty)
99+
case s"$name -> $targets" =>
100+
PassThrough(name, targets.split(", ").toVector)
101+
modules.foldLeft(Initial)(_ + _)
102+
end Machine
103+
104+
// The primary state machine state comprises the machine itself, the number of
105+
// button presses and a queue of outstanding pulses.
106+
107+
final case class MachineFSM(
108+
machine: Machine,
109+
presses: Long = 0,
110+
queue: Queue[Pulse] = Queue.empty,
111+
):
112+
def nextState: MachineFSM = queue.dequeueOption match
113+
// If the queue is empty, we increment the button presses and enqueue a
114+
// button press pulse
115+
case None =>
116+
copy(presses = presses + 1, queue = Queue(Pulse.ButtonPress))
117+
118+
case Some((Pulse(source, destination, level), tail)) =>
119+
machine.modules.get(destination) match
120+
// If a pulse reaches a pass-through, enqueue pulses for all the module
121+
// destinations
122+
case Some(passThrough: PassThrough) =>
123+
copy(queue = tail ++ passThrough.pulses(level))
124+
125+
// If a low pulse reaches a flip-flop, update the flip-flop state in the
126+
// machine and enqueue pulses for all the module destinations
127+
case Some(flipFlop: FlipFlop) if !level =>
128+
val flipFlop2 = flipFlop.copy(state = !flipFlop.state)
129+
copy(
130+
machine = machine + flipFlop2,
131+
queue = tail ++ flipFlop2.pulses(flipFlop2.state)
132+
)
133+
134+
// If a pulse reaches a conjunction, update the source state in the
135+
// conjunction and enqueue pulses for all the module destinations
136+
// according to the conjunction state
137+
case Some(conjunction: Conjunction) =>
138+
val conjunction2 = conjunction.copy(
139+
state = if level then conjunction.state + source
140+
else conjunction.state - source
141+
)
142+
val active = machine.sources(conjunction2.name) == conjunction2.state
143+
copy(
144+
machine = machine + conjunction2,
145+
queue = tail ++ conjunction2.pulses(!active)
146+
)
147+
148+
// In all other cases just discard the pulse and proceed
149+
case _ =>
150+
copy(queue = tail)
151+
end MachineFSM
152+
153+
// An unruly and lawless find-map-get
154+
extension [A](self: Iterator[A])
155+
def findMap[B](f: A => Option[B]): B = self.flatMap(f).next()
156+
157+
// The problem 1 state machine comprises the number of low and high pulses
158+
// processed, and whether the problem is complete (after 1000 presses). This
159+
// state machine gets updated by each state of the primary state machine.
160+
161+
final case class Problem1FSM(
162+
lows: Long,
163+
highs: Long,
164+
complete: Boolean,
165+
):
166+
// If the head of the pulse queue is a low or high pulse then update the
167+
// low/high count. If the pulse queue is empty and the button has been pressed
168+
// 1000 times then complete.
169+
inline def +(state: MachineFSM): Problem1FSM =
170+
state.queue.headOption match
171+
case Some(Pulse(_, _, false)) => copy(lows = lows + 1)
172+
case Some(Pulse(_, _, true)) => copy(highs = highs + 1)
173+
case None if state.presses == 1000 => copy(complete = true)
174+
case None => this
175+
176+
// The result is the product of lows and highs
177+
def solution: Option[Long] = Option.when(complete)(lows * highs)
178+
end Problem1FSM
179+
180+
object Problem1FSM:
181+
final val Initial = Problem1FSM(0, 0, false)
182+
183+
// Part 1 is solved by first constructing the primary state machine that
184+
// executes the pulse machinery. Each state of this machine is then fed to a
185+
// second problem 1 state machine. We then run the combined state machines to
186+
// completion.
187+
188+
def part1(input: String): Long =
189+
val machine = Machine.parse(input)
190+
Iterator
191+
.iterate(MachineFSM(machine))(_.nextState)
192+
.scanLeft(Problem1FSM.Initial)(_ + _)
193+
.findMap(_.solution)
194+
end part1
195+
196+
// The problem 2 state machine is looking for the least common multiple of the
197+
// cycle lengths of the subgraphs that feed into the output "rx" module. When it
198+
// observes a high pulse from the final module of one these subgraphs, it
199+
// records the number of button presses to reach this state.
200+
201+
final case class Problem2FSM(
202+
cycles: Map[ModuleName, Long],
203+
):
204+
inline def +(state: MachineFSM): Problem2FSM =
205+
state.queue.headOption match
206+
case Some(Pulse(src, _, true)) if cycles.get(src).contains(0L) =>
207+
copy(cycles = cycles + (src -> state.presses))
208+
case _ => this
209+
210+
// We are complete if we have the cycle value for each subgraph
211+
def solution: Option[Long] =
212+
Option.when(cycles.values.forall(_ > 0))(lcm(cycles.values))
213+
214+
private def lcm(list: Iterable[Long]): Long =
215+
list.foldLeft(1L)((a, b) => b * a / gcd(a, b))
216+
217+
@tailrec private def gcd(x: Long, y: Long): Long =
218+
if y == 0 then x else gcd(y, x % y)
219+
end Problem2FSM
220+
221+
object Problem2FSM:
222+
def apply(machine: Machine): Problem2FSM =
223+
new Problem2FSM(subgraphs(machine).map(_ -> 0L).toMap)
224+
225+
// The problem is characterized by a terminal module ("rx") that is fed by
226+
// several subgraphs so we look to see which are the sources of the terminal
227+
// module; these are the subgraphs whose cycle lengths we need to count.
228+
private def subgraphs(machine: Machine): Set[ModuleName] =
229+
val terminal = (machine.sources.keySet -- machine.modules.keySet).head
230+
machine.sources(machine.sources(terminal).head)
231+
232+
// Part 2 is solved by first constructing the primary state machine that
233+
// executes the pulse machinery. Each state of this machine is then fed to a
234+
// second problem 2 state machine. We then run the combined state machines to
235+
// completion.
236+
237+
def part2(input: String): Long =
238+
val machine = Machine.parse(input)
239+
240+
Iterator
241+
.iterate(MachineFSM(machine))(_.nextState)
242+
.scanLeft(Problem2FSM(machine))(_ + _)
243+
.findMap(_.solution)
244+
end part2

2023/src/day24.scala

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package day24
2+
3+
import locations.Directory.currentDir
4+
import inputs.Input.loadFileSync
5+
6+
@main def part1: Unit =
7+
println(s"The solution is ${part1(loadInput())}")
8+
// println(s"The solution is ${part1(sample1)}")
9+
10+
@main def part2: Unit =
11+
println(s"The solution is ${part2(loadInput())}")
12+
// println(s"The solution is ${part2(sample1)}")
13+
14+
def loadInput(): String = loadFileSync(s"$currentDir/../input/day24")
15+
16+
val sample1 = """
17+
19, 13, 30 @ -2, 1, -2
18+
18, 19, 22 @ -1, -1, -2
19+
20, 25, 34 @ -2, -2, -4
20+
12, 31, 28 @ -1, -2, -1
21+
20, 19, 15 @ 1, -5, -3
22+
""".strip
23+
24+
final case class Hail(x: Long, y: Long, z: Long, vx: Long, vy: Long, vz: Long):
25+
def xyProjection: Hail2D = Hail2D(x, y, vx, vy)
26+
def xzProjection: Hail2D = Hail2D(x, z, vx, vz)
27+
28+
object Hail:
29+
def parseAll(input: String): Vector[Hail] =
30+
input.linesIterator.toVector.map:
31+
case s"$x, $y, $z @ $dx, $dy, $dz" =>
32+
Hail(x.trim.toLong, y.trim.toLong, z.trim.toLong,
33+
dx.trim.toLong, dy.trim.toLong, dz.trim.toLong)
34+
35+
final case class Hail2D(x: Long, y: Long, vx: Long, vy: Long):
36+
private val a: BigDecimal = BigDecimal(vy)
37+
private val b: BigDecimal = BigDecimal(-vx)
38+
private val c: BigDecimal = BigDecimal(vx * y - vy * x)
39+
40+
def deltaV(dvx: Long, dvy: Long): Hail2D = copy(vx = vx - dvx, vy = vy - dvy)
41+
42+
// If the paths of these hailstones intersect, return the intersection
43+
def intersect(hail: Hail2D): Option[(BigDecimal, BigDecimal)] =
44+
val denominator = a * hail.b - hail.a * b
45+
Option.when(denominator != 0):
46+
((b * hail.c - hail.b * c) / denominator,
47+
(c * hail.a - hail.c * a) / denominator)
48+
49+
// Return the time at which this hail will intersect the given point
50+
def timeTo(posX: BigDecimal, posY: BigDecimal): BigDecimal =
51+
if vx == 0 then (posY - y) / vy else (posX - x) / vx
52+
end Hail2D
53+
54+
extension [A](self: Vector[A])
55+
// all non-self element pairs
56+
def allPairs: Vector[(A, A)] = self.tails.toVector.tail.flatMap(self.zip)
57+
58+
extension [A](self: Iterator[A])
59+
// An unruly and lawless find-map-get
60+
def findMap[B](f: A => Option[B]): B = self.flatMap(f).next()
61+
62+
def intersections(
63+
hails: Vector[Hail2D],
64+
min: Long,
65+
max: Long
66+
): Vector[(Hail2D, Hail2D)] =
67+
for
68+
(hail0, hail1) <- hails.allPairs
69+
(x, y) <- hail0.intersect(hail1)
70+
if x >= min && x <= max && y >= min && y <= max &&
71+
hail0.timeTo(x, y) >= 0 && hail1.timeTo(x, y) >= 0
72+
yield (hail0, hail1)
73+
end intersections
74+
75+
def part1(input: String): Long =
76+
val hails = Hail.parseAll(input)
77+
val hailsXY = hails.map(_.xyProjection)
78+
intersections(hailsXY, 200000000000000L, 400000000000000L).size
79+
end part1
80+
81+
def findRockOrigin(
82+
hails: Vector[Hail2D],
83+
vx: Long,
84+
vy: Long
85+
): Option[(Long, Long)] =
86+
val hail0 +: hail1 +: hail2 +: _ = hails.map(_.deltaV(vx, vy)): @unchecked
87+
for
88+
(x0, y0) <- hail0.intersect(hail1)
89+
(x1, y1) <- hail0.intersect(hail2)
90+
if x0 == x1 && y0 == y1
91+
time = hail0.timeTo(x0, y0)
92+
yield (hail0.x + hail0.vx * time.longValue,
93+
hail0.y + hail0.vy * time.longValue)
94+
end findRockOrigin
95+
96+
final case class Spiral(
97+
x: Long,
98+
y: Long,
99+
dx: Long,
100+
dy: Long,
101+
count: Long,
102+
limit: Long
103+
):
104+
def next: Spiral =
105+
if count > 0 then
106+
copy(x = x + dx, y = y + dy, count = count - 1)
107+
else if dy == 0 then
108+
copy(x = x + dx, y = y + dy, dy = dx, dx = -dy, count = limit)
109+
else
110+
copy(x = x + dx, y = y + dy, dy = dx, dx = -dy,
111+
count = limit + 1, limit = limit + 1)
112+
end next
113+
end Spiral
114+
115+
object Spiral:
116+
final val Start = Spiral(0, 0, 1, 0, 0, 0)
117+
118+
def part2(input: String): Long =
119+
val hails = Hail.parseAll(input)
120+
121+
val hailsXY = hails.map(_.xyProjection)
122+
val (x, y) = Iterator
123+
.iterate(Spiral.Start)(_.next)
124+
.findMap: spiral =>
125+
findRockOrigin(hailsXY, spiral.x, spiral.y)
126+
127+
val hailsXZ = hails.map(_.xzProjection)
128+
val (_, z) = Iterator
129+
.iterate(Spiral.Start)(_.next)
130+
.findMap: spiral =>
131+
findRockOrigin(hailsXZ, spiral.x, spiral.y)
132+
133+
x + y + z
134+
end part2

0 commit comments

Comments
 (0)