Metadata
-
Date
-
Tagged
-
Part of series
- Advent of Code 2023 Day 1
- Advent of Code 2023 Day 2
- Advent of Code 2023 Day 3
- Advent of Code 2023 Day 4
- Advent of Code 2023 Day 5
- Advent of Code 2023 Day 6
- Advent of Code 2023 Day 7
- Advent of Code 2023 Day 8
- Advent of Code 2023 Day 9
- Advent of Code 2023 Day 10
- Advent of Code 2023 Day 11
- Advent of Code 2023 Day 12
- Advent of Code 2023 Day 13
- Advent of Code 2023 Day 14
- Advent of Code 2023 Day 15
- Advent of Code 2023 Day 16
- Advent of Code 2023 Day 17
- Advent of Code 2023 Day 18
- Advent of Code 2023 Day 19
- Advent of Code 2023 Day 20
- Advent of Code 2023 Day 21
- Advent of Code 2023 Day 22
- Advent of Code 2023 Day 23
- Advent of Code 2023 Day 25
-
Older post
-
Newer post
Advent of Code 2023 Day 19
Day 19: Aplenty
https://adventofcode.com/2023/day/19
You arrive at the spot the machine part are forming a formidable pile. The elves are already very busy sorthing the parts, but could use your help.
Each part is rated on 4 categories:
x
: Extremely cool lookingm
: Musical (it makes a noise when you hit it)a
: Aerodynamics
: Shiny
Then each part is sent through some workflows that ultimately decide whether to accept or reject the part.
Today’s input is a list of workflows, and parts. An example input looks like this:
Each named workflow has a list of rules.
- If a part passes a rule, it moves to the destination that rule dictates (a new workflow, or a final accept/reject decision)
- If a part fails a rule, it continues to the next rule in the current list of rules.
The rules are a pass/fail condition that checks if one of the xmas
values is greater than, or smaller than a given number.
The last “rule” in a workflow has no condition, its destination always applies when that rule is reached.
An example workflow is ex{x>10:one,m<20:two,a>30:R,A}
.
It’s named ex
, and has 4 rules.
- Rule
x>10:one
: If the part’sx
is more than10
, send the part to the workflow namedone
. - Rule
m<20:two:
Otherwise, if the part’sm
is less than20
, send the part to the workflow namedtwo
. - Rule
a>30:R:
Otherwise, if the part’sa
is more than30
, the part is immediately rejected (R
). - Rule
A
: Otherwise, because no other rules matched the part, the part is immediately accepted (A
).
An example part is {x=787,m=2655,a=1222,s=2876}
It has these ratings:
x
: Extremely cool looking: 787m
: Musical: 2655a
: Aerodynamic: 1222s
: Shiny: 2876
Each part starts at the workflow named in
.
The example parts follow these paths through the workflows while being checked:
{x=787,m=2655,a=1222,s=2876}
: in -> qqz -> qs -> lnx -> A{x=1679,m=44,a=2067,s=496}
: in -> px -> rfg -> gd -> R{x=2036,m=264,a=79,s=2244}
: in -> qqz -> hdj -> pv -> A{x=2461,m=1339,a=466,s=291}
: in -> px -> qkq -> crn -> R{x=2127,m=1623,a=2188,s=1013}
: in -> px -> rfg -> A
Parsing
Today was a heavy parsing day. I decided to put all data into neat data structures, preferring verbosity and clarity.
A part with the 4 ratings it has:
The final decision whether a part is accepted or rejected:
The destination a rule points to when it applies is either a new workflow, or a final decision:
A rule in a workflow can either be a check, or if it’s the last one, only a new destination:
A ckeck tests if a single rating of a part is greater than or less than a given number:
To determine which ranking the check is for:
To determine which kind of comparison the check makes:
The parsing is split into 2 parts, parsing a list of workflows, and parsing a list of parts:
Eack workflow is represented by an entry into a map, the key is the workflow name, and the value is the list of rules in that workflow. The trickiest part was making sure to correctly handle that last rule that has no check:
The parsing of the parts is a lot more straightforward. For each line, I start with a part that has rating 0 in every category. I loop through every key=value pair, and set the appropriate rating.
Part 1
The question asks what you get if you add together all of the rating numbers for all of the parts that ultimately get accepted?
The code I want to work towards loops over every part, determines whether or not it gets accepted, and sums up all xmas
ratings.
Helpers
A method on Part
to determine if a part passes a Check
:
I use that helper in both of my solutions (I like the recursive one most).
The meat of the solution is that accepted
method on Part
.
Option 1: iteratively
The accepted
method uses an infinite loop that gets broken when a part receives a final decision.
Outside of the loop I track which workflow the part is currently in (it starts off as the workflow named in
)
In the loop I track the current location the part is at (it starts off as the desination of the workflow named in
).
The infinite loop contains a loop that goes through every rule in the current workflow.
When the part should go to a new destination, it breaks out of that for
loop.
A new destination can happen:
- if a check passed
- if it was the final rule that has no condition
After breaking out of that for
loop I check wich new destination the part went to:
- If the new destination was a workflow, we update the current workflow and go through the outer (infinite) loop again.
- If the new destination was a final decision, the whole function returns with that final decision.
Option 2: recursively
A recursive function that takes in the current Part
, the current Dest
, and a map of all workflows.
If the dest
is a final one, return the result from the function, this is the base case.
If the dest
is a workflow is when the recursing happens.
It loops through all rules of the workflow at the current destination.
It then returns the result of following that rule.
(if it passes, recurse with the new destination, if it fails, move on to the next rule in the workflow by advancing the for
loop)
Code
Depending on whether or not you choose the recursive solution, the way you call the accepted
helper on a part slightly changes.
With the recursive version, you have to pass in the starting destination.
Part 2
The sorting still isn’t going quick enough, time to change strategies.
The elves want to figure out which combinations of ratings will be accepted or rejected.
Each of the 4 ratings (x
, m
, a
, s
) ranges from 1 up to, and including 4000.
In the example 167409079868000 combinations of ratings will be accepted.
The question asks how many distinct combinations of ratings will be accepted by the workflows?
Only consider the workflows, the list of individual part ratings is no longer relevant.
I represented all possible ranges for a part as a PartRange
.
A PartRange
has 4 ranges, 1 for each rating:
The plan is to start with a single PartRange
with ranges that go from 1 to 4000 in each category.
Then apply the workflow named "in"
, and sum up all the distinct ranges that lead to a final accept decision.
This is the code I want to work towards, naming the main helper accepted
again.
Only this time, it does something different.
Helpers
A the combos
method that takes a PartRange
and returns the amount of distinct combinations it represents.
For every rating category, it takes the end of it’s range, subtracts the start, and adds 1 because that range was inclusive.
Then it multiplies all those possibilities.
In math this is called permutations. Like for a 4 digit number the amount of permutations is 10 * 10 * 10 * 10, for 10 options per digit.
A helper on PartRange
that determines which relevant range passes a check, and which range fails a check.
Remember, a check looks at 1 specific range, either x
, m
, a
, or s
.
There are 3 different options:
- The entire range passes a check
- The entire range fails a check
- The range is split in 2, a failing range, and a passing range
I represented these 3 options as an enum in code:
The helper returns one of these three options.
Inside the method, it first extracts the range the check applies to.
It stores the start and end of that range (start
and end
).
It stores the number the check applies (called rhs
).
Depending on the order of these 3 variables, and the operator (greater than or lessthan), the range falls in one of those 3 variants.
There are 3 different possible orderings of start
, end
, and rhs
.
When you order those 3 numbers from smallest to largest (start is always smaller than end):
start
-end
-rhs
rhs
-start
-end
start
-rhs
-end
Each of these options has 2 possible operators, greaterthan, or lessthan.
Covering every possibility, each one leads to one of those 3 CheckedRange
variants.
The ones where a range totally fails, or totally passes are not that interesting.
In the cases where a range should be split, 2 entirely new ranges are constructed from the original range:
- A passing one
- A failing one
This uses a different helper that’s only there as convenience for me to make a new PartRange
, but replace one of the x
, m
, a
, s
ranges with a new one.
Now the meat of the problem, the helper that returns a list of PartRange
s that lead to a final accept decision.
Again, it’s a recursive function that takes in the current destination.
If that destination is a final decision, the result is the entire range if it’s accepted, or nothing if it’s rejected.
This is the base case of the recursion, eventually every PartRange
is either accepted, or rejected.
If that destination is a workflow, a similar logic to part1 happens.
I keep track of the current PartRange
, and a list of all PartRange
s in this workflow that eventually lead to an accept.
Then I loop through all rules in the workflow.
- If a check passes for the full current range, I recurse to it’s destination and add the result to that list of valid
PartRange
s. - If a check fails for the full current range, I do nothing and move on to the next rule in the workflow.
- If a check splits a range, I recurse with the passing part, and set the current
PartRange
to the failing part before moving on to the next rule in this workflow. (If I don’t do this, I’d never reach the next rule in a workflow, but I would have passed the check that just happened)
But it’s not necessary to keep track of all the PartRange
s that eventually pass.
More efficient would be to immediately calculate the distinct combinations of that PartRange
and add it to a sum.
So here is the same helper, only this time, it returns that sum instead of a list of PartRange
.
I also rewrote the for
loop as a fold
. An operation that’s called reduce in many languages.
It takes a list of something, and condenses it down into a single thing.
Inside that fold
the accumulator (the thing that’s reused every loop and ultimately returned) has 2 parts:
- The current
PartRange
- The
sum
so far
When it’s done, the thing that interests me is the final value of the sum
.
Code
The variant that does not keep track of all passing PartRange
s: