2019
Day 1: The Tyranny of the Rocket Equation
Part 1: Fuel Requirements
Calculate fuel needed for module masses: divide by 3 (round down), subtract 2.
# Basic fuel calculation for each module mass
total = 0
for reading in data:
reading = (int(reading) // 3) - 2
total += reading
Alternative: Use list comprehension for cleaner code:
total = sum((int(mass) // 3) - 2 for mass in data)
Part 2: Recursive Fuel
Each fuel mass requires its own fuel, repeat until no more fuel needed.
# Add fuel for fuel until no more needed
total = 0
for reading in data:
while reading > 0:
reading = (int(reading) // 3) - 2
if reading > 0:
total += reading
Alternative: Recursive solution:
def calc_fuel(mass):
fuel = (mass // 3) - 2
return 0 if fuel <= 0 else fuel + calc_fuel(fuel)
Useful Tools
math.floor()
for rounding- List comprehensions for cleaner code
- Recursion or iteration both work well
Day 2: 1202 Program Alarm
Part 1: IntCode Computer
Build a simple computer that processes opcodes (1 for add, 2 for multiply, 99 to halt). Each instruction uses four positions in memory.
def execute(memory):
pointer = 0
while pointer < len(memory):
opcode = memory[pointer]
if opcode == 99:
break
pos1, pos2, pos3 = memory[pointer + 1:pointer + 4]
if opcode == 1: # Addition
memory[pos3] = memory[pos1] + memory[pos2]
elif opcode == 2: # Multiplication
memory[pos3] = memory[pos1] * memory[pos2]
pointer += 4
return memory[0]
Alternative: Create a class for better organization and reuse:
class IntCodeComputer:
def __init__(self, program):
self.memory = program.copy()
self.pointer = 0
Part 2: Parameter Search
Find noun and verb (values 0-99) that produce target output when placed in positions 1 and 2.
target = 19690720
for noun in range(100):
for verb in range(100):
program = input_data.copy()
program[1] = noun
program[2] = verb
if execute(program) == target:
return 100 * noun + verb
Useful Tools
- Classes for organizing complex logic
- Copy lists to avoid modifying original data
- Exception handling for memory bounds
- Range for parameter search space
Day 3: Crossed Wires
Part 1: Find Closest Intersection
Track two wire paths on a grid and find their closest intersection point by Manhattan distance. Using NetworkX to track paths.
def add_wire_path(wire_moves, graph):
x, y = 0, 0
for move in wire_moves:
direction = move[0]
distance = int(move[1:])
prev_x, prev_y = x, y
# Update position based on direction
if direction == 'R': x += distance
elif direction == 'L': x -= distance
elif direction == 'U': y += distance
elif direction == 'D': y -= distance
# Add each point along the path
step_x = 1 if x > prev_x else -1 if x < prev_x else 0
step_y = 1 if y > prev_y else -1 if y < prev_y else 0
curr_x, curr_y = prev_x, prev_y
while (curr_x, curr_y) != (x, y):
curr_x += step_x
curr_y += step_y
graph.add_edge((curr_x - step_x, curr_y - step_y),
(curr_x, curr_y))
# Find intersections using set operations
intersections = set(g1.nodes()).intersection(set(g2.nodes()))
return min(abs(x) + abs(y) for x, y in intersections)
Part 2: Shortest Combined Path
Find intersection with minimum combined steps along both wires.
def add_wire_path(wire_moves, graph):
x, y = 0, 0
total_steps = 0
steps_to_point = {}
# Similar movement logic as Part 1
while (curr_x, curr_y) != (x, y):
curr_x += step_x
curr_y += step_y
total_steps += 1
# Track first visit to each point
if (curr_x, curr_y) not in steps_to_point:
steps_to_point[(curr_x, curr_y)] = total_steps
return steps_to_point
# Find minimum combined steps to any intersection
return min(steps_wire1[point] + steps_wire2[point]
for point in intersections)
Useful Tools
- NetworkX for graph operations
- Set operations for finding intersections
- Dictionary to track steps to points
- Manhattan distance calculation
Day 4: Secure Container
Part 1: Password Rules
Find valid passwords in range that have increasing digits and at least one double digit.
for input in inputrange:
input = list(map(int, str(input)))
# Check if sorted matches original (digits never decrease)
sorted_list = input[:]
sorted_list.sort()
# Check for doubles by comparing with unique set
if (sorted_list == input) and (list(set(input)) != input):
output.append(input)
Alternative approach using comprehension:
valid = [num for num in range(start, end)
if all(d1 <= d2 for d1, d2 in zip(str(num), str(num)[1:]))
and any(d1 == d2 for d1, d2 in zip(str(num), str(num)[1:]))]
Part 2: Exact Pairs
Add rule that one digit must appear exactly twice.
i = 0
while i < len(digits)-1:
count = 1
while i < len(digits)-1 and digits[i] == digits[i+1]:
count += 1
i += 1
if count == 2: # Check for exactly two adjacent digits
has_exact_double = True
i += 1
Useful Tools
- map() for converting strings to digits
- zip() for pairwise comparisons
- set() for finding unique digits
- list slicing for comparisons
Day 5: Sunny with a Chance of Asteroids
Part 1: Extended IntCode
This extended day 2 to dd input/output operations and parameter modes to IntCode computer. New opcodes: 3 (input), 4 (output).
def get_parameter_value(self, param_num, instruction):
# Get mode (0=position, 1=immediate) from instruction
mode = (instruction // (10 ** (param_num + 1))) % 10
param = self.memory[self.instruction_pointer + param_num]
if mode == 0: # Position mode
return self.memory[param]
else: # Immediate mode
return param
# Handle input/output operations
elif opcode == 3: # Input
result_pos = self.memory[self.instruction_pointer + 1]
self.memory[result_pos] = self.input_values.pop(0)
self.instruction_pointer += 2
elif opcode == 4: # Output
param1 = self.get_parameter_value(1, instruction)
self.outputs.append(param1)
self.instruction_pointer += 2
Part 2: Jumps and Comparisons
Add jump and comparison operations: opcodes 5 (jump-if-true), 6 (jump-if-false), 7 (less than), 8 (equals).
elif opcode == 5: # Jump-if-true
param1 = self.get_parameter_value(1, instruction)
param2 = self.get_parameter_value(2, instruction)
if param1 != 0:
self.instruction_pointer = param2
else:
self.instruction_pointer += 3
elif opcode == 7: # Less than
param1 = self.get_parameter_value(1, instruction)
param2 = self.get_parameter_value(2, instruction)
result_pos = self.memory[self.instruction_pointer + 3]
self.memory[result_pos] = 1 if param1 < param2 else 0
self.instruction_pointer += 4
Useful Tools
- Integer division/modulo for parameter modes
- Lists for input/output queues
- Class structure for computer state
- Parameter mode abstraction
Day 6: Universal Orbit Map
Part 1: Total Orbits
Count direct and indirect orbits in map using directed graph. Just like Day 3, NetworkX makes this task simpler.
def build_orbit_graph(orbit_pairs):
G = nx.DiGraph()
for orbited, orbiter in orbit_pairs:
G.add_edge(orbited, orbiter)
return G
def count_orbits(G):
total = 0
for node in G.nodes():
# Count steps to COM for each object
paths = nx.shortest_path_length(G, 'COM', node)
total += paths
return total
Part 2: Orbital Transfers
Find minimum transfers between objects using undirected graph.
def orbital_transfers(G):
# Find objects that YOU and SAN orbit
you_orbit = next(G.predecessors('YOU'))
san_orbit = next(G.predecessors('SAN'))
# Convert to undirected for transfers
UG = G.to_undirected()
return nx.shortest_path_length(UG, you_orbit, san_orbit)
Useful Tools
- NetworkX for graph operations
- Directed graphs for orbit hierarchy
- Undirected graphs for transfers
- Shortest path algorithms