Last active
December 26, 2024 16:22
-
-
Save patrickfuller/3465e18cb5fbb85abd211db94a070edb to your computer and use it in GitHub Desktop.
Advent of Code 2024
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Solutions for 2024 Advent of Code puzzles. | |
https://adventofcode.com/2024 | |
""" | |
import argparse | |
from collections import Counter, defaultdict | |
from copy import deepcopy | |
from functools import cmp_to_key | |
from itertools import permutations, product | |
from math import gcd, log2, prod | |
import re | |
def day_1_part_1(): | |
"""Solve Day 1 Part 1 puzzle.""" | |
with open('day_1.txt') as in_file: | |
locations = [ | |
[int(x) for x in line.split()] | |
for line in in_file | |
] | |
left, right = zip(*locations) | |
return sum(abs(l - r) for l, r in zip(sorted(left), sorted(right))) | |
def day_1_part_2(): | |
"""Solve Day 1 Part 2 puzzle.""" | |
with open('day_1.txt') as in_file: | |
locations = [ | |
[int(x) for x in line.split()] | |
for line in in_file | |
] | |
left, right = zip(*locations) | |
right_count = Counter(right) | |
return sum(l * right_count[l] for l in left) | |
def day_2_part_1(): | |
"""Solve Day 2 Part 1 puzzle.""" | |
def check_report(report): | |
return all(i < j <= i + 3 for i, j in zip(report[:-1], report[1:])) | |
with open('day_2.txt') as in_file: | |
reports = [ | |
[int(x) for x in line.split()] | |
for line in in_file | |
] | |
safe_report_count = 0 | |
for report in reports: | |
is_safe = check_report(report) or check_report(report[::-1]) | |
safe_report_count += is_safe | |
return safe_report_count | |
def day_2_part_2(): | |
"""Solve Day 2 Part 2 puzzle.""" | |
def check_report(report): | |
return all(i < j <= i + 3 for i, j in zip(report[:-1], report[1:])) | |
def check_report_slices(report): | |
return any( | |
check_report(report[:i] + report[i+1:]) | |
for i in range(len(report)) | |
) | |
with open('day_2.txt') as in_file: | |
reports = [ | |
[int(x) for x in line.split()] | |
for line in in_file | |
] | |
safe_report_count = 0 | |
for report in reports: | |
is_safe = check_report(report) or check_report(report[::-1]) | |
if not is_safe: | |
is_safe = check_report_slices(report) or check_report_slices(report[::-1]) | |
safe_report_count += is_safe | |
return safe_report_count | |
def day_3_part_1(): | |
"""Solve Day 3 Part 1 puzzle.""" | |
with open('day_3.txt') as in_file: | |
memory = in_file.read() | |
matches = re.findall(r'mul\((\d{1,3}),(\d{1,3})\)', memory) | |
return sum(int(a) * int(b) for a, b in matches) | |
def day_3_part_2(): | |
"""Solve Day 3 Part 2 puzzle.""" | |
with open('day_3.txt') as in_file: | |
memory = in_file.read() | |
enabled = re.sub(r"don't\(\).*?(?:$|do\(\))", '', memory, flags=re.DOTALL) | |
matches = re.findall(r'mul\((\d{1,3}),(\d{1,3})\)', enabled) | |
return sum(int(a) * int(b) for a, b in matches) | |
def day_4_part_1(): | |
"""Solve Day 4 Part 1 puzzle.""" | |
with open('day_4.txt') as in_file: | |
word_search = [list(row.strip()) for row in in_file] | |
# Add padding to the word search to avoid edge cases | |
for i, row in enumerate(word_search): | |
word_search[i] = ['*'] + row + ['*'] | |
padded_row = ['*' for _ in range(len(word_search[0]))] | |
word_search = [padded_row] + word_search + [padded_row] | |
count = 0 | |
directions = [ | |
(-1, 0), (1, 0), (0, -1), (0, 1), | |
(-1, -1), (-1, 1), (1, -1), (1, 1), | |
] | |
for y, row in enumerate(word_search): | |
for x, char in enumerate(row): | |
if char != 'X': | |
continue | |
for dx, dy in directions: | |
for i, search_char in enumerate('MAS', start=1): | |
if word_search[y + i * dy][x + i * dx] != search_char: | |
break | |
else: | |
count += 1 | |
return count | |
def day_4_part_2(): | |
"""Solve Day 4 Part 2 puzzle.""" | |
with open('day_4.txt') as in_file: | |
word_search = [list(row.strip()) for row in in_file] | |
for i, row in enumerate(word_search): | |
word_search[i] = ['*'] + row + ['*'] | |
padded_row = ['*' for _ in range(len(word_search[0]))] | |
word_search = [padded_row] + word_search + [padded_row] | |
count = 0 | |
directions = [(-1, 1), (1, 1), (-1, -1), (1, -1)] | |
patterns = ['MSMS', 'MMSS', 'SSMM', 'SMSM'] | |
for y, row in enumerate(word_search): | |
for x, char in enumerate(row): | |
if char != 'A': | |
continue | |
pattern = ''.join(word_search[y + dy][x + dx] for dx, dy in directions) | |
if pattern in patterns: | |
count += 1 | |
return count | |
def day_5_part_1(): | |
"""Solve Day 5 Part 1 puzzle.""" | |
with open('day_5.txt') as in_file: | |
rule_data, update_data = in_file.read().strip().split('\n\n') | |
rules = {i: set() for i in range(10, 100)} | |
for rule in rule_data.split('\n'): | |
before, after = [int(x) for x in rule.split('|')] | |
rules[before].add(after) | |
total = 0 | |
for update in update_data.split('\n'): | |
pages = [int(x) for x in update.split(',')] | |
for i, page in enumerate(pages): | |
if any(other_page not in rules[page] for other_page in pages[i + 1:]): | |
break | |
else: | |
total += pages[len(pages) // 2] | |
return total | |
def day_5_part_2(): | |
"""Solve Day 5 Part 2 puzzle.""" | |
with open('day_5.txt') as in_file: | |
rule_data, update_data = in_file.read().strip().split('\n\n') | |
rules = {i: set() for i in range(10, 100)} | |
for rule in rule_data.split('\n'): | |
before, after = [int(x) for x in rule.split('|')] | |
rules[before].add(after) | |
def sort_key(x, y): | |
if y in rules[x]: | |
return -1 | |
elif x in rules[y]: | |
return 1 | |
return 0 | |
total = 0 | |
for update in update_data.split('\n'): | |
pages = [int(x) for x in update.split(',')] | |
for i, page in enumerate(pages): | |
if any(other_page not in rules[page] for other_page in pages[i + 1:]): | |
pages.sort(key=cmp_to_key(sort_key)) | |
total += pages[len(pages) // 2] | |
break | |
return total | |
def day_6_part_1(): | |
"""Solve Day 6 Part 1 puzzle.""" | |
with open('day_6.txt') as in_file: | |
lab_map = [list(row.strip()) for row in in_file] | |
directions = [(0, -1), (1, 0), (0, 1), (-1, 0)] | |
d = 0 | |
x, y = next((x, y) for y, row in enumerate(lab_map) for x, char in enumerate(row) if char == '^') | |
while 0 <= x < len(lab_map) and 0 <= y < len(lab_map[0]): | |
dx, dy = directions[d] | |
if lab_map[y][x] == '#': | |
x, y = x - dx, y - dy | |
d = (d + 1) % 4 | |
else: | |
lab_map[y][x] = 'X' | |
x, y = x + dx, y + dy | |
return sum(row.count('X') for row in lab_map) | |
def day_6_part_2(): | |
"""Solve Day 6 Part 2 puzzle.""" | |
with open('day_6.txt') as in_file: | |
lab_map = [list(row.strip()) for row in in_file] | |
directions = [(0, -1), (1, 0), (0, 1), (-1, 0)] | |
start = next((x, y) for y, row in enumerate(lab_map) for x, char in enumerate(row) if char == '^') | |
# Find all the traversed points. These are all we need to check for loops. | |
x, y = start | |
d = 0 | |
to_check = set() | |
while 0 <= x < len(lab_map) and 0 <= y < len(lab_map[0]): | |
dx, dy = directions[d] | |
if lab_map[y][x] == '#': | |
x, y = x - dx, y - dy | |
d = (d + 1) % 4 | |
else: | |
if lab_map[y][x] != '^': | |
to_check.add((x, y)) | |
x, y = x + dx, y + dy | |
def is_loop(m, x=start[0], y=start[1]): | |
d = 0 | |
traversed = {(x, y, d)} | |
while 0 <= x < len(m) and 0 <= y < len(m[0]): | |
dx, dy = directions[d] | |
if m[y][x] == '#': | |
x, y = x - dx, y - dy | |
d = (d + 1) % 4 | |
elif m[y][x] == 'X' and (x, y, d) in traversed: | |
return True | |
else: | |
m[y][x] = 'X' | |
traversed.add((x, y, d)) | |
x, y = x + dx, y + dy | |
return False | |
total = 0 | |
for x, y in to_check: | |
new_map = deepcopy(lab_map) | |
new_map[y][x] = '#' | |
if is_loop(new_map): | |
total += 1 | |
return total | |
def day_7_part_1(): | |
"""Solve Day 7 Part 1 puzzle.""" | |
with open('day_7.txt') as in_file: | |
equations = [] | |
for line in in_file: | |
test, values = line.split(': ') | |
equations.append((int(test), [int(x) for x in values.split()])) | |
def check_equation(test, values, operators): | |
total = values[0] | |
for operator, value in zip(operators, values[1:]): | |
if total > test: | |
return False | |
elif operator == '+': | |
total += value | |
elif operator == '*': | |
total *= value | |
return total == test | |
total = 0 | |
for test, values in equations: | |
for option in product('+*', repeat=len(values)-1): | |
if check_equation(test, values, option): | |
total += test | |
break | |
return total | |
def day_7_part_2(): | |
"""Solve Day 7 Part 2 puzzle.""" | |
with open('day_7.txt') as in_file: | |
equations = [] | |
for line in in_file: | |
test, values = line.split(': ') | |
equations.append((int(test), [int(x) for x in values.split()])) | |
def check_equation(test, values, operators): | |
total = values[0] | |
for operator, value in zip(operators, values[1:]): | |
if total > test: | |
return False | |
elif operator == '+': | |
total += value | |
elif operator == '*': | |
total *= value | |
elif operator == '|': | |
total = int(str(total) + str(value)) | |
return total == test | |
total = 0 | |
for test, values in equations: | |
for option in product('+*|', repeat=len(values)-1): | |
if check_equation(test, values, option): | |
total += test | |
break | |
return total | |
def day_8_part_1(): | |
"""Solve Day 8 Part 1 puzzle.""" | |
with open('day_8.txt') as in_file: | |
antenna_map = [list(row.strip()) for row in in_file] | |
antennas = {} | |
for y, row in enumerate(antenna_map): | |
for x, char in enumerate(row): | |
if char != '.': | |
antennas.setdefault(char, set()).add((x, y)) | |
antinodes = set() | |
for points in antennas.values(): | |
for (x1, y1), (x2, y2) in permutations(points, 2): | |
dx, dy = x2 - x1, y2 - y1 | |
x, y = x2 + dx, y2 + dy | |
if 0 <= x < len(antenna_map[0]) and 0 <= y < len(antenna_map): | |
antinodes.add((x, y)) | |
return len(antinodes) | |
def day_8_part_2(): | |
"""Solve Day 8 Part 2 puzzle.""" | |
with open('day_8.txt') as in_file: | |
antenna_map = [list(row.strip()) for row in in_file] | |
antennas = {} | |
for y, row in enumerate(antenna_map): | |
for x, char in enumerate(row): | |
if char != '.': | |
antennas.setdefault(char, set()).add((x, y)) | |
antinodes = set() | |
for points in antennas.values(): | |
for (x1, y1), (x2, y2) in permutations(points, 2): | |
dx, dy = x2 - x1, y2 - y1 | |
divisor = gcd(dx, dy) | |
dx, dy = dx // divisor, dy // divisor | |
x, y = x2, y2 | |
while 0 <= x < len(antenna_map[0]) and 0 <= y < len(antenna_map): | |
antinodes.add((x, y)) | |
x, y = x + dx, y + dy | |
return len(antinodes) | |
def day_9_part_1(): | |
"""Solve Day 9 Part 1 puzzle.""" | |
with open('day_9.txt') as in_file: | |
disk_map = [int(x) for x in in_file.read().strip()] | |
blocks = [] | |
for i, block_count in enumerate(disk_map): | |
file_id = i // 2 if i % 2 == 0 else None | |
blocks += [file_id] * block_count | |
l, r = 0, len(blocks) - 1 | |
while l < r: | |
if blocks[r] is None: | |
r -= 1 | |
elif blocks[l] is not None: | |
l += 1 | |
else: | |
blocks[l], blocks[r] = blocks[r], blocks[l] | |
return sum(i * (x or 0) for i, x in enumerate(blocks)) | |
def day_9_part_2(): | |
"""Solve Day 9 Part 2 puzzle. | |
This one took me a couple of hours. Lots of fussing with indices before | |
deciding to rework the data structure. | |
""" | |
with open('day_9.txt') as in_file: | |
disk_map = [int(x) for x in in_file.read().strip()] | |
disk, spaces, files = [], [], [] | |
for i, block_count in enumerate(disk_map): | |
is_file = (i % 2 == 0) | |
if is_file: | |
files.append((i // 2, block_count, len(disk))) | |
else: | |
spaces.append((block_count, len(disk))) | |
disk += [int(i // 2) if is_file else None for _ in range(block_count)] | |
while files: | |
file_id, file_len, file_index = files.pop() | |
for i, (space_len, space_index) in enumerate(spaces): | |
if file_index < space_index: | |
break | |
if space_len >= file_len: | |
disk[file_index:file_index + file_len] = [None] * file_len | |
disk[space_index:space_index + file_len] = [file_id] * file_len | |
if space_len > file_len: | |
spaces[i] = (space_len - file_len, space_index + file_len) | |
else: | |
spaces.pop(i) | |
break | |
return sum(i * (x or 0) for i, x in enumerate(disk)) | |
def day_10_part_1(): | |
"""Solve Day 10 Part 1 puzzle.""" | |
with open('day_10.txt') as in_file: | |
topographic_map = [[int(x) for x in row.strip()] for row in in_file] | |
def traverse(root_x, root_y): | |
summits = set() | |
queue = [(root_x, root_y, 0)] | |
while queue: | |
x, y, depth = queue.pop(0) | |
next_depth = depth + 1 | |
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: | |
nx, ny = x + dx, y + dy | |
in_bounds = 0 <= nx < len(topographic_map[0]) and 0 <= ny < len(topographic_map) | |
if in_bounds and topographic_map[ny][nx] == next_depth: | |
if next_depth == 9: | |
summits.add((nx, ny)) | |
else: | |
queue.append((nx, ny, next_depth)) | |
return len(summits) | |
total = 0 | |
for y, row in enumerate(topographic_map): | |
for x, cell in enumerate(row): | |
if cell == 0: | |
total += traverse(x, y) | |
return total | |
def day_10_part_2(): | |
"""Solve Day 10 Part 2 puzzle.""" | |
with open('day_10.txt') as in_file: | |
topographic_map = [[int(x) for x in row.strip()] for row in in_file] | |
def traverse(root_x, root_y): | |
total = 0 | |
queue = [(root_x, root_y, 0)] | |
while queue: | |
x, y, depth = queue.pop(0) | |
next_depth = depth + 1 | |
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: | |
nx, ny = x + dx, y + dy | |
in_bounds = 0 <= nx < len(topographic_map[0]) and 0 <= ny < len(topographic_map) | |
if in_bounds and topographic_map[ny][nx] == next_depth: | |
if next_depth == 9: | |
total += 1 | |
else: | |
queue.append((nx, ny, next_depth)) | |
return total | |
total = 0 | |
for y, row in enumerate(topographic_map): | |
for x, cell in enumerate(row): | |
if cell == 0: | |
total += traverse(x, y) | |
return total | |
def day_11_part_1(): | |
"""Solve Day 11 Part 1 puzzle.""" | |
with open('day_11.txt') as in_file: | |
stones = [int(x) for x in in_file.read().split(' ')] | |
def blink(stones): | |
new_stones = [] | |
for i, stone in enumerate(stones): | |
stone_str = str(stone) | |
if stone == 0: | |
stones[i] = 1 | |
elif len(stone_str) % 2 == 0: | |
midpoint = len(stone_str) // 2 | |
stones[i] = int(stone_str[:midpoint]) | |
new_stones.append(int(stone_str[midpoint:])) | |
else: | |
stones[i] = stone * 2024 | |
return stones + new_stones | |
for _ in range(25): | |
stones = blink(stones) | |
return len(stones) | |
def day_11_part_2(): | |
"""Solve Day 11 Part 2 puzzle.""" | |
with open('day_11.txt') as in_file: | |
stones = [int(x) for x in in_file.read().split(' ')] | |
stone_counts = Counter(stones) | |
def blink(stone_counts): | |
new_counts = defaultdict(int) | |
for stone, count in stone_counts.items(): | |
if stone == 0: | |
new_counts[1] += count | |
elif len(str(stone)) % 2 == 1: | |
new_counts[stone * 2024] += count | |
else: | |
div = 10 ** (len(str(stone)) // 2) | |
new_counts[stone // div] += count | |
new_counts[stone % div] += count | |
return new_counts | |
for _ in range(75): | |
stone_counts = blink(stone_counts) | |
return sum(count for _, count in stone_counts.items()) | |
def day_12_part_1(): | |
"""Solve Day 12 Part 1 puzzle.""" | |
with open('day_12.txt') as in_file: | |
grid = [list(line.strip()) for line in in_file] | |
visited = [[False] * len(grid[0]) for _ in range(len(grid))] | |
def flood_fill(x, y, target): | |
points = set() | |
in_bounds = 0 <= y < len(grid) and 0 <= x < len(grid[0]) | |
if not in_bounds or visited[y][x] or grid[y][x] != target: | |
return points | |
points.add((x, y)) | |
visited[y][x] = True | |
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: | |
points.update(flood_fill(x + dx, y + dy, target)) | |
return points | |
def get_area_and_perimeter(points): | |
area = len(points) | |
perimeter = 0 | |
for x, y in points: | |
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: | |
if (x + dx, y + dy) not in points: | |
perimeter += 1 | |
return area, perimeter | |
regions = [] | |
for y, row in enumerate(grid): | |
for x, cell in enumerate(row): | |
if not visited[y][x]: | |
points = flood_fill(x, y, cell) | |
regions.append(get_area_and_perimeter(points)) | |
return sum(area * perimeter for area, perimeter in regions) | |
def day_12_part_2(): | |
"""Solve Day 12 Part 2 puzzle.""" | |
with open('day_12.txt') as in_file: | |
grid = [list(line.strip()) for line in in_file] | |
visited = [[False] * len(grid[0]) for _ in range(len(grid))] | |
def flood_fill(x, y, target): | |
points = set() | |
in_bounds = 0 <= y < len(grid) and 0 <= x < len(grid[0]) | |
if not in_bounds or visited[y][x] or grid[y][x] != target: | |
return points | |
points.add((x, y)) | |
visited[y][x] = True | |
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: | |
points.update(flood_fill(x + dx, y + dy, target)) | |
return points | |
def get_area_and_sides(points): | |
count = 0 | |
for x, y in points: | |
# The number of sides is equal to the number of corners | |
for dx, dy in [(1, 1), (1, -1), (-1, 1), (-1, -1)]: | |
w, h, d = (x + dx, y), (x, y + dy), (x + dx, y + dy) | |
# Convex corners | |
# █. .█ .? ?. | |
# .? ?. █. .█ | |
if w not in points and h not in points: | |
count += 1 | |
# Concave corners | |
# ██ ██ █. .█ | |
# █. .█ ██ ██ | |
if w in points and h in points and d not in points: | |
count += 1 | |
return len(points), count | |
regions = [] | |
for y, row in enumerate(grid): | |
for x, cell in enumerate(row): | |
if not visited[y][x]: | |
points = flood_fill(x, y, cell) | |
regions.append(get_area_and_sides(points)) | |
return sum(area * sides for area, sides in regions) | |
def day_13_part_1(): | |
"""Solve Day 13 Part 1 puzzle.""" | |
pattern = re.compile( | |
r'Button A: X\+(\d+), Y\+(\d+)\n' | |
r'Button B: X\+(\d+), Y\+(\d+)\n' | |
r'Prize: X=(\d+), Y=(\d+)' | |
) | |
def parse(machine): | |
return [int(x) for x in pattern.search(machine).groups()] | |
with open('day_13.txt') as in_file: | |
machines = [parse(machine) for machine in in_file.read().split('\n\n')] | |
# This is a linear algebra problem, where we are given the constants | |
# a_x * a + b_x * b = prize_x | |
# a_y * a + b_y * b = prize_y | |
# min(3 * a + b) | |
# As a matrix | |
# [ a_x b_x ] [ a ] = [ prize_x ] | |
# [ a_y b_y ] [ b ] = [ prize_y ] | |
total = 0 | |
for a_x, a_y, b_x, b_y, prize_x, prize_y in machines: | |
det = a_x * b_y - b_x * a_y | |
if det == 0: | |
continue | |
a = (prize_x * b_y - prize_y * b_x) / det | |
b = (a_x * prize_y - a_y * prize_x) / det | |
if a >= 0 and b >= 0 and a.is_integer() and b.is_integer(): | |
total += int(3 * a + b) | |
return total | |
def day_13_part_2(): | |
"""Solve Day 13 Part 2 puzzle.""" | |
pattern = re.compile( | |
r'Button A: X\+(\d+), Y\+(\d+)\n' | |
r'Button B: X\+(\d+), Y\+(\d+)\n' | |
r'Prize: X=(\d+), Y=(\d+)' | |
) | |
def parse(machine): | |
return [int(x) for x in pattern.search(machine).groups()] | |
with open('day_13.txt') as in_file: | |
machines = [parse(machine) for machine in in_file.read().split('\n\n')] | |
total = 0 | |
for a_x, a_y, b_x, b_y, prize_x, prize_y in machines: | |
prize_x += 10000000000000 | |
prize_y += 10000000000000 | |
det = a_x * b_y - b_x * a_y | |
if det == 0: | |
continue | |
a = (prize_x * b_y - prize_y * b_x) / det | |
b = (a_x * prize_y - a_y * prize_x) / det | |
if a >= 0 and b >= 0 and a.is_integer() and b.is_integer(): | |
total += int(3 * a + b) | |
return total | |
def day_14_part_1(): | |
"""Solve Day 14 Part 1 puzzle.""" | |
pattern = re.compile(r'p=(\d+),(\d+) v=(-?\d+),(-?\d+)') | |
with open('day_14.txt') as in_file: | |
robots = [[int(x) for x in pattern.search(robot).groups()] for robot in in_file] | |
w, h = 101, 103 | |
for _ in range(100): | |
for i, (x, y, dx, dy) in enumerate(robots): | |
x = (x + dx) % w | |
y = (y + dy) % h | |
robots[i] = (x, y, dx, dy) | |
quadrants = [0] * 4 | |
m_x, m_y = w // 2, h // 2 | |
for x, y, _, _ in robots: | |
if x == m_x or y == m_y: | |
continue | |
quadrant = 2 * (x > m_x) + (y > m_y) | |
quadrants[quadrant] += 1 | |
return prod(quadrants) | |
def day_14_part_2(): | |
"""Solve Day 14 Part 2 puzzle.""" | |
pattern = re.compile(r'p=(\d+),(\d+) v=(-?\d+),(-?\d+)') | |
with open('day_14.txt') as in_file: | |
robots = [[int(x) for x in pattern.search(robot).groups()] for robot in in_file] | |
w, h = 101, 103 | |
def render(robots): | |
grid = [[' '] * w for _ in range(h)] | |
for x, y, _, _ in robots: | |
grid[y][x] = '#' | |
return '\n'.join(''.join(row) for row in grid) | |
for number_of_seconds in range(1, 100000): | |
for i, (x, y, dx, dy) in enumerate(robots): | |
x = (x + dx) % w | |
y = (y + dy) % h | |
robots[i] = (x, y, dx, dy) | |
# Metric to filter through the search space | |
row_counts = Counter(y for _, y, _, _ in robots) | |
col_counts = Counter(x for x, _, _, _ in robots) | |
max_row = max(row_counts.values()) | |
max_col = max(col_counts.values()) | |
if max_row * max_col > 600: | |
print(f'\nSeconds = {number_of_seconds}') | |
print(render(robots)) | |
input() | |
return number_of_seconds | |
def day_15_part_1(): | |
"""Solve Day 15 Part 1 puzzle.""" | |
with open('day_15.txt') as in_file: | |
raw_map, raw_instructions = in_file.read().split('\n\n') | |
warehouse = [list(row) for row in raw_map.splitlines()] | |
instructions = raw_instructions.replace('\n', '') | |
robot = next((x, y) | |
for y, row in enumerate(warehouse) for x, cell in enumerate(row) | |
if cell == '@' | |
) | |
moves = { | |
'^': (0, -1), | |
'>': (1, 0), | |
'v': (0, 1), | |
'<': (-1, 0), | |
} | |
for instruction in instructions: | |
rx, ry = robot | |
dx, dy = moves[instruction] | |
nx, ny = rx + dx, ry + dy | |
if warehouse[ny][nx] == '#': | |
continue | |
elif warehouse[ny][nx] == '.': | |
robot = (nx, ny) | |
warehouse[ny][nx] = '@' | |
warehouse[ry][rx] = '.' | |
elif warehouse[ny][nx] == 'O': | |
ox, oy = nx, ny | |
to_move = [] | |
while warehouse[oy][ox] == 'O': | |
ox += dx | |
oy += dy | |
to_move.append((ox, oy)) | |
if warehouse[oy][ox] == '.': | |
for x, y in to_move: | |
warehouse[y][x] = 'O' | |
robot = (nx, ny) | |
warehouse[ny][nx] = '@' | |
warehouse[ry][rx] = '.' | |
# print('\n'.join(''.join(row) for row in warehouse)) | |
return sum(x + 100 * y | |
for y, row in enumerate(warehouse) for x, cell in enumerate(row) | |
if cell == 'O' | |
) | |
def day_15_part_2(): | |
"""Solve Day 15 Part 2 puzzle.""" | |
# DOES NOT WORK | |
# Could not think of elegant way of doing this | |
# Inelegant way was boring | |
with open('day_15_test.txt') as in_file: | |
raw_map, raw_instructions = in_file.read().split('\n\n') | |
raw_map = (raw_map | |
.replace('#', '##') | |
.replace('O', '[]') | |
.replace('.', '..') | |
.replace('@', '@.') | |
) | |
warehouse = [list(row) for row in raw_map.splitlines()] | |
instructions = raw_instructions.replace('\n', '') | |
boxes = set() | |
walls = set() | |
for y, row in enumerate(warehouse): | |
for x, cell in enumerate(row): | |
if cell == '[': | |
boxes.add((x, y)) | |
elif cell == '#': | |
walls.add((x, y)) | |
elif cell == '@': | |
robot = (x, y) | |
moves = { | |
'^': (0, -1), | |
'>': (1, 0), | |
'v': (0, 1), | |
'<': (-1, 0), | |
} | |
for instruction in instructions: | |
rx, ry = robot | |
dx, dy = moves[instruction] | |
nx, ny = rx + dx, ry + dy | |
if (nx, ny) in walls: | |
continue | |
# Moving right, hits box | |
elif dx == 1 and (nx, ny) in boxes: | |
ox, oy = nx, ny | |
to_move = [] | |
while (ox, oy) in boxes: | |
to_move.append((ox, oy)) | |
ox += 2 | |
if (ox, oy) not in walls: | |
for x, y in to_move: | |
boxes.add((x + 1, y)) | |
boxes.remove((x, y)) | |
robot = (nx, ny) | |
# Moving left, hits box | |
elif dx == -1 and (nx - 1, ny) in boxes: | |
ox, oy = nx, ny | |
to_move = [] | |
while (ox, oy) in boxes: | |
to_move.append((ox, oy)) | |
ox -= 2 | |
if (ox, oy) not in walls: | |
for x, y in to_move: | |
boxes.add((x - 1, y)) | |
boxes.remove((x, y)) | |
robot = (nx, ny) | |
# Moving up, hits left box | |
elif dy == -1 and (nx, ny) in boxes: | |
ox, oy = nx, ny | |
box_row = [(ox, oy)] | |
to_move = [] | |
for (bx, by) in box_row: | |
if (bx - 1, by - 1) in boxes: | |
continue | |
# Moving up, hits right box | |
elif dy == -1 and (nx - 1, ny) in boxes: | |
continue | |
# Moving down, hits box | |
elif dy == 1 and ((nx, ny) in boxes or (nx - 1, ny) in boxes): | |
continue | |
else: | |
robot = (nx, ny) | |
def render(): | |
output = [] | |
for y, _ in enumerate(warehouse): | |
new_row = '' | |
for x, _ in enumerate(row): | |
if (x, y) in walls: | |
new_row += '#' | |
elif (x, y) in boxes: | |
new_row += '[' | |
elif (x - 1, y) in boxes: | |
new_row += ']' | |
elif (x, y) == robot: | |
new_row += '@' | |
else: | |
new_row += ' ' | |
output.append(new_row) | |
return '\n'.join(output) | |
print(render()) | |
return sum(x + 100 * y | |
for y, row in enumerate(warehouse) for x, cell in enumerate(row) | |
if cell == '[' | |
) | |
def day_16_part_1(): | |
"""Solve Day 16 Part 1 puzzle.""" | |
with open('day_16.txt') as in_file: | |
maze = [list(row) for row in in_file.read().splitlines()] | |
start = next((x, y) for y, row in enumerate(maze) for x, cell in enumerate(row) if cell == 'S') | |
directions = [(0, 1), (1, 0), (0, -1), (-1, 0)] | |
queue = [(start, 0, (1, 0))] | |
traversed = set() | |
while True: | |
(x, y), distance, direction = queue.pop(0) | |
if maze[y][x] == 'E': | |
return distance | |
for dx, dy in directions: | |
nx, ny = x + dx, y + dy | |
if maze[ny][nx] == '#' or (nx, ny) in traversed: | |
continue | |
if (dx, dy) == direction: | |
points = 0 | |
elif (-dx, -dy) == direction: | |
points = 2000 | |
else: | |
points = 1000 | |
queue.append(((nx, ny), distance + 1 + points, (dx, dy))) | |
traversed.add((x, y)) | |
queue.sort(key=lambda x: x[1]) | |
def day_16_part_2(): | |
"""Solve Day 16 Part 2 puzzle.""" | |
with open('day_16.txt') as in_file: | |
maze = [list(row) for row in in_file.read().splitlines()] | |
start = next((x, y) for y, row in enumerate(maze) for x, cell in enumerate(row) if cell == 'S') | |
directions = [(0, 1), (1, 0), (0, -1), (-1, 0)] | |
queue = [(start, 0, (1, 0), [start])] | |
traversed = {} | |
winning_score = 98484 | |
winning_paths = set() | |
while queue: | |
(x, y), distance, direction, path = queue.pop(0) | |
if maze[y][x] == 'E': | |
winning_paths |= set(path) | |
for dx, dy in directions: | |
nx, ny = x + dx, y + dy | |
if maze[ny][nx] == '#': | |
continue | |
if (dx, dy) == direction: | |
points = 0 | |
elif (-dx, -dy) == direction: | |
points = 2000 | |
else: | |
points = 1000 | |
new_score = distance + 1 + points | |
if traversed.get((nx, ny), winning_score) < new_score: | |
continue | |
p = path.copy() | |
p.append((nx, ny)) | |
queue.append(((nx, ny), new_score, (dx, dy), p)) | |
traversed[(x, y)] = new_score | |
queue.sort(key=lambda x: x[1]) | |
return len(winning_paths) | |
def day_17_part_1(): | |
"""Solve Day 17 Part 1 puzzle.""" | |
pattern = re.compile( | |
r'Register A: (\d+)\n' | |
r'Register B: (\d+)\n' | |
r'Register C: (\d+)\n\n' | |
r'Program: ([\d,]+)' | |
) | |
with open('day_17.txt') as in_file: | |
data = pattern.search(in_file.read()).groups() | |
registers = [int(x) for x in data[:-1]] | |
program = [int(x) for x in data[-1].split(',')] | |
pointer = 0 | |
output = [] | |
def combo(x): | |
return x if x < 4 else registers[x - 4] | |
def adv(x): | |
registers[0] = registers[0] // 2**combo(x) | |
def bxl(x): | |
registers[1] = registers[1] ^ x | |
def bst(x): | |
registers[1] = combo(x) % 8 | |
def jnz(x): | |
nonlocal pointer | |
if registers[0] != 0 and pointer != x: | |
pointer = x - 2 | |
def bxc(x): | |
registers[1] ^= registers[2] | |
def out(x): | |
output.append(combo(x) % 8) | |
def bdv(x): | |
registers[1] = registers[0] // 2**combo(x) | |
def cdv(x): | |
registers[2] = registers[0] // 2**combo(x) | |
instructions = [adv, bxl, bst, jnz, bxc, out, bdv, cdv] | |
while pointer < len(program): | |
opcode, operand = program[pointer:pointer + 2] | |
instructions[opcode](operand) | |
pointer += 2 | |
return ','.join(str(x) for x in output) | |
def day_17_part_2(): | |
"""Solve Day 17 Part 2 puzzle.""" | |
pattern = re.compile( | |
r'Register A: (\d+)\n' | |
r'Register B: (\d+)\n' | |
r'Register C: (\d+)\n\n' | |
r'Program: ([\d,]+)' | |
) | |
with open('day_17_test.txt') as in_file: | |
data = pattern.search(in_file.read()).groups() | |
registers = [int(x) for x in data[:-1]] | |
program = [int(x) for x in data[-1].split(',')] | |
# HERE | |
# Idea is to invert the program and run it backwards | |
# to get to initial register state | |
# 2,4,1,1,7,5,4,6,0,3,1,4,5,5,3,0 | |
# bst, bxc, bxl | |
output = program.copy() | |
def combo(x): | |
return x if x < 4 else registers[x - 4] | |
def adv(x): | |
registers[0] = registers[0] // 2**combo(x) | |
def bxl(x): | |
registers[1] = registers[1] ^ x | |
def bst(x): | |
registers[1] = combo(x) % 8 | |
def jnz(x): | |
nonlocal pointer | |
if registers[0] != 0 and pointer != x: | |
pointer = x - 2 | |
def bxc(x): | |
registers[1] ^= registers[2] | |
def out(x): | |
output.append(combo(x) % 8) | |
def bdv(x): | |
registers[1] = registers[0] // 2**combo(x) | |
def cdv(x): | |
registers[2] = registers[0] // 2**combo(x) | |
instructions = [adv, bxl, bst, jnz, bxc, out, bdv, cdv] | |
for a in range(100000000, 10000000000000): | |
if a % 100000 == 0: | |
print(a) | |
registers = [a, 0, 0] | |
output = [] | |
pointer = 0 | |
while pointer < len(program): | |
opcode, operand = program[pointer:pointer + 2] | |
instructions[opcode](operand) | |
pointer += 2 | |
if ','.join(str(x) for x in output) == data[-1]: | |
return a | |
parser = argparse.ArgumentParser(description="Run 2024 Advent of Code solutions.") | |
parser.add_argument('day', type=int, choices=range(1, 26), metavar='[1, 25]', | |
help='Advent code day to run.') | |
args = parser.parse_args() | |
part_1, part_2 = locals()[f'day_{args.day}_part_1'], locals()[f'day_{args.day}_part_2'] | |
print(f"Part 1:\n{part_1()}\n") | |
print(f"Part 2:\n{part_2()}") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment