Dazbo's Advent of Code solutions, written in Python
regexDataclassLambda FunctionDefaultdict
It is the Reindeer Olympics. We’re told that our reindeer each have two attributes:
So our input data looks something like this:
Comet can fly 14 km/s for 10 seconds, but then must rest for 127 seconds.
Dancer can fly 16 km/s for 11 seconds, but then must rest for 162 seconds.
After exactly 2503 seconds, what distance has the winning reindeer travelled?
This is easy enough to do.
Given a number of seconds, we just need to determine how many of these seconds were spent travelling. We can do this by:
I’ve decided to encapsulate all of this in a Reindeer class:
@dataclass
class Reindeer():
""" A reindeer flies at *speed* (km/s) for a given *duration* (s), then must *rest* (s). """
name: str
speed: int
duration: int
rest: int
def cycle_time(self) -> int:
return self.duration + self.rest
def distance_at_t(self, t: int) -> int:
"""Computes the distance travelled by this reindeer in the time given """
total_cycles = t // self.cycle_time()
remainder = t % self.cycle_time()
remainder_travelling = min(remainder, self.duration)
return ((self.duration*total_cycles) + remainder_travelling) * self.speed
Here I’ve made use of a dataclass
, since we don’t need to modify any variables, once our reindeer has been instantiated.
Note how this Reindeer
class knows how to work out how far it has travelled, given any number of seconds.
Now let’s process the input data, using regex:
def process_reindeer_data(data) -> list[Reindeer]:
"""Process input lines, and return a dict of reindeer.
Each line is of the format:
"Vixen can fly 8 km/s for 8 seconds, but then must rest for 53 seconds."
Returns: [list]: Reindeers
"""
reindeer_pattern = re.compile(r"^(\w+) can fly (\d+) km/s for (\d+) seconds, but then must rest for (\d+) seconds.")
reindeer_list = []
for line in data:
reindeer_name, speed, duration, rest = reindeer_pattern.findall(line)[0]
reindeer_list.append(Reindeer(reindeer_name, int(speed), int(duration), int(rest)))
return reindeer_list
Finally, let’s pull it together with a main()
method:
def main():
# input_file = os.path.join(SCRIPT_DIR, SAMPLE_INPUT_FILE)
input_file = os.path.join(SCRIPT_DIR, INPUT_FILE)
with open(input_file, mode="rt") as f:
data = f.read().splitlines()
reindeer_list = process_reindeer_data(data)
print("Part 1")
winner = max(reindeer_list, key=lambda x: x.distance_at_t(TIME))
print(f"Winning reindeer: {winner.name} with distance of {winner.distance_at_t(TIME)}.")
This is pretty simple:
process_reindeer_data()
function to retrieve all the reindeer as a list
of Reindeer
objects.max()
to get the reindeer with the largest distance_at_t(TIME)
, by passing in this method as a lambda function to the max()
function.Easy!
Now we’re told that every second that passes, the reindeer that is in the lead at that second gets a point.
After exactly 2503 seconds, how many points does the winning reindeer have?
To solve this:
t=2503
, we get the reindeer that has travelled furthest for each second until (and including) t=2503
.defaultdict
to store the running score for each reindeer. Recall that by using a defaultdict
with a value type of int
, we don’t have to initialise the reindeer scores to 0. This happens implicitly, the first time we reference a reindeer.All I need to do is add this code:
print("\nPart 2")
reindeer_points = defaultdict(int)
# we need to determine the winner for each second that has elapsed,
# and allocate a point to that reindeer
for i in range(1, TIME+1):
current_winner = max(reindeer_list, key=lambda x: x.distance_at_t(i))
reindeer_points[current_winner.name] += 1
print("Points:")
for a_reindeer in reindeer_points:
print(f"{a_reindeer}: {reindeer_points[a_reindeer]}")
winner_on_points = max(reindeer_points.items(), key=lambda x: x[1])[0]
print(f"Winning reindeer: {winner_on_points} who has {reindeer_points[winner_on_points]} points.")
Here’s the complete solution:
from dataclasses import dataclass
import os
import re
import time
from collections import defaultdict
SCRIPT_DIR = os.path.dirname(__file__)
INPUT_FILE = "input/input.txt"
SAMPLE_INPUT_FILE = "input/sample_input.txt"
TIME = 2503
@dataclass
class Reindeer():
""" A reindeer flies at *speed* (km/s) for a given *duration* (s), then must *rest* (s). """
name: str
speed: int
duration: int
rest: int
def cycle_time(self) -> int:
return self.duration + self.rest
def distance_at_t(self, t: int) -> int:
"""Computes the distance travelled by this reindeer in the time given """
total_cycles = t // self.cycle_time()
remainder = t % self.cycle_time()
remainder_travelling = min(remainder, self.duration)
return ((self.duration*total_cycles) + remainder_travelling) * self.speed
def main():
# input_file = os.path.join(SCRIPT_DIR, SAMPLE_INPUT_FILE)
input_file = os.path.join(SCRIPT_DIR, INPUT_FILE)
with open(input_file, mode="rt") as f:
data = f.read().splitlines()
reindeer_list = process_reindeer_data(data)
print("Part 1")
winner = max(reindeer_list, key=lambda x: x.distance_at_t(TIME))
print(f"Winning reindeer: {winner.name} with distance of {winner.distance_at_t(TIME)}.")
print("\nPart 2")
reindeer_points = defaultdict(int)
# we need to determine the winner for each second that has elapsed,
# and allocate a point to that reindeer
for i in range(1, TIME+1):
current_winner = max(reindeer_list, key=lambda x: x.distance_at_t(i))
reindeer_points[current_winner.name] += 1
print("Points:")
for a_reindeer in reindeer_points:
print(f"{a_reindeer}: {reindeer_points[a_reindeer]}")
winner_on_points = max(reindeer_points.items(), key=lambda x: x[1])[0]
print(f"Winning reindeer: {winner_on_points} who has {reindeer_points[winner_on_points]} points.")
def process_reindeer_data(data) -> list[Reindeer]:
"""Process input lines, and return a dict of reindeer.
Each line is of the format:
"Vixen can fly 8 km/s for 8 seconds, but then must rest for 53 seconds."
Returns: [list]: Reindeers
"""
reindeer_pattern = re.compile(r"^(\w+) can fly (\d+) km/s for (\d+) seconds, but then must rest for (\d+) seconds.")
reindeer_list = []
for line in data:
reindeer_name, speed, duration, rest = reindeer_pattern.findall(line)[0]
reindeer_list.append(Reindeer(reindeer_name, int(speed), int(duration), int(rest)))
return reindeer_list
if __name__ == "__main__":
t1 = time.perf_counter()
main()
t2 = time.perf_counter()
print(f"\nExecution time: {t2 - t1:0.4f} seconds")
The results look like this:
Part 1
Winning reindeer: Donner with distance of 2655.
Part 2
Points:
Dancer: 1
Rudolph: 863
Cupid: 13
Blitzen: 5
Prancer: 147
Comet: 21
Vixen: 1059
Donner: 394
Winning reindeer: Vixen who has 1059 points.
Execution time: 0.0125 seconds
That’s pretty fast. I’m happy with that code.