2023-12-2

Created on Monday, December 23, 2024.

Day 2

Ok, this sounds complex, but in reality, we're just filtering the list of games based on whether it's possible given the limitations. We're going to need to parse the structure, which I'll do into a dictionary, and then filter out any that don't meet the criteria.

There are multiple draws per line, so we'll want to create a structure that looks a bit like {Game 1: [{blue: 3, red: 4}, {red 1, green 2, blue 6}, {green 2}]}.

## Import ipytest and get it setup for use in Python Notebook
import pytest
import ipytest
ipytest.autoconfig()
lines = """Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green""".split("\n")

def parse_draw(draw):
    """Take a single draw, divided by commas and turn into a dictionary"""
    ret = {}
    for cubes in draw.split(", "):
        num,name = cubes.split(" ")
        ret[name] = int(num)
    return ret

assert parse_draw("1 red, 2 green, 6 blue") == {"red":1, "green":2, "blue":6}
    
def parse_line(line):
    """ Take a game, divide into multiple draws, and return a structure for that"""
    ret = {}
    id_s,draws_s = line.split(":")
    ret["id"] = int(id_s.split(" ")[-1])
    draws = []
    for draw in draws_s.split("; "):
        draws.append(parse_draw(draw.strip()))
    ret["draws"] = draws
    return ret

assert parse_line("Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green") == {"id":1, "draws":[
    {"red":4, "blue":3},
    {"red":1, "green":2, "blue":6},
    {"green":2}]}

Right, we have something that can parse each line and turn it into a data structure, so now we need to build some filtering logic

games = [parse_line(line.strip()) for line in lines]

def possible_draw(draw, bag):
    """ Is it possible to draw cubes 'draw' out of bag 'bag'"""
    for colour,amount in draw.items():
        if colour not in bag or bag[colour] < amount:
            return False
    return True

bag = parse_draw("12 red, 13 green, 14 blue")
assert possible_draw(parse_draw("3 blue, 4 red"), bag)
assert possible_draw(parse_draw("1 red, 2 green, 6 blue"), bag)
assert possible_draw(parse_draw("2 green"), bag)

assert possible_draw(parse_draw("8 green, 6 blue, 20 red"), bag) == False

def possible_game(game, bag):
    """ Given repeated draws, is this possible?"""
    for draw in game["draws"]:
        if not possible_draw(draw, bag):
            return False
    return True

assert possible_game(games[0], bag)
assert possible_game(games[1], bag)
assert possible_game(games[2], bag) == False

Fantastic, lets sum the ID's

sum([game["id"] for game in games if possible_game(game, bag)])

8

That works, lets do it on real data

real_games = [parse_line(line.strip()) for line in open("day2.txt").readlines()]
sum([game["id"] for game in real_games if possible_game(game, bag)])

2101

Part 2

Oh, not what I was expecting for part 2. I built my functions on the assumption that they might change whether cubes were put back in the bag or smething. But actually what we need to do is build the minimal size bag that can meet any given game. We then calculate the power of a bag, and sum the powers.

from functools import reduce
def bag_power(bag):
    return reduce(lambda a,b: a*b, bag.values())

assert bag_power(parse_draw("4 red, 2 green, 6 blue")) == 48

Right, can I be smart about working out the smallest bag?

We can problem use the reduce function here, which can iterate over a list of values, and returns something that combines the total so far, and the next item. We want to take the maximum of any colour we've seen so far, and also the maximum of any colour we've not seen.

def max_bag(bag, draw):
    new_max = {}
    for colour,qty in bag.items():
        new_max[colour] = qty
        if colour in draw and draw[colour] > qty:
            new_max[colour] = draw[colour]
    for colour,qty in draw.items():
        if colour not in new_max or new_max[colour]< qty:
            new_max[colour] = qty
    return new_max

assert reduce(max_bag, games[0]["draws"]) == parse_draw("4 red, 2 green, 6 blue")

game_powers = [48, 12, 1560, 630, 36]

for game,power in zip(games, game_powers):
    assert bag_power(reduce(max_bag, game["draws"])) == power

Right, that works, lets try in prod!

sum([bag_power(reduce(max_bag, game["draws"])) for game in real_games])

58269

Next

2023-12-3

Previous

2023-12-1