2
\$\begingroup\$

problem for day 2

Part 1

You're launched high into the atmosphere! The apex of your trajectory just barely reaches the surface of a large island floating in the sky. You gently land in a fluffy pile of leaves. It's quite cold, but you don't see much snow. An Elf runs over to greet you.

The Elf explains that you've arrived at Snow Island and apologizes for the lack of snow. He'll be happy to explain the situation, but it's a bit of a walk, so you have some time. They don't get many visitors up here; would you like to play a game in the meantime?

As you walk, the Elf shows you a small bag and some cubes which are either red, green, or blue. Each time you play this game, he will hide a secret number of cubes of each color in the bag, and your goal is to figure out information about the number of cubes.

To get information, once a bag has been loaded with cubes, the Elf will reach into the bag, grab a handful of random cubes, show them to you, and then put them back in the bag. He'll do this a few times per game.

You play several games and record the information from each game (your puzzle input). Each game is listed with its ID number (like the 11 in Game 11: ...) followed by a semicolon-separated list of subsets of cubes that were revealed from the bag (like 3 red, 5 green, 4 blue).

For example, the record of a few games might look like this:

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

In game 1, three sets of cubes are revealed from the bag (and then put back again). The first set is 3 blue cubes and 4 red cubes; the second set is 1 red cube, 2 green cubes, and 6 blue cubes; the third set is only 2 green cubes.

The Elf would first like to know which games would have been possible if the bag contained only 12 red cubes, 13 green cubes, and 14 blue cubes?

In the example above, games 1, 2, and 5 would have been possible if the bag had been loaded with that configuration. However, game 3 would have been impossible because at one point the Elf showed you 20 red cubes at once; similarly, game 4 would also have been impossible because the Elf showed you 15 blue cubes at once. If you add up the IDs of the games that would have been possible, you get 8.

Determine which games would have been possible if the bag had been loaded with only 12 red cubes, 13 green cubes, and 14 blue cubes. What is the sum of the IDs of those games?

Part 2

The Elf says they've stopped producing snow because they aren't getting any water! He isn't sure why the water stopped; however, he can show you how to get to the water source to check it out for yourself. It's just up ahead!

As you continue your walk, the Elf poses a second question: in each game you played, what is the fewest number of cubes of each color that could have been in the bag to make the game possible?

Again consider the example games from earlier:

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

In game 1, the game could have been played with as few as 4 red, 2 green, and 6 blue cubes. If any color had even one fewer cube, the game would have been impossible. Game 2 could have been played with a minimum of 1 red, 3 green, and 4 blue cubes. Game 3 must have been played with at least 20 red, 13 green, and 6 blue cubes. Game 4 required at least 14 red, 3 green, and 15 blue cubes. Game 5 needed no fewer than 6 red, 3 green, and 2 blue cubes in the bag. The power of a set of cubes is equal to the numbers of red, green, and blue cubes multiplied together. The power of the minimum set of cubes in game 1 is 48. In games 2-5 it was 12, 1560, 630, and 36, respectively. Adding up these five powers produces the sum 2286.

For each game, find the minimum set of cubes that must have been present. What is the sum of the power of these sets?

import re


def part_1():
    max_red = 12
    max_green = 13
    max_blue = 14

    passed_game_sum = 0

    def process_game(rs):
        for r in rs:
            for c in r.strip().split(','):
                pick = re.findall(r'^([0-9]+) ([a-z]+)', c.strip())

                if pick[0][1] == 'red' and int(pick[0][0]) > max_red:
                    return 'failed', parsed_line[0][0]
                elif pick[0][1] == 'green' and int(pick[0][0]) > max_green:
                    return 'failed', parsed_line[0][0]
                elif pick[0][1] == 'blue' and int(pick[0][0]) > max_blue:
                    return 'failed', parsed_line[0][0]

        return 'passed', parsed_line[0][0]

    with open('input.txt', 'r') as file:
        for line in file.read().splitlines():
            parsed_line = re.findall(r'^Game ([0-9]+): (.+)$', line)
            rounds = parsed_line[0][1].split(';')

            if (game := process_game(rounds))[0] == 'passed':
                passed_game_sum += int(game[1])

        print(passed_game_sum)


def part_2():
    set_power = 0

    def process_game(rs):
        fewest_red = 0
        fewest_green = 0
        fewest_blue = 0

        for r in rs:
            for c in r.strip().split(','):
                pick = re.findall(r'^([0-9]+) ([a-z]+)', c.strip())[0]

                quantity = int(pick[0])
                color = pick[1]

                if color == 'red' and quantity > fewest_red:
                    fewest_red = quantity
                elif color == 'green' and quantity > fewest_green:
                    fewest_green = quantity
                elif color == 'blue' and quantity > fewest_blue:
                    fewest_blue = quantity

        return fewest_red * fewest_green * fewest_blue

    with open('input.txt', 'r') as file:

        for line in file.read().splitlines():
            parsed_line = re.findall(r'^Game ([0-9]+): (.+)$', line)
            rounds = parsed_line[0][1].split(';')

            set_power += process_game(rounds)

        print(set_power)


