Juha-Matti Santala
Community Builder. Dreamer. Adventurer.

Advent of Code - 2022

This is a solution to Day 1 of Advent of Code 2022.

Day 1 - Calorie Counting

Santa's reindeer typically eat regular reindeer food, but they need a lot of magical energy to deliver presents on Christmas. For that, their favorite snack is a special type of star fruit that only grows deep in the jungle. The Elves have brought you on their annual expedition to the grove where the fruit grows.

To supply enough magical energy, the expedition needs to retrieve a minimum of fifty stars by December 25th. Although the Elves assure you that the grove has plenty of fruit, you decide to grab any fruit you see along the way, just in case.

Collect stars by solving puzzles. Two puzzles will be made available on each day in the Advent calendar; the second puzzle is unlocked when you complete the first. Each puzzle grants one star. Good luck!

The jungle must be too overgrown and difficult to navigate in vehicles or access from the air; the Elves' expedition traditionally goes on foot. As your boats approach land, the Elves begin taking inventory of their supplies. One important consideration is food - in particular, the number of Calories each Elf is carrying (your puzzle input).

The Elves take turns writing down the number of Calories contained by the various meals, snacks, rations, etc. that they've brought with them, one item per line. Each Elf separates their own inventory from the previous Elf's inventory (if any) by a blank line.

For example, suppose the Elves finish writing their items' Calories and end up with the following list:

1000
2000
3000

4000

5000
6000

7000
8000
9000

10000

This list represents the Calories of the food carried by five Elves:

  • The first Elf is carrying food with 1000, 2000, and 3000 Calories, a total of 6000 Calories.
  • The second Elf is carrying one food item with 4000 Calories.
  • The third Elf is carrying food with 5000 and 6000 Calories, a total of 11000 Calories.
  • The fourth Elf is carrying food with 7000, 8000, and 9000 Calories, a total of 24000 Calories.
  • The fifth Elf is carrying one food item with 10000 Calories.

In case the Elves get hungry and need extra snacks, they need to know which Elf to ask: they'd like to know how many Calories are being carried by the Elf carrying the most Calories. In the example above, this is 24000 (carried by the fourth Elf).

Happy December!

I'm very excited that it's December and Advent of Code again. Welcome to my solutions for this year, as I try to solve problems and help elves save the Christmas. This year, like the last, I'm solving these puzzles wiht Python, while writing them open and explaining my thinking and tools & approaches I use in these Jupyter Notebooks.

Let's get started!

Read input

Right away, we start with a multisection input and I learned that my last year's utils.read_multisection_input was written for a very different type of multisection input, so I started by rewriting that to allow any number of blocks.

My input readers accept three different parameters:

  • day which is a number to state which day's input we want to read
  • transformer function that will be run on each line of the input
  • example which tells the function if we want to read the actual input or an example input

Over the years I'e learned that it helps a lot when I keep the data input and modification separate from the other code so I can focus on the puzzle solving and get the same type of input data each day.

from utils import read_multisection_input

def transformer(line):
    return [int(value) for value in line.split('\n')]

calories = read_multisection_input(1, transformer)
example = read_multisection_input(1, transformer, example=True)

Part 1

Find the Elf carrying the most Calories. How many total Calories is that Elf carrying?

Our input here is a list of list of numbers, with each sublist indicating a single Elf's carry. We can use Python's generator expression to calculate the sum for each Elf and then take the max value of those.

Lists and generators

In Python, you can create a new list from existing with a list comprehension:

numbers = [1, 2, 3]
squares = [num**2 for num in numbers] # = [1, 4, 9]

This is very handy and nice as it allows us to map and filter values to create new lists. However, if the original list is very big, it can take a lot of memory to always create new lists.

That's where generators come in handy. A generator expression looks very similar to list comprehension but we use parenthesis instead of square brackets (or if the generator expression is immediately passed to a function like in my solution below, you can omit the extra parenthesis):

numbers = [1, 2, 3]
squares = (num**2 for num in numbers)

In this example, squares is a generator object. It will calculate the values when needed so it's very memory efficient for large quantities of data.

largest = max(sum(cal) for cal in calories)

print(f'Part 1: {largest}')
assert largest == 67016

Part 2

By the time you calculate the answer to the Elves' question, they've already realized that the Elf carrying the most Calories of food might eventually run out of snacks.

To avoid this unacceptable situation, the Elves would instead like to know the total Calories carried by the top three Elves carrying the most Calories. That way, even if one of those Elves runs out of snacks, they still have two backups.

In the example above, the top three Elves are the fourth Elf (with 24000 Calories), then the third Elf (with 11000 Calories), then the fifth Elf (with 10000 Calories). The sum of the Calories carried by these three elves is 45000.

Find the top three Elves carrying the most Calories. How many Calories are those Elves carrying in total?

Now, instead of just calculating the max value, we need to find the top three.

Here, I sort the sums (with reverse=True to sort decreasing), take first three elements with list slicing and calculate their sum.

Edit #1: As a friend pointed out to me, selecting first three items of a list sorted in descending order is the same as selecting last three items of a list sorted in ascending order. So in this solution, you could also so:

top_3 = sorted((sum(x) for x in calories))[-3:]

Indexing and slicing lists

To get an individual value from a Python list, you can use indexes:

numbers = [2, 4, 8, 16]
item = numbers[2] # == 8

By providing an index into a square bracket after the variable name, you can access an individual element. If the index is negative number, Python will count from the end:

numbers = [2, 4, 8, 16]
last = numbers[-1] # == 16

This is especially handy for accessing the last item in the list like above.

If you want to get a sublist, you can use slices.

numbers = [1, 2, 3, 4, 5]
middle_three = numbers[1:4] # == [2, 3, 4]

The first number is the start value and is inclusive and the second number is end and is not inclusive. So our slice 1:4 will include indexes 1, 2 and 3. If you want to include items from the beginning of the list, you can omit the start value and if you want to include items until the end, you can omit the end value:

numbers = [1, 2, 3, 4, 5]
first_three = numbers[:3] # == [1, 2, 3]
last_three = numbers[3:] # == [3, 4, 5]

If you omit both, you create a copy of the list:

numbers = [1, 2, 3]
new_numbers = numbers[:] # == [1, 2, 3]

You can also provide a third number: step. By default, slices will advance one number at the time but with step, we can adjust that. If the step is negative, it will go the list in reverse order.

numbers = [1, 2, 3, 4, 5, 6, 7, 8]
odds = numbers[::2] # == [1, 3, 5, 7]
evens = numbers[1::2] # == [2, 4, 6, 8]
reverse = numbers[::-1] # = [8, 7, 6, 5, 4, 3, 2, 1]
top_3 = sorted((sum(x) for x in calories), reverse=True)[:3]
total = sum(top_3)

print(f'Part 2: {total}')
assert total == 200116

2 stars! ⭐️⭐️

The first day is a wrap!

I'm really happy the first day was a straight-forward one as that helped me make sure that everything works and get myself adjusted into the Advent of Code mood for the next 23 days to come.