Skip to content

Commit 16240b6

Browse files
mbovelbishabosha
andauthored
Add text for day 19 (#505)
* Add text for day 19 * Fix typos * Update docs/2023/puzzles/day19.md * Update docs/2023/puzzles/day19.md * Update docs/2023/puzzles/day19.md * Update docs/2023/puzzles/day19.md * credit mbovel --------- Co-authored-by: Jamie Thompson <[email protected]>
1 parent e069278 commit 16240b6

File tree

1 file changed

+222
-0
lines changed

1 file changed

+222
-0
lines changed

docs/2023/puzzles/day19.md

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,232 @@ import Solver from "../../../../../website/src/components/Solver.js"
22

33
# Day 19: Aplenty
44

5+
by [@mbovel](https://github.com/mbovel)
6+
57
## Puzzle description
68

79
https://adventofcode.com/2023/day/19
810

11+
## Data structures
12+
13+
We define the following data structures for today's puzzle:
14+
15+
```scala
16+
enum Channel:
17+
case X, M, A, S
18+
19+
enum Operator:
20+
case LessThan, GreaterThan
21+
22+
enum Result:
23+
case Reject, Accept
24+
25+
enum Instruction:
26+
case IfThenElse(
27+
channel: Channel,
28+
operator: Operator,
29+
value: Int,
30+
thenBranch: GoTo | Return,
31+
elseBranch: Instruction
32+
)
33+
case Return(result: Result)
34+
case GoTo(target: String)
35+
36+
import Instruction.*
37+
38+
type Workflow = Map[String, Instruction]
39+
40+
case class Part(x: Int, m: Int, a: Int, s: Int)
41+
```
42+
43+
## Parsing
44+
45+
Then, we write the associated parsing methods, using `String`'s `split` method and [regular expression patterns](https://docs.scala-lang.org/tour/regular-expression-patterns.html)
46+
47+
```scala
48+
object Channel:
49+
def parse(input: String): Channel =
50+
input match
51+
case "x" => Channel.X
52+
case "m" => Channel.M
53+
case "a" => Channel.A
54+
case "s" => Channel.S
55+
case _ => throw Exception(s"Invalid channel: $input")
56+
57+
object Operator:
58+
def parse(input: String): Operator =
59+
input match
60+
case "<" => Operator.LessThan
61+
case ">" => Operator.GreaterThan
62+
case _ => throw Exception(s"Invalid operator: $input")
63+
64+
object Result:
65+
def parse(input: String): Result =
66+
input match
67+
case "R" => Result.Reject
68+
case "A" => Result.Accept
69+
case _ => throw Exception(s"Invalid result: $input")
70+
71+
object Instruction:
72+
private val IfThenElseRegex = """([xmas])([<>])(\d+):(\w+),(.*)""".r
73+
private val ReturnRegex = """([RA])""".r
74+
private val GoToRegex = """(\w+)""".r
75+
def parse(input: String): Instruction =
76+
input match
77+
case IfThenElseRegex(channel, operator, value, thenBranch, elseBranch) =>
78+
Instruction.parse(thenBranch) match
79+
case thenBranch: (GoTo | Return) =>
80+
IfThenElse(
81+
Channel.parse(channel),
82+
Operator.parse(operator),
83+
value.toInt,
84+
thenBranch,
85+
Instruction.parse(elseBranch)
86+
)
87+
case _ => throw Exception(s"Invalid then branch: $thenBranch")
88+
case ReturnRegex(result) => Return(Result.parse(result))
89+
case GoToRegex(target) => GoTo(target)
90+
case _ => throw Exception(s"Invalid instruction: $input")
91+
92+
object Workflow:
93+
def parse(input: String): Workflow =
94+
input.split("\n").map(parseBlock).toMap
95+
96+
private val BlockRegex = """(\w+)\{(.*?)\}""".r
97+
private def parseBlock(input: String): (String, Instruction) =
98+
input match
99+
case BlockRegex(label, body) =>
100+
(label, Instruction.parse(body))
101+
102+
object Part:
103+
val PartRegex = """\{x=(\d+),m=(\d+),a=(\d+),s=(\d+)\}""".r
104+
def parse(input: String): Part =
105+
input match
106+
case PartRegex(x, m, a, s) => Part(x.toInt, m.toInt, a.toInt, s.toInt)
107+
case _ => throw Exception(s"Invalid part: $input")
108+
```
109+
110+
## Part 1 – Evaluation
111+
112+
These helpers allow us to implement the core logic succinctly:
113+
114+
```scala
115+
def part1(input: String): Int =
116+
val Array(workflowLines, partLines) = input.split("\n\n")
117+
val workflow = Workflow.parse(workflowLines.trim())
118+
val parts = partLines.trim().split("\n").map(Part.parse)
119+
120+
def eval(part: Part, instruction: Instruction): Result =
121+
instruction match
122+
case IfThenElse(channel, operator, value, thenBranch, elseBranch) =>
123+
val channelValue = channel match
124+
case Channel.X => part.x
125+
case Channel.M => part.m
126+
case Channel.A => part.a
127+
case Channel.S => part.s
128+
val result = operator match
129+
case Operator.LessThan => channelValue < value
130+
case Operator.GreaterThan => channelValue > value
131+
if result then eval(part, thenBranch) else eval(part, elseBranch)
132+
case Return(result) => result
133+
case GoTo(target) => eval(part, workflow(target))
134+
135+
parts
136+
.collect: part =>
137+
eval(part, workflow("in")) match
138+
case Result.Reject => 0
139+
case Result.Accept => part.x + part.m + part.a + part.s
140+
.sum
141+
```
142+
143+
## Part 2 – Abstract evaluation
144+
145+
To solve the second part efficiently, we use [abstract interpretation](https://en.wikipedia.org/wiki/Abstract_interpretation) to count the number of executions of the workflow that lead to an `Accept` result.
146+
147+
The _abstract domain_ in our case is sets of `Part`s.
148+
149+
We represent such sets with the `AbstractPart` structure, where the value associated to each channel is not a number, but a range of possible values:
150+
151+
```scala
152+
case class Range(from: Long, until: Long):
153+
assert(from < until)
154+
def count() = until - from
155+
156+
object Range:
157+
def safe(from: Long, until: Long): Option[Range] =
158+
if from < until then Some(Range(from, until)) else None
159+
160+
case class AbstractPart(x: Range, m: Range, a: Range, s: Range):
161+
def count() = x.count() * m.count() * a.count() * s.count()
162+
163+
def withChannel(channel: Channel, newRange: Range) =
164+
channel match
165+
case Channel.X => copy(x = newRange)
166+
case Channel.M => copy(m = newRange)
167+
case Channel.A => copy(a = newRange)
168+
case Channel.S => copy(s = newRange)
169+
170+
def getChannel(channel: Channel) =
171+
channel match
172+
case Channel.X => x
173+
case Channel.M => m
174+
case Channel.A => a
175+
case Channel.S => s
176+
```
177+
178+
We will start the evaluation with abstract parts that contain all possible values for each channel: four ranges from 1 until 4001.
179+
180+
When we evaluate an `IfThenElse` instruction, we split the argument `AbstractPart` into two parts, one that contains only the values that satisfy the condition, and one that contains only the values that do not satisfy the condition.
181+
182+
This is achieved with the `split` method of `AbstractPart`:
183+
184+
```scala
185+
def split(
186+
channel: Channel,
187+
value: Int
188+
): (Option[AbstractPart], Option[AbstractPart]) =
189+
val currentRange = getChannel(channel)
190+
(
191+
Range.safe(currentRange.from, value).map(withChannel(channel, _)),
192+
Range.safe(value, currentRange.until).map(withChannel(channel, _))
193+
)
194+
```
195+
196+
Using these helpers, we can implement the abstract evaluator as follows:
197+
198+
```scala
199+
def part2(input: String): Long = combinations(input, 4001)
200+
201+
extension [T](part: (T, T)) private inline def swap: (T, T) = (part._2, part._1)
202+
203+
def combinations(input: String, until: Long): Long =
204+
val Array(workflowLines, _) = input.split("\n\n")
205+
val workflow = Workflow.parse(workflowLines.trim())
206+
207+
def count(part: AbstractPart, instruction: Instruction): Long =
208+
instruction match
209+
case IfThenElse(channel, operator, value, thenBranch, elseBranch) =>
210+
val (trueValues, falseValues) =
211+
operator match
212+
case Operator.LessThan => part.split(channel, value)
213+
case Operator.GreaterThan => part.split(channel, value + 1).swap
214+
trueValues.map(count(_, thenBranch)).getOrElse(0L)
215+
+ falseValues.map(count(_, elseBranch)).getOrElse(0L)
216+
case Return(Result.Accept) => part.count()
217+
case Return(Result.Reject) => 0L
218+
case GoTo(target) => count(part, workflow(target))
219+
220+
count(
221+
AbstractPart(
222+
Range(1, until),
223+
Range(1, until),
224+
Range(1, until),
225+
Range(1, until)
226+
),
227+
workflow("in")
228+
)
229+
```
230+
9231
## Solutions from the community
10232

11233
- [Solution](https://github.com/merlinorg/aoc2023/blob/main/src/main/scala/Day19.scala) by [merlin](https://github.com/merlinorg/)

0 commit comments

Comments
 (0)