diff --git a/2022/day11.py b/2022/day11.py new file mode 100644 index 0000000..283f8fd --- /dev/null +++ b/2022/day11.py @@ -0,0 +1,155 @@ +# -*- encoding: utf-8 -*- + +import copy +import sys +from functools import reduce +from typing import Callable + + +class Monkey: + + id: int + items: list[int] + worry_fn: Callable[[int], int] + test_value: int + throw_targets: dict[bool, int] + + def __init__( + self, + id: int, + items: list[int], + worry_fn: Callable[[int], int], + test_value: int, + throw_targets: dict[bool, int], + ): + self.id = id + self.items = items + self.worry_fn = worry_fn + self.test_value = test_value + self.throw_targets = throw_targets + + def __eq__(self, o: object) -> bool: + if not isinstance(o, Monkey): + return False + return self.id == o.id + + def __hash__(self) -> int: + return hash(self.id) + + +def parse_monkey(lines: list[str]) -> Monkey: + assert lines[0].startswith("Monkey") + + monkey_id = int(lines[0].split()[-1][:-1]) + + # parse items + items = [int(r.strip()) for r in lines[1].split(":")[1].split(",")] + + # parse worry + worry_fn: Callable[[int], int] + worry_s = lines[2].split("new =")[1].strip() + operand = worry_s.split()[-1].strip() + + if worry_s.startswith("old *"): + if operand == "old": + + def worry_fn(w: int) -> int: + return w * w + + else: + + def worry_fn(w: int) -> int: + return w * int(operand) + + elif worry_s.startswith("old +"): + if operand == "old": + + def worry_fn(w: int) -> int: + return w + w + + else: + + def worry_fn(w: int) -> int: + return w + int(operand) + + else: + assert False, worry_s + + # parse test + assert lines[3].split(":")[1].strip().startswith("divisible by") + test_value = int(lines[3].split()[-1]) + + assert lines[4].strip().startswith("If true") + assert lines[5].strip().startswith("If false") + throw_targets = {True: int(lines[4].split()[-1]), False: int(lines[5].split()[-1])} + + assert monkey_id not in throw_targets.values() + + return Monkey(monkey_id, items, worry_fn, test_value, throw_targets) + + +def run( + monkeys: list[Monkey], n_rounds: int, me_worry_fn: Callable[[int], int] +) -> dict[Monkey, int]: + """ + The list of monkeys is modified in place. + + Args: + monkeys: Initial list of monkeys. The Monkey will be modified in places (their + items attributes). + n_rounds: Number of rounds to run. + me_worry_fn: Worry function to apply after the Monkey operation (e.g., divide + by 3 for round 1). + + Returns: + A mapping containing, for each monkey, the number of items inspected. + """ + inspects = {monkey: 0 for monkey in monkeys} + + for round in range(n_rounds): + + for monkey in monkeys: + for item in monkey.items: + inspects[monkey] += 1 + + # compute the new worry level + item = me_worry_fn(monkey.worry_fn(item)) + + # find the target + target = monkey.throw_targets[item % monkey.test_value == 0] + assert target != monkey.id + + monkeys[target].items.append(item) + + # clear after the loop + monkey.items.clear() + + return inspects + + +def monkey_business(inspects: dict[Monkey, int]) -> int: + sorted_levels = sorted(inspects.values()) + return sorted_levels[-2] * sorted_levels[-1] + + +monkeys = [parse_monkey(block.splitlines()) for block in sys.stdin.read().split("\n\n")] + +# case 1: we simply divide the worry by 3 after applying the monkey worry operation +answer_1 = monkey_business( + run(copy.deepcopy(monkeys), 20, me_worry_fn=lambda w: w // 3) +) +print(f"answer 1 is {answer_1}") + +# case 2: to keep reasonable level values, we can use a modulo operation, we need to +# use the product of all "divisible by" test so that the test remains valid +# +# (a + b) % c == ((a % c) + (b % c)) % c --- this would work for a single test value +# +# (a + b) % c == ((a % d) + (b % d)) % c --- if d is a multiple of c, which is why here +# we use the product of all test value +# +total_test_value = reduce(lambda w, m: w * m.test_value, monkeys, 1) +answer_2 = monkey_business( + run(copy.deepcopy(monkeys), 10_000, me_worry_fn=lambda w: w % total_test_value) +) +print(f"answer 2 is {answer_2}") diff --git a/2022/inputs/day11.txt b/2022/inputs/day11.txt new file mode 100644 index 0000000..ec2c855 --- /dev/null +++ b/2022/inputs/day11.txt @@ -0,0 +1,55 @@ +Monkey 0: + Starting items: 54, 53 + Operation: new = old * 3 + Test: divisible by 2 + If true: throw to monkey 2 + If false: throw to monkey 6 + +Monkey 1: + Starting items: 95, 88, 75, 81, 91, 67, 65, 84 + Operation: new = old * 11 + Test: divisible by 7 + If true: throw to monkey 3 + If false: throw to monkey 4 + +Monkey 2: + Starting items: 76, 81, 50, 93, 96, 81, 83 + Operation: new = old + 6 + Test: divisible by 3 + If true: throw to monkey 5 + If false: throw to monkey 1 + +Monkey 3: + Starting items: 83, 85, 85, 63 + Operation: new = old + 4 + Test: divisible by 11 + If true: throw to monkey 7 + If false: throw to monkey 4 + +Monkey 4: + Starting items: 85, 52, 64 + Operation: new = old + 8 + Test: divisible by 17 + If true: throw to monkey 0 + If false: throw to monkey 7 + +Monkey 5: + Starting items: 57 + Operation: new = old + 2 + Test: divisible by 5 + If true: throw to monkey 1 + If false: throw to monkey 3 + +Monkey 6: + Starting items: 60, 95, 76, 66, 91 + Operation: new = old * old + Test: divisible by 13 + If true: throw to monkey 2 + If false: throw to monkey 5 + +Monkey 7: + Starting items: 65, 84, 76, 72, 79, 65 + Operation: new = old + 5 + Test: divisible by 19 + If true: throw to monkey 6 + If false: throw to monkey 0