if __name__ == '__main__':
    part_1()
    part_2()

```
\$\endgroup\$
0

1 Answer 1

5
\$\begingroup\$

Use functions instead of repeating yourself. The solutions for part 1 and part 2 contain similar code for file reading, text parsing, and so forth. Repeated code can become a real hassle: every bug fix or improvement requires editing in multiple sites.

A function to read input. This is perhaps the simplest and most obvious place to start. If you do continue with AoC, you might find yourself enhancing a function like this to be able to read from a file, STDIN, or even the clipboard.

def read_input(path):
    with open(path) as fh:
        return [line.strip() for line in fh]

Don't use findall() if you know the regex will return only one thing. All of your regexes will only return one match. Using re.findall() in a situation like that mis-communicates the intent of your code.

Some parsing advice. Give meaningful names to constants like separators, markers, etc. And when feasible, prefer basic techniques like strip and split over regex.

GID_SEP = ': '
DRAW_SEP = '; '
PICK_SEP = ', '

Keep separate tasks separate. Your current solutions have two levels of operation: (1) line processing, where you do some parsing and some top-level calculations; and (2) game processing, where you again combine parsing with calculation. It usually results in more flexible, maintainable, and readable code to separate the tasks in a sharper way. For example: read input; parse input into meaningful data; make calculations; communicate results.

When feasible, make data declarative rather than cryptic. For example, you parse each pick into a two-element tuple and your code has to make repeated use of things like pick[0][1]. That demands a lot of mental overload for a reader of your code (meaning yourself). Much better is to unpack that data into more readable parts, which can be done with something as simple as n, color = pick.

Modeling the data. This problem seems well suited to a couple of dataclasses: one to represent a game, and one to represent a draw of cubes. Simple dataclasses like these not only push your code in the direction of meaningful, declarative data; they also create natural homes for related tasks, such as the following: the ability to create a Game instance from an input line of text; the ability to create a Draw instance from some text; and the ability to compare Draw instances to see whether one is less than another, which is the essence of part 1, and a behavior we get for free from dataclass(order = True).

from dataclasses import dataclass

@dataclass(order = True)
class Draw:
    red: int = 0
    green: int = 0
    blue: int = 0

    @classmethod
    def from_text(cls, text):
        # Example: "3 green, 4 blue, 1 red".
        draw = cls()
        for pick in text.split(PICK_SEP):
            n, col = pick.split()
            setattr(draw, col, int(n))
        return draw

@dataclass
class Game:
    gid: int
    draws: list[Draw]

    @classmethod
    def from_text(cls, text):
        # Example: "Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red".
        gid_text, draws_text = text.split(GID_SEP)
        return cls(
            gid = int(gid_text.split()[1]),
            draws = [
                Draw.from_text(dt)
                for dt in draws_text.split(DRAW_SEP)
            ],
        )

When feasible, don't hardcode file paths. When working on a program, it's often helpful to experiment with different inputs. If you hardcode input file paths, experimentation becomes a bit of a hassle. Much better to parameterize the file path so you can pass it as a command-line argument. Here's one way to write the orchestration code.

import sys

def main(args):
    path = args[0]
    games = [
        Game.from_text(line)
        for line in read_input(path)
    ]
    print(part_1(games))

if __name__ == '__main__':
    main(sys.argv[1:])

With effective data and clear separation of tasks, the solution code often becomes simple. For part 1, we take some Game instances and return the sum of the game IDs that have all draws less than or equal to the cube inventory (which we can also represent as a "draw").

def part_1(games):
    inventory = Draw(red = 12, green = 13, blue = 14)
    return sum(
        g.gid
        for g in games
        if all(d <= inventory for d in g.draws)
    )

Part 2: a few ideas to consider. Here's a sketch of how I might solve this part. Good luck!

class Draw:
    ...

    @property
    def power(self):
        return self.red * self.green * self.blue

    def satisfy(self, other):
        # Take another Draw.
        # Increase self to satisfy needs of other.
        ...

@dataclass
class Game:
    ...

    @property
    def minimum_draw(self):
        # Return the minimum Draw to satisfy the game's draws.
        inventory = Draw()
        ...

def part_2(games):
    return sum(
        g.minimum_draw.power
        for g in games
    )
\$\endgroup\$
1
  • 1
    \$\begingroup\$ I think I would add that it might be worthwhile to convert the list comprehensions in read_input and main into generator comprehensions. It probably doesn't matter for this, but could help if the input file was substantially longer. But +1 from me. \$\endgroup\$
    – Teepeemm
    Dec 4, 2023 at 13:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.