class Monkey < Struct.new(:items, :worry_level, :throw_to)
attr_reader :inspected_items
def process(monkeys)
@inspected_items ||= 0
while items.any?
adjusted_worry_level = worry_level.(items.shift)
monkeys[throw_to.(adjusted_worry_level)].items.push(adjusted_worry_level)
@inspected_items += 1
end
end
end
def monkey_business_level(rounds, adjust_worry_level)
monkeys = File.read('inputs/11.txt',).split("\n\n").map(&:lines).map { |lines| lines.map { _1.scan(/[\w+*]+/) } }.map do |lines|
items = lines[1][2..].map(&:to_i)
worry_level = case lines[2][-2..]
in ['*', 'old'] then -> { adjust_worry_level.(_1 * _1) }
in ['+', value] then -> { adjust_worry_level.(_1 + value.to_i) }
in ['*', value] then -> { adjust_worry_level.(_1 * value.to_i) }
end
throw_to = -> { (_1 % lines[3].last.to_i).zero? ? lines[4].last.to_i : lines[5].last.to_i }
Monkey.new(items, worry_level, throw_to)
end
rounds.times { monkeys.each { _1.process(monkeys) } }
monkeys.map(&:inspected_items).sort.last(2).inject(&:*)
end
a = monkey_business_level(20, -> { _1 / 3 })
b = monkey_business_level(10_000, -> { _1 % 9_699_690 })
require 'minitest/autorun'
describe 'day 11' do
it 'part a' do assert_equal 88_208, a end
it 'part b' do assert_equal 21_115_867_968, b end
end
I spent too much time trying to parse the input line by line until I chose to use a regular expression.
Python Solution
re_monkey = re.compile(
r'Monkey \d+:\n'
r' Starting items: (\d+(?:, \d+)*)\n'
r' Operation: new = ((?:\bold\b|[ 0-9*+-])+)\n'
r' Test: divisible by (\d+)\n'
r' If true: throw to monkey (\d+)\n'
r' If false: throw to monkey (\d+)'
)
@dataclass
class Monkey:
"""A monkey modifies, inspects and throws items."""
items: list[int]
operation: Callable[[int], int]
divisor: int
target_if_false: int
target_if_true: int
def read_file(file: TextIO) -> list[Monkey]:
"""Read monkeys from the `file`."""
data = file.read()
res = []
for mat in re_monkey.finditer(data):
res.append(Monkey(
items=[int(chunk) for chunk in mat.group(1).split(', ')],
operation=eval(f'lambda old: {mat.group(2)}'),
divisor=int(mat.group(3)),
target_if_true=int(mat.group(4)),
target_if_false=int(mat.group(5))
))
return res
def monkey_business(
monkeys: list[Monkey],
rounds: int,
reduce_stress_level: Callable[[int], int]
) -> int:
"""Calculate the level of monkey business after a number of rounds."""
counter = collections.Counter()
for _ in range(rounds):
for i, monkey in enumerate(monkeys):
counter[i] += len(monkey.items)
for item in monkey.items:
item = monkey.operation(item)
item = reduce_stress_level(item)
if item % monkey.divisor == 0:
target = monkey.target_if_true
else:
target = monkey.target_if_false
monkeys[target].items.append(item)
monkey.items.clear()
return product(count for idx, count in counter.most_common(2))
def part1(file: TextIO) -> int:
"""Solve the first part of the puzzle."""
monkeys = read_file(file)
return monkey_business(monkeys, 20, lambda item: item // 3)
def part2(file: TextIO) -> int:
"""Solve the second part of the puzzle."""
monkeys = read_file(file)
modulus = product(monkey.divisor for monkey in monkeys)
return monkey_business(monkeys, 10_000, lambda item: item % modulus)
MONKEY_RE = re.compile(r"""
Monkey (?P<id>\d):
Starting items: (?P<items>(?:\d+, )*\d+)
Operation: new = old (?P<operator>[+*]) (?P<operand>\d+|old)
Test: divisible by (?P<test_num>\d+)
If true: throw to monkey (?P<true>\d+)
If false: throw to monkey (?P<false>\d+)
""".strip())
def input_parser(self, puzzle_input: str) -> InputType:
monkeys = []
for block in puzzle_input.split("\n\n"):
match = MONKEY_RE.match(block)
if not match:
raise ValueError(f"Block did not match regex. {block}")
numbers = {k: int(v) for k, v in match.groupdict().items() if v.isdigit()}
monkeys.append(Monkey(
id=numbers["id"],
items=[int(i) for i in match.group("items").split(", ")],
operator={"+": operator.add, "*": operator.mul}[match.group("operator")],
operand=None if match.group("operand") == "old" else numbers["operand"],
test=numbers["test_num"],
true=numbers["true"],
false=numbers["false"],
inspected=0,
))
return monkeys
Python Solver
def solver(self, monkeys: list[Monkey], rounds: int, div: bool) -> int:
"""Simulate rounds of monkeys inspecting and throwing items."""
# Use the LCM to keep the item size low.
lcm = math.lcm(*[m.test for m in monkeys])
# Cycle through rounds and monkeys.
for _ in range(rounds):
for monkey in monkeys:
# Track how many items the monkey inspected.
monkey.inspected += len(monkey.items)
# For each item, update values and throw it to another monkey.
for item in monkey.items:
item = monkey.operator(item, monkey.operand or item)
if div:
item = item // 3
item %= lcm
# Throw the item to the next monkey.
next_monkey = monkey.true if (item % monkey.test == 0) else monkey.false
monkeys[next_monkey].items.append(item)
monkey.items = []
inspected = sorted(monkey.inspected for monkey in monkeys)
return self.mult(inspected[-2:])
I did poorly on the leaderboard today because I tried to use a lambda to save the various test/operations as a function … but fell prey to the lambda late binding trap. This resulted in much head scratching, confusion and debugging.