Advent Of Code 2022 - Day 11
Day 11 - Monkey in the Middle
See Day 11 for a detailed description of the problem.
Continuing to solve the Advent of Code 2022 problems (see Advent of Code - Day 1).
Links:
To run the example code in this post save the code into file such as
advent.jactl
and take your input from the Advent of Code site (e.g.advent.txt
) and run it like this:$ cat advent.txt | java -jar jactl-2.1.0.jar advent.jactl
Part 1
This puzzle has a bunch of monkeys throwing items amongst each other based on a “worry” score for each item
(read the [puzzle description]https://adventofcode.com/2022/day/11) for a full explanation.
Each monkey starts with a given set of items and for each round, each monkey performs an operation
like old * 19
to calculate the new score of each of the items they hold.
The new score is then divided by 3 and then, based on whether it is divisible by a given number (different per monkey),
the item is passed to one of two other monkeys.
The input consists of parsing input like this:
Monkey 0:
Starting items: 79, 98
Operation: new = old * 19
Test: divisible by 23
If true: throw to monkey 2
If false: throw to monkey 3
Monkey 1:
Starting items: 54, 65, 75, 74
Operation: new = old + 6
Test: divisible by 19
If true: throw to monkey 2
If false: throw to monkey 0
...
This is pretty easy to do with regex patterns, and we parse each monkey’s details into a map of values that looks like this:
[
inspections:0,
items:[79, 98],
operation:'old * 19',
divisible:23,
true:2,
false:3
]
I always write the code based on the test input given before trying to apply the solution to my specific input,
so I didn’t know how complex the expressions for operation
could be.
The format was not specified and the examples had old * 19
, old + 6
, old * old
, and old + 3
.
I decided to make my solution completely generic and took advantage of the eval
statement to evaluate the
expression each time, passing in a Map which contained the old
field set to the current worry score.
This made the code pretty simple.
Finally, the result after 20 rounds is determined by finding the two busiest monkeys (in terms of how many items they inspect) and multiply their total item inspection counts together.
def monkeys = [], monkey
stream(nextLine).each{
/^Monkey (.*):/n and do { monkey = [inspections:0]; monkeys[$1] = monkey }
/^ *Starting items: (.*)$/r and do { monkey.items = $1.split(/, /).map{ it as int } }
/^ *Operation: *new *= *(.*)$/n and monkey.operation = $1
/^ *Test: divisible by (.*)$/n and monkey.divisible = $1
/^ *If (.*): throw to monkey (.*)$/n and monkey.($1) = $2 // Note 1
}
def processMonkey = { monkey ->
def items = monkey.items
monkey.items = []
items.each{ item ->
item = eval(monkey.operation, [old: item]) / 3 // Note 2
monkeys[item % monkey.divisible ? monkey.false : monkey.true].items <<= item // Note 3
monkey.inspections++
}
}
20.each{ monkeys.each(processMonkey) }
monkeys.map{ it.inspections }.sort{ a,b -> b <=> a }.limit(2).reduce(1){ m,it -> m*it }
Notes:
monkey.($1) = $2
will store the value from the second regex group into the field name of the first regex group (in this case the field will betrue
orfalse
).- We use
eval
to evaluate the expression, passing in the current value as the value for theold
global variable. We then divide by 3 as specified by the requirements (this prevents the numbers blowing up into huge values). - We determine whether the new score is divisible by the
monkey.divisible
and if not divisible add the item to the monkey whose value is given by themonkey.false
field and if it is divisible we add the item to the monkey given by themonkey.true
field. Note thatx % y
in a boolean expression will betrue
when it is not divisible since when it is not divisible it evaluates to non-zero.
Part 2
For part 2, the only difference is that we no longer divide the scores by 3 each time, and instead of 20 iterations, we now have 10000 iterations.
This means that the values quickly explode beyond even what a 64 bit long can handle.
Since Jactl can use BigDecimal values for decimal values, we could use those instead since they can scale to whatever value is needed (as long as you have enough memory). The number of iterations and the size of the numbers, however, means that it would run for a prohibitively long period of time even if there was enough memory.
We only need the value to determine which monkey to give the item to, and this is determined by whether the
score is divisible by the monkey’s divisible
value.
Since the items pass between any number of monkeys, if we calculate the product of all of the divisible
values
and just keep the score modulo this product, this will give us a score that still gives us the correct result
when looking at the remainder based on the divisible
value for any individual monkey.
Proof: If you take the remainder of a number
n
dividing by some productp * q
and then take the result of that and calculate the remainder dividing by one of the divisors of the product (e.g.q
) the result will be the same as the remainder ofn
after dividing byq
:(n % (p * q)) % q == n % q
This is easily proven by expressing
n
asx*p*q + (y*q + z)
wherey*q + z < p*q
andz < q
and using the definition of the remainder operator (n % d == n - (int)(n/d)
whenn >= 0
).
The code for part 2 now looks like this:
def monkeys = [], monkey
stream(nextLine).each{
/^Monkey (.*):/n and do { monkey = [inspections:0L]; monkeys[$1] = monkey }
/^ *Starting items: (.*)$/r and do { monkey.items = $1.split(/, /).map{ it as long } }
/^ *Operation: *new *= *(.*)$/r and monkey.operation = $1
/^ *Test: divisible by (.*)$/n and monkey.divisible = $1
/^ *If (.*): throw to monkey (.*)$/n and monkey.($1) = $2
/^ *If false: throw to monkey (.*)$/n and monkey.false = $1
}
def divisorProduct = monkeys.reduce(1){ m,it -> it.divisible * m } // Note 1
def processMonkey = { monkey ->
def items = monkey.items
monkey.items = []
items.each{ item ->
item = eval(monkey.operation, [old: item]) % divisorProduct // Note 2
monkeys[item % monkey.divisible ? monkey.false : monkey.true].items <<= item
monkey.inspections++
}
}
10000.each{ monkeys.each(processMonkey) }
monkeys.map{ it.inspections }.sort{ a,b -> b <=> a }.limit(2).reduce(1){ m,it -> m*it }
Notes:
- We calculate the product of all the
divisible
values of the monkeys using thereduce()
method. This is passed a initial value1
and a closure which takes two values — the last value, and the current element — and returns the next value. - This is the change from part 1 where we used to divide by 3, and now we take the remainder after dividing by the
product of the
divisible
values.