2015
Day 1: Not Quite Lisp
Part 1
Count parentheses to determine final floor. Opening parenthesis means up one floor, closing means down one floor. It could have been done in a more efficient way.
floor = 0
for instruction in data[0]:
if instruction == '(':
floor += 1
elif instruction == ')':
floor -= 1
print floor
Part 2
Find first position that enters basement (floor -1). This meant stepping through the floors which I had already done in part 1 rather than simply counting all the left parentesise and counting the right parenthesise and subtracting right from left.
floor = 0
position = 1
for instruction in data[0]:
if instruction == '(':
floor += 1
elif instruction == ')':
floor -= 1
if floor == -1:
print position
break
position += 1
Day 2: I Was Told There Would Be No Math
Part 1
Calculate wrapping paper needed for presents based on dimensions. Need surface area plus smallest side area.
box = 0
l, w, h = int(dimensions[0]), int(dimensions[1]), int(dimensions[2])
box += 2*l*w
box += 2*w*h
box += 2*l*h
dimensions = [l,w,h]
box += sorted(dimensions)[0]*sorted(dimensions)[1]
Part 2
Calculate ribbon needed: smallest perimeter plus volume for bow.
l, w, h = int(dimensions[0]), int(dimensions[1]), int(dimensions[2])
dimensions = sorted([l,w,h])
ribbon = (dimensions[0]+dimensions[1])*2 # Smallest perimeter
bow = l*h*w # Volume for bow
total = ribbon + bow
Day 3: Perfectly Spherical Houses in a Vacuum
Part 1
Track Santa's movement through a grid, following directional instructions. Each arrow moves one space in that direction, and we count unique coordinates visited.
x = 0
y = 0
visits = []
visits.append(tuple([x,y]))
for instruction in instructions:
if instruction == "^":
x += 1
elif instruction == "<":
y -= 1
elif instruction == ">":
y += 1
elif instruction == "v":
x -= 1
visits.append(tuple([x,y]))
print len(set(visits))
Part 2
Now Santa and Robo-Santa alternate moves, starting from the same position. Track both of their positions and count unique houses visited by either of them.
x = [0,0] # Santa and Robo-Santa x positions
y = [0,0] # Santa and Robo-Santa y positions
actor = 0 # Toggle between Santa (0) and Robo-Santa (1)
visits = []
visits.append(tuple([x[actor],y[actor]]))
for instruction in instructions:
if instruction == "^":
x[actor] += 1
elif instruction == "<":
y[actor] -= 1
elif instruction == ">":
y[actor] += 1
elif instruction == "v":
x[actor] -= 1
visits.append(tuple([x[actor],y[actor]]))
actor = 0 if actor == 1 else 1
Day 4: The Ideal Stocking Stuffer
Part 1
Find lowest number that produces MD5 hash starting with five zeros when combined with input.
Part 2
Similar to part 1 but requires six leading zeros, testing hash mining performance.
input = "ckczppom"
x = 0
hash = ""
while hash[:6] != "000000":
hash = hashlib.md5(input+str(x)).hexdigest()
x += 1
Day 5: Doesn't He Have Intern-Elves For This?
Part 1
String validation using multiple regex patterns for vowels, doubles, and forbidden strings.
import re
count = 0
prog1 = re.compile(r".*[aeiou].*[aeiou].*[aeiou].*") # Three vowels
prog2 = re.compile(r"(.)\1") # Double letter
prog3 = re.compile(r"(ab|cd|pq|xy)") # Forbidden strings
for line in data:
result1 = prog1.search(line)
result2 = prog2.search(line)
result3 = prog3.search(line)
if result1 != None and result2 != None and result3 == None:
count += 1
Part 2
Advanced string patterns using regex for repeating pairs and letter sandwiches.
import re
count = 0
prog1 = re.compile(r"(.{2}).*\1") # Pair appears twice
prog2 = re.compile(r"((.).)\2") # Letter repeats with one between
for line in data:
result1 = prog1.search(line)
result2 = prog2.search(line)
if result1 != None and result2 != None:
count += 1
Day 6: Probably a Fire Hazard
Part 1
Manipulate a 1000x1000 grid of lights with various operations (on, off, toggle).
Part 2
Similar grid operations but with brightness levels instead of binary states.
def do(this, that, what):
this = this.strip().split(',')
that = that.strip().split(',')
for x in range(int(this[0]), int(that[0])+1):
for y in range(int(this[1]), int(that[1])+1):
if what == 'on':
board[x][y] += 1
if what == 'off':
board[x][y] -= 1
if board[x][y] < 0:
board[x][y] = 0
if what == 'toggle':
board[x][y] += 2
# Initialize 1000x1000 grid
board = [[0 for y in range(1000)] for x in range(1000)]
# Process instructions
for line in data:
words = line.split(" ")
if words[1] == 'on':
do(words[2], words[4], words[1])
elif words[1] == 'off':
do(words[2], words[4], words[1])
else:
do(words[1], words[3], words[0])
# Calculate total brightness
total = sum(sum(row) for row in board)
Day 7: Some Assembly Required
Part 1
Process circuit instructions to calculate wire values through bitwise operations. Each wire can carry a 16-bit signal and gates perform various operations (AND, OR, NOT, SHIFT).
Original iterative solution:
wires = {}
errors = 1
while errors >= 1:
errors = 0
for line in data:
try:
instruction = line.split()
# Direct assignment
if len(instruction) == 3:
if instruction[0].isdigit():
wires[instruction[2]] = int(instruction[0])
else:
wires[instruction[2]] = wires[instruction[0]]
# NOT operation
if len(instruction) == 4:
if instruction[1].isdigit():
wires[instruction[3]] = int(instruction[1]) ^ 65535
else:
wires[instruction[3]] = wires[instruction[1]] ^ 65535
# Binary operations
if len(instruction) == 5:
if instruction[1] == 'AND':
val1 = int(instruction[0]) if instruction[0].isdigit() else wires[instruction[0]]
wires[instruction[4]] = val1 & wires[instruction[2]]
elif instruction[1] == 'OR':
val1 = int(instruction[0]) if instruction[0].isdigit() else wires[instruction[0]]
wires[instruction[4]] = val1 | wires[instruction[2]]
elif instruction[1] == 'LSHIFT':
wires[instruction[4]] = wires[instruction[0]] << int(instruction[2])
elif instruction[1] == 'RSHIFT':
wires[instruction[4]] = wires[instruction[0]] >> int(instruction[2])
except:
errors += 1
Alternative using NetworkX to handle dependencies:
import networkx as nx
def build_circuit(data):
G = nx.DiGraph()
operations = {}
for line in data:
out = line.split(' -> ')
dest = out[1].strip()
source = out[0].split()
# Handle different instruction types
if len(source) == 1: # Direct assignment
if source[0].isdigit():
G.add_node(dest, value=int(source[0]))
else:
G.add_edge(source[0], dest)
operations[dest] = lambda x: x
elif source[0] == 'NOT':
G.add_edge(source[1], dest)
operations[dest] = lambda x: ~x & 65535
else: # Binary operations
G.add_edge(source[0], dest)
G.add_edge(source[2], dest)
if source[1] == 'AND':
operations[dest] = lambda x, y: x & y
elif source[1] == 'OR':
operations[dest] = lambda x, y: x | y
elif source[1] == 'LSHIFT':
operations[dest] = lambda x, y: (x << int(y)) & 65535
elif source[1] == 'RSHIFT':
operations[dest] = lambda x, y: x >> int(y)
return G, operations
# Process in topological order
G, operations = build_circuit(data)
values = {}
for wire in nx.topological_sort(G):
if wire not in values:
deps = list(G.predecessors(wire))
if not deps: # No dependencies, must be a constant
values[wire] = G.nodes[wire]['value']
elif len(deps) == 1: # Unary operation
values[wire] = operations[wire](values[deps[0]])
else: # Binary operation
values[wire] = operations[wire](values[deps[0]], values[deps[1]])
Part 2
Reset circuit and override wire 'b' with previous value of 'a'.
Original approach:
wires = {}
wires['b'] = 956 # Value from Part 1
# Rest same as Part 1
NetworkX approach:
G, operations = build_circuit(data)
G.nodes['b']['value'] = 956 # Override b
# Rest same as Part 1
The NetworkX solution offers several advantages:
- Handles dependencies explicitly through graph structure
- Processes wires in correct order using topological sort
- More efficient as each wire is calculated exactly once
- Cleaner separation between circuit structure and evaluation
Day 8: Matchsticks
Part 1
Calculate the difference between string literals in code and their in-memory string values, handling escape sequences.
import re
clean = 0
total = 0
for line in data:
total += (len(line)-1) # -1 for newline
# Remove surrounding quotes
line = line[1:len(line)-2]
# Replace hex escapes with single character
line = re.sub(r"\\x[0-9a-f]{2}", "#", line)
# Replace escaped quotes with single character
line = re.sub(r'\\"', "#", line)
# Replace double backslashes with single character
line = re.sub(r'\\\\', "#", line)
clean += len(line)
print(total - clean)
Part 2
Encode strings by escaping special characters and adding quotes, then find length difference.
import re
start = 0
encoded = 0
for line in data:
line = line[:len(line)-1] # Remove newline
start += len(line)
# Double up backslashes
line = re.sub(r'\\', '##', line)
# Escape quotes
line = re.sub(r'\"', '\\"', line)
# Add 2 for surrounding quotes
encoded += len(line) + 2
print(encoded - start)
Key concepts:
- Regular expressions for string manipulation
- Handling multiple types of escape sequences
- String length calculations with different encodings
- Progressive string transformation
Common patterns:
- Using re.sub() for replacements
- Processing strings in multiple passes
- Tracking lengths before and after transformations
The puzzle introduces:
- String escaping and encoding
- Regular expression substitutions
- Length difference calculations
- Multi-step string processing
Day 9: All in a Single Night
Part 1
Find the shortest route that visits each location exactly once, given a set of distances between locations.
from itertools import permutations
import sys
# Build graph of distances
places = set()
distances = dict()
for line in data:
(source, _, dest, _, distance) = line.split()
places.add(source)
places.add(dest)
distances.setdefault(source, dict())[dest] = int(distance)
distances.setdefault(dest, dict())[source] = int(distance)
# Try all possible routes
shortest = sys.maxsize
for items in permutations(places):
dist = sum(map(lambda x, y: distances[x][y], items[:-1], items[1:]))
shortest = min(shortest, dist)
print(f"shortest: {shortest}")
Part 2
Find the longest route that visits each location exactly once.
# Same setup as Part 1, just track longest instead
longest = 0
for items in permutations(places):
dist = sum(map(lambda x, y: distances[x][y], items[:-1], items[1:]))
longest = max(longest, dist)
print(f"longest: {longest}")
Alternative visualization approach using NetworkX:
import networkx as nx
import matplotlib.pyplot as plt
def visualize_routes(distances):
G = nx.Graph()
# Add edges with weights
for source in distances:
for dest in distances[source]:
G.add_edge(source, dest, weight=distances[source][dest])
# Create layout and draw
pos = nx.spring_layout(G)
nx.draw(G, pos, with_labels=True, node_color='lightblue',
node_size=500, font_size=10, font_weight='bold')
# Add edge labels
edge_labels = nx.get_edge_attributes(G, 'weight')
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels)
return G
# Usage:
G = visualize_routes(distances)
Key concepts:
- Using itertools.permutations for brute force path finding
- Bidirectional graph representation with dictionaries
- Clever use of map() with multiple iterables
- Default dictionaries for graph building
Common patterns:
- Graph representation with nested dictionaries
- Using permutations for exhaustive search
- Pairwise iteration over route points
- Running min/max tracking
The puzzle introduces:
- Traveling Salesman Problem (TSP)
- Graph theory basics
- Permutation generation
- Distance calculations
- Path optimization
Day 10: Elves Look, Elves Say
Part 1
Generate the "Look-and-Say" sequence 40 times starting with "1321131112". Each term describes the previous term by counting consecutive digits.
Regex approach:
import re
input = "1321131112"
for step in range(40):
# Find groups of repeated digits
regex = re.compile(r"(([0-9])\2*)")
output = regex.findall(input)
# Build new sequence
input = ""
for each in output:
input = input + str(len(each[0])) + each[1]
print(len(input))
Alternative iterative approach:
def look_and_say(sequence):
result = []
current = sequence[0]
count = 1
for digit in sequence[1:]:
if digit == current:
count += 1
else:
result.extend([str(count), current])
current = digit
count = 1
result.extend([str(count), current])
return ''.join(result)
sequence = "1321131112"
for step in range(40):
sequence = look_and_say(sequence)
Part 2
Same process but for 50 iterations instead of 40.
# Both approaches work the same, just change range(40) to range(50)
Key concepts:
- Pattern matching with regex
- String building
- Sequence generation
- Grouping consecutive elements
The regex pattern (([0-9])\2*)
breaks down as:
([0-9])
captures any digit (first group)\2*
matches zero or more occurrences of the same digit- Outer parentheses create the full match group
The iterative approach:
- More explicit logic
- Often faster than regex for this case
- More memory efficient
- Easier to understand for those unfamiliar with regex
Common optimizations:
- Using list and join() instead of string concatenation
- Pre-allocating result list size
- Using itertools.groupby for grouping (though often slower for this case)
Day 11: Corporate Policy
Part 1
A uniquely interesting puzzle where manual solving was actually more efficient than coding. The task of finding the next valid password following specific rules could be solved through simple observation and logical deduction.
While coding wasn't necessary, implementing it programmatically explores several valuable programming concepts:
def has_straight(password):
for i in range(len(password)-2):
if (ord(password[i+1]) == ord(password[i]) + 1 and
ord(password[i+2]) == ord(password[i]) + 2):
return True
return False
def has_pairs(password):
pairs = set()
i = 0
while i < len(password)-1:
if password[i] == password[i+1]:
pairs.add(password[i])
i += 2
else:
i += 1
return len(pairs) >= 2
Key programming concepts demonstrated:
- Working with character ASCII values using ord()
- String iteration and indexing
- Pattern matching in sequences
- Set usage for unique pair tracking
- Sequential validation checks
Part 2
Finding the next valid password after part 1's answer. Again, trivially solvable by hand but demonstrates the reusability of programmatic solutions.
This puzzle is a great example of when to consider whether writing code is the most efficient approach. While the programmatic solution teaches valuable concepts, the manual solution was far more straightforward.
Day 12: JSAbacusFramework.io
Part 1
Extract all numbers from a JSON document and sum them, including negative numbers.
import re
# Find all numbers in the JSON string
regex = re.compile(r"-*[0-9]+")
numbers = regex.findall(data[0])
# Convert to integers and sum
total = sum(int(num) for num in numbers)
print(total)
Part 2
Same as part 1, but exclude numbers in any object (and its children) that has a property with value "red".
import json
def sum_numbers(obj):
if isinstance(obj, list):
return sum(sum_numbers(item) for item in obj)
if isinstance(obj, dict):
# Skip this object if it contains "red" as a value
if "red" in obj.values():
return 0
return sum(sum_numbers(value) for value in obj.values())
if isinstance(obj, int):
return obj
return 0
# Parse JSON and process
data = json.loads(data[0])
total = sum_numbers(data)
print(total)
Key concepts:
- Regular expressions for number extraction
- JSON parsing and traversal
- Recursive object processing
- Type checking with isinstance()
The puzzle introduces:
- Working with JSON data
- Pattern matching with regex
- Recursive data structure traversal
- Conditional sum calculations
The contrast between parts 1 and 2 is interesting:
- Part 1 can use simple regex without understanding the JSON structure
- Part 2 requires proper JSON parsing and tree traversal
- Shows when string processing vs structured parsing is appropriate
Day 13: Knights of the Dinner Table
Part 1
Calculate optimal seating arrangement for happiness by considering bidirectional happiness values between adjacent pairs. I came back to this very recently now I have a better understanding of how useful itertools is.
import re
from itertools import permutations
def calculate_happiness(arrangement, happiness):
total = 0
n = len(arrangement)
for i in range(n):
person = arrangement[i]
next_person = arrangement[(i + 1) % n]
total += happiness[person][next_person]
total += happiness[next_person][person]
return total
# Build happiness dictionary
happiness = {}
people = set()
for line in lines:
match = re.match(r'(\w+) would (gain|lose) (\d+) happiness units by sitting next to (\w+)', line)
person1, gainlose, amount, person2 = match.groups()
amount = int(amount) * (-1 if gainlose == 'lose' else 1)
if person1 not in happiness:
happiness[person1] = {}
happiness[person1][person2] = amount
people.add(person1)
people.add(person2)
# Calculate maximum happiness
first_person = list(people)[0]
rest_people = list(people)[1:]
max_happiness = max(calculate_happiness([first_person] + list(perm), happiness)
for perm in permutations(rest_people))
Part 2
Add yourself to the seating arrangement with zero happiness impact.
# Add yourself with zero happiness values
happiness['you'] = {person: 0 for person in people}
for person in people:
happiness[person]['you'] = 0
people.add('you')
# Use same calculation as Part 1
Day 14: Reindeer Olympics
Part 1
Calculate distance traveled by reindeer with different speed, flying time, and rest time patterns.
def calculate_distance(reindeer_stats, time):
speed = reindeer_stats['speed']
fly_time = reindeer_stats['fly_time']
rest_time = reindeer_stats['rest_time']
cycle_time = fly_time + rest_time
complete_cycles = time // cycle_time
remaining_time = min(fly_time, time % cycle_time)
return speed * (complete_cycles * fly_time + remaining_time)
# Calculate for each reindeer after 2503 seconds
race_time = 2503
distances = {name: calculate_distance(stats, race_time)
for name, stats in reindeer.items()}
max_distance = max(distances.values())
Part 2
Award points each second to reindeer in the lead, return highest point total.
def calculate_points(reindeer, total_time):
points = {name: 0 for name in reindeer}
for second in range(1, total_time + 1):
# Get current distances
current_distances = {name: calculate_distance(stats, second)
for name, stats in reindeer.items()}
# Award points to leaders
max_distance = max(current_distances.values())
for name, distance in current_distances.items():
if distance == max_distance:
points[name] += 1
return max(points.values())
race_time = 2503
max_points = calculate_points(reindeer, race_time)
Day 15: Science for Hungry People
Part 1
Find the optimal cookie recipe by trying different ingredient combinations that sum to 100 teaspoons.
def calculate_score(ingredients, amounts):
properties = {'capacity': 0, 'durability': 0, 'flavor': 0, 'texture': 0}
for ingredient, amount in amounts.items():
for prop in properties:
properties[prop] += ingredients[ingredient][prop] * amount
if any(value <= 0 for value in properties.values()):
return 0
return math.prod(properties.values())
def find_combinations(total, n):
if n == 1:
yield (total,)
return
for i in range(total + 1):
for j in find_combinations(total - i, n - 1):
yield (i,) + j
# Try all combinations summing to 100
max_score = max(calculate_score(ingredients, dict(zip(ingredients.keys(), combo)))
for combo in find_combinations(100, len(ingredients)))
Part 2
Same as part 1 but only consider recipes with exactly 500 calories.
max_score = 0
for combo in find_combinations(100, len(ingredients)):
amounts = dict(zip(ingredients.keys(), combo))
calories = sum(ingredients[ing]['calories'] * amt
for ing, amt in amounts.items())
if calories == 500:
max_score = max(max_score, calculate_score(ingredients, amounts))
Day 16: Aunt Sue
Part 1
Match MFCSAM readings to find the correct Aunt Sue.
mfcsam = {
'children': 3, 'cats': 7, 'samoyeds': 2, 'pomeranians': 3,
'akitas': 0, 'vizslas': 0, 'goldfish': 5, 'trees': 3,
'cars': 2, 'perfumes': 1
}
for sue_num, properties in aunts.items():
if all(mfcsam[prop] == value for prop, value in properties.items()):
return sue_num
Part 2
Same but with special comparison rules for certain properties.
def matches_reading(prop, value):
if prop in ['cats', 'trees']:
return value > mfcsam[prop]
if prop in ['pomeranians', 'goldfish']:
return value < mfcsam[prop]
return value == mfcsam[prop]
for sue_num, properties in aunts.items():
if all(matches_reading(prop, value) for prop, value in properties.items()):
return sue_num
Day 17: No Such Thing as Too Much
Part 1
Find all combinations of containers that sum to exact volume.
def find_combinations(containers, target, current=None, start=0):
if current is None:
current = []
if sum(current) == target:
return [current[:]]
if sum(current) > target or start >= len(containers):
return []
# Try with and without current container
with_current = find_combinations(containers, target,
current + [containers[start]], start + 1)
without_current = find_combinations(containers, target,
current, start + 1)
return with_current + without_current
combinations = find_combinations(containers, 150)
print(len(combinations))
Part 2
Find number of ways to use minimum number of containers.
combinations = find_combinations(containers, 150)
min_containers = min(len(combo) for combo in combinations)
min_container_count = sum(1 for combo in combinations
if len(combo) == min_containers)
Day 18: Like a GIF For Your Yard
Part 1
Simulate Conway's Game of Life variant for 100 steps.
def count_active_neighbors(grid, x, y):
neighbors = 0
rows, cols = len(grid), len(grid[0])
for dx, dy in itertools.product([-1, 0, 1], repeat=2):
if dx == dy == 0:
continue
new_x, new_y = x + dx, y + dy
if 0 <= new_x < rows and 0 <= new_y < cols:
neighbors += grid[new_x][new_y]
return neighbors
def simulate_step(grid):
new_grid = [[False] * len(grid[0]) for _ in range(len(grid))]
for i in range(len(grid)):
for j in range(len(grid[0])):
neighbors = count_active_neighbors(grid, i, j)
new_grid[i][j] = neighbors == 3 or (grid[i][j] and neighbors == 2)
return new_grid
# Run simulation for 100 steps
for _ in range(100):
grid = simulate_step(grid)
Part 2
Same but corners always stay on.
def fix_corners(grid):
rows, cols = len(grid), len(grid[0])
for x, y in [(0, 0), (0, cols-1), (rows-1, 0), (rows-1, cols-1)]:
grid[x][y] = True
# Run simulation with fixed corners
fix_corners(grid)
for _ in range(100):
grid = simulate_step(grid)
fix_corners(grid)