Day 11

The input looks 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

Monkey 2:
  Starting items: 79, 60, 97
  Operation: new = old * old
  Test: divisible by 13
    If true: throw to monkey 1
    If false: throw to monkey 3

Monkey 3:
  Starting items: 74
  Operation: new = old + 3
  Test: divisible by 17
    If true: throw to monkey 0
    If false: throw to monkey 1

The problem is 1) parsing the notes, 2) creating a monkey class with that info and 3) making the inspect/throw functions.

My solution for this one felt way cleaner than the one I took for the similarly class problem Day 9:

import re
import math

with open("ex_input.txt") as file:
    input = file.read()

input = input.split("\n\n")

class monkey():
    def __init__(self, id, items, operation, test, passed, failed):
        self.id = id
        self.items = items
        self.operation = operation 
        self.test = int(test)
        self.passed = int(passed)
        self.failed = int(failed)
        self.inspected = 0
    
    def inspect(self):
        for item in self.items:
            self.inspected += 1
            worry = eval(self.operation.replace("old", str(item)))//3
            if worry%self.test == 0:
                monkey_list[self.passed].items.append(worry)
            else:
                monkey_list[self.failed].items.append(worry)  
        self.items = [] 

    def monkey_business(self):
        self.inspect()
    
def parse_notes(note):
    """
    parses monkey notes
    """
    monkey_id = re.search("Monkey (\d*)", note).group(1)
    # parse items
    items = re.search("items: ([\d ,]*)", note).group(1)
    items = items.split(", ")

    operation = re.search("new = (.*)", note).group(1)
    test = re.search("by (\d*)", note).group(1)
    true_cond = re.search("true: throw to monkey (\d*)", note).group(1)
    false_cond = re.search("false: throw to monkey (\d*)", note).group(1)
    return monkey_id, items, operation, test,true_cond, false_cond

# parse notes, asign to a list of monkeys
notes = []
for n_monkeys in range(len(input)):
    notes.append(parse_notes(input[n_monkeys]))
monkey_list = [monkey(*notes[x]) for x in range(len(notes))]

# 20 rounds
for _ in range(20):
    for monkey in monkey_list:
        monkey.inspect()

# get inspection metric
inspected = []
for monkey in monkey_list:
    inspected.append(monkey.inspected)
sorted_inspected = sorted(inspected)
print("Solution 1:", math.prod(sorted_inspected[-2:]))

The second part involves no longer dividing by 3 the worry metric, and increasing the rounds to 10000, resulting in slow computation times and big integer overflow.

I admit I had to check hints on this one because my maths game is very very weak. Anyway, in the end it turns out it’s a matter of computing the least common multiple to keep the validation of tests ok without feeding the operation monstruous integers. Makes sense. The implementation is quite trivial: modify the monkey function inspect() to divide by the LCM:

    def inspect(self, lcm):

        for item in self.items:
            self.inspected += 1
            worry = eval(self.operation.replace("old", str(item)))%lcm
            if worry%self.test == 0:
                monkey_list[self.passed].items.append(worry)
            else:
                monkey_list[self.failed].items.append(worry)
    self.items = []

and instead of the 20 rounds, compute the LCM of the tests of all monkeys, feed it into inspect, then increase rounds to 10000:

lcm = []
for monkey in monkey_list:
    lcm.append(monkey.test)
lcm = math.prod(lcm)

for _ in range(10000):
    for monkey in monkey_list:
        monkey.inspect(lcm)

The rest is the same.

I should have realized before, but well… First time I find a similar problem. A signal that I should go back to math principles.