Skip to main content

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

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