python solutions through day17

This commit is contained in:
alexchao26
2024-12-27 16:10:36 -05:00
parent 5c4d5ebb0f
commit 249ab84e46
9 changed files with 1084 additions and 0 deletions
+121
View File
@@ -0,0 +1,121 @@
from dataclasses import dataclass
# index by index brute force with a sliding window optimization to make it linear...
def part1(input: str) -> int:
total_disk_space: int = 0
for x in list(input):
total_disk_space += int(x)
file_system: list[int] = [-1] * total_disk_space
is_file: bool = True
index: int = 0
file_number: int = 0
for x in list(input):
if not is_file:
index += int(x)
is_file = not is_file
else:
for _ in range(int(x)):
if is_file:
file_system[index] = file_number
index += 1
file_number += 1
is_file = not is_file
# rearrange file to left via sliding window
left: int = 0
right: int = len(file_system) - 1
while left < right:
if file_system[right] == -1:
right -= 1
elif file_system[left] != -1:
left += 1
elif file_system[left] == -1:
file_system[left], file_system[right] = (
file_system[right],
file_system[left],
)
left += 1
# checksum is index in string * number value (file number)
checksum: int = 0
for i in range(len(file_system)):
if file_system[i] == -1:
break
checksum += i * file_system[i]
return checksum
@dataclass
class FileSystemSpace:
start: int
size: int
file_number: int
def part2(input: str) -> int:
files: list[FileSystemSpace] = []
empty_spaces: list[FileSystemSpace] = []
is_file: bool = True
index: int = 0
for size in [int(x) for x in list(input)]:
file_or_empty = FileSystemSpace(index, size, len(files))
index += size
if is_file:
files.append(file_or_empty)
else:
empty_spaces.append(file_or_empty)
is_file = not is_file
# brute force finding a space to move each file into
for file in reversed(files):
for empty_space in empty_spaces:
# prevent moving files to higher spots in the file system...
if empty_space.start > file.start:
break
# large enough empty space found
if empty_space.size >= file.size:
file.start = empty_space.start
empty_space.start += file.size
empty_space.size -= file.size
break
# print_util(files)
checksum: int = 0
for file in files:
for x in range(file.size):
checksum += (file.start + x) * file.file_number
return checksum
def print_util(all_files: list[FileSystemSpace]):
last_index: int = 0
for file in all_files:
last_index = max(last_index, file.start + file.size)
fs: list[str] = ["."] * (last_index + 1)
for file in all_files:
for i in range(file.start, file.start + file.size):
fs[i] = str(file.file_number)
print("".join(fs))
example = """2333133121414131402"""
input = open("input.txt").read().strip()
print(f"part1 example: {part1(example)} want 1928")
print(f"part1: {part1(input)} want 6200294120911")
print(f"part2 example: {part2(example)} want 2858")
print(f"part2: {part2(input)} want 6227018762750")
+99
View File
@@ -0,0 +1,99 @@
def part1(input: str) -> int:
grid: list[list[int, int]] = []
for line in input.splitlines():
grid.append([int(x) for x in list(line)])
# alternative pythonic way...
# grid = [[int(char) for char in line] for line in input.splitlines()]
ans: int = 0
for r in range(len(grid)):
for c in range(len(grid[0])):
if grid[r][c] == 0:
ans += len(dfs_backtrack_unique_end_coords(grid, r, c, {(r, c)}))
return ans
# I misread the instructions slightly and went for a "unique paths" algo at first.
# It would be simpler to pass the "reachable 9s coords" set in as an arg and update
# it in the termination case, then take the length of that set in the part1() function.
# This would remove the need to combine the sets which is potentially expensive (and ugly)
def dfs_backtrack_unique_end_coords(
grid: list[list[int, int]], row: int, col: int, visited: set[tuple[int, int]]
) -> set[tuple[int, int]]:
if grid[row][col] == 9:
return {(row, col)}
all_coords: set[tuple[int, int]] = set()
for diff in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nextRow = row + diff[0]
nextCol = col + diff[1]
if not (0 <= nextRow < len(grid) and 0 <= nextCol < len(grid[0])):
continue
if (nextRow, nextCol) in visited:
continue
if grid[nextRow][nextCol] == grid[row][col] + 1:
visited.add((nextRow, nextCol))
new_coords = dfs_backtrack_unique_end_coords(
grid, nextRow, nextCol, visited
)
# combines the two sets, see comment above function def
all_coords.update(new_coords)
visited.remove((nextRow, nextCol))
return all_coords
def part2(input: str) -> int:
grid = [[int(char) for char in line] for line in input.splitlines()]
ans: int = 0
for r in range(len(grid)):
for c in range(len(grid[0])):
if grid[r][c] == 0:
ans += dfs_backtrack_unique_paths(grid, r, c, {(r, c)})
return ans
def dfs_backtrack_unique_paths(
grid: list[list[int, int]], row: int, col: int, visited: set[tuple[int, int]]
) -> int:
if grid[row][col] == 9:
# unique path found
return 1
total: int = 0
for diff in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nextRow = row + diff[0]
nextCol = col + diff[1]
if not (0 <= nextRow < len(grid) and 0 <= nextCol < len(grid[0])):
continue
if (nextRow, nextCol) in visited:
continue
if grid[nextRow][nextCol] == grid[row][col] + 1:
visited.add((nextRow, nextCol))
total += dfs_backtrack_unique_paths(grid, nextRow, nextCol, visited)
visited.remove((nextRow, nextCol))
return total
example = """89010123
78121874
87430965
96549874
45678903
32019012
01329801
10456732"""
input = open("input.txt").read().strip()
print(f"part1 example: {part1(example)} want 36")
print(f"part1: {part1(input)} want 782")
print(f"part2 example: {part2(example)} want 81")
print(f"part2 example: {part2(input)} want 1694")
+50
View File
@@ -0,0 +1,50 @@
# If the stone is engraved with the number 0, it is replaced by a stone engraved with the number 1.
# If the stone is engraved with a number that has an even number of digits, it is replaced by two stones. The left half of the digits are engraved on the new left stone, and the right half of the digits are engraved on the new right stone. (The new numbers don't keep extra leading zeroes: 1000 would become stones 10 and 0.)
# If none of the other rules apply, the stone is replaced by a new stone; the old stone's number multiplied by 2024 is engraved on the new stone.
# actually modelling this would be a pain in the ass, error-prone, and probably
# not fast enough for part 2 where there will presumably be more blinks
# instead if we only care about the final number of stones, we can just see
# how many stone each original stone splits into, and we can memoize it
def day11(input: str, blinks: int) -> int:
total_stones: int = 0
memo: dict[tuple[int, int], int] = {}
for stone in input.split(" "):
total_stones += calculate_final_stones_count(stone, blinks, memo)
return total_stones
def calculate_final_stones_count(num_as_str: str, blinks_left: int, memo) -> int:
key = (num_as_str, blinks_left)
if key in memo:
return memo[key]
if blinks_left == 0:
return 1
total_stones: int = 0
if num_as_str == "0":
total_stones += calculate_final_stones_count("1", blinks_left - 1, memo)
elif len(num_as_str) % 2 == 0:
# convert back and forth again to get rid of leading zeroes
left_num = str(int(num_as_str[: len(num_as_str) // 2]))
right_num = str(int(num_as_str[len(num_as_str) // 2 :]))
total_stones += calculate_final_stones_count(left_num, blinks_left - 1, memo)
total_stones += calculate_final_stones_count(right_num, blinks_left - 1, memo)
else:
new_num: int = str(int(num_as_str) * 2024)
total_stones += calculate_final_stones_count(new_num, blinks_left - 1, memo)
memo[key] = total_stones
return total_stones
example = """125 17"""
input = open("input.txt").read().strip()
print(f"part1 example: {day11(example, 6)} want 22")
print(f"part1 example: {day11(example, 25)} want 55312")
print(f"part1: {day11(input, 25)} want 189092")
print(f"part2: {day11(input, 75)} want 224869647102559")
+164
View File
@@ -0,0 +1,164 @@
from collections import defaultdict
def day12(input: str, part: int) -> int:
grid = [list(line) for line in input.splitlines()]
visited: set[tuple[int, int]] = {}
cost: int = 0
for r in range(len(grid)):
for c in range(len(grid[0])):
coord = (r, c)
if coord not in visited:
island_coords: set[tuple[int, int]] = set()
flood_fill_island(grid, r, c, visited, island_coords)
if part == 1:
cost += len(island_coords) * get_perimeter_of_island(island_coords)
elif part == 2:
edge_count: int = get_edge_count_of_island(island_coords)
cost += len(island_coords) * edge_count
else:
raise ("unexpected part")
return cost
# refactored to just populate the entire island_coords set and visited set
# just populates the island_coords so does not need to return anything
def flood_fill_island(
grid: list[list[str]],
row: int,
col: int,
visited: set[tuple[int, int]],
island_coords: set[tuple[int, int]],
):
visited[(row, col)] = True
island_coords.add((row, col))
for diff in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
next_row = row + diff[0]
next_col = col + diff[1]
if 0 <= next_row < len(grid) and 0 <= next_col < len(grid[0]):
if (next_row, next_col) in visited:
continue
# if in range, check if neighbor is same to recurse
if grid[next_row][next_col] == grid[row][col]:
# if does match and unvisited, recurse
flood_fill_island(grid, next_row, next_col, visited, island_coords)
# for part1 cost calculation
def get_perimeter_of_island(island_coords: set[tuple[int, int]]) -> int:
perimeter: int = 0
for coord in island_coords:
for diff in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
next_coord = (coord[0] + diff[0], coord[1] + diff[1])
if next_coord not in island_coords:
perimeter += 1
return perimeter
def get_edge_count_of_island(island_coords: set[tuple[int, int]]) -> int:
edges: int = 0
map_dir_to_coord: dict[tuple[int, int], set[tuple[int, int]]] = defaultdict(set)
for coord in island_coords:
for diff in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
# this coord has already been accounted for in an edge
if coord in map_dir_to_coord[diff]:
continue
next_coord = (coord[0] + diff[0], coord[1] + diff[1])
# not in the island means it is bordering an edge...
if next_coord not in island_coords:
# collect all coords that make up this same edge, basically we need to go perpendicular to diff
collect_all_coords_on_edge(island_coords, coord, diff, map_dir_to_coord)
edges += 1
return edges
perpendicular_dirs: dict[tuple[int, int], list[tuple[int, int]]] = {
(0, -1): [(-1, 0), (1, 0)],
(0, 1): [(-1, 0), (1, 0)],
(-1, 0): [(0, -1), (0, 1)],
(1, 0): [(0, -1), (0, 1)],
}
def collect_all_coords_on_edge(
island_coords: set[tuple[int, int]],
coord: tuple[int, int],
empty_dir_diff: tuple[int, int],
map_dir_to_coord: dict[tuple[int, int], set[tuple[int, int]]],
):
# mark self
map_dir_to_coord[empty_dir_diff].add(coord)
for perp in perpendicular_dirs[empty_dir_diff]:
next_coord = (coord[0] + perp[0], coord[1] + perp[1])
# we're collecting the entire connected edge that is facing a single direction,
# so stop checking if we're "off" the island
# do not need to check if we're inside the grid because we can just leverage island_coords
if next_coord not in island_coords:
continue
# if already visited for facing this direction, we can also skip
if next_coord in map_dir_to_coord[empty_dir_diff]:
continue
# continue if next_coord is not a contiguous part of the edge
if (next_coord[0] + empty_dir_diff[0], next_coord[1] + empty_dir_diff[1]) in island_coords:
continue
# need to continue exploring recursively
collect_all_coords_on_edge(
island_coords, next_coord, empty_dir_diff, map_dir_to_coord
)
small_example = """AAAA
BBCD
BBCC
EEEC"""
example = """RRRRIICCFF
RRRRIICCCF
VVRRRCCFFF
VVRCCCJFFF
VVVVCJJCFE
VVIVCCJJEE
VVIIICJJEE
MIIIIIJJEE
MIIISIJEEE
MMMISSJEEE"""
part2_example = """AAAAAA
AAABBA
AAABBA
ABBAAA
ABBAAA
AAAAAA"""
input = open("input.txt").read().strip()
print(f"part1 small_example: {day12(small_example, 1)} want 140")
print(f"part1 example: {day12(example, 1)} want 1930")
print(f"part1: {day12(input, 1)} want 1473408")
print(f"part2 small_example: {day12(small_example, 2)} want 80")
# this one helped debug the disjointed edges bug (second and fourth rows pointing east)
print(f"part2 example: {day12("""EEEEE
EXXXX
EEEEE
EXXXX
EEEEE""", 2)} want 236")
print(f"part2 part2_example: {day12(part2_example, 2)} want 368")
print(f"part2 example: {day12(example, 2)} want 1206")
print(f"part2: {day12(input, 2)} want 886364")
+62
View File
@@ -0,0 +1,62 @@
import re
def part1(input: str, part: int) -> int:
ans: int = 0
for machine in input.split("\n\n"):
lines = machine.split("\n")
a_parts: list[str] = re.findall(r"\d+", lines[0])
Ax, Ay = int(a_parts[0]), int(a_parts[1])
b_parts: list[str] = re.findall(r"\d+", lines[1])
Bx, By = int(b_parts[0]), int(b_parts[1])
prize_parts: list[str] = re.findall(r"\d+", lines[2])
Px, Py = int(prize_parts[0]), int(prize_parts[1])
if part == 2:
Px += 10000000000000
Py += 10000000000000
# Ax * a + Bx * b = Px
# Ay * a + By * b = Py
# Solve for a and b...
# a = (Px - Bx * b) / Ax
# b * (By * Ax - Ay * Bx) = Py * Ax - Ay * Px
b = (Py * Ax - Ay * Px) / (By * Ax - Ay * Bx)
a = (Px - Bx * b) / Ax
if b % 1 == 0 and a % 1 == 0:
ans += 3 * a + b
return int(ans)
# A 3, B 1
# moves right along X, forward along Y
example = """Button A: X+94, Y+34
Button B: X+22, Y+67
Prize: X=8400, Y=5400
Button A: X+26, Y+66
Button B: X+67, Y+21
Prize: X=12748, Y=12176
Button A: X+17, Y+86
Button B: X+84, Y+37
Prize: X=7870, Y=6450
Button A: X+69, Y+23
Button B: X+27, Y+71
Prize: X=18641, Y=10279"""
input = open("input.txt").read().strip()
print(f"part1 example: {part1(example, 1)} want 480")
print(f"part1: {part1(input, 1)} want 28059")
# don't think this result was given in the prompt
print(f"part1 example: {part1(example, 2)} want 875318608908")
print(f"part1: {part1(input, 2)} want 102255878088512")
+108
View File
@@ -0,0 +1,108 @@
import re
def part1(input: str, wide: int, tall: int, seconds: int) -> int:
# don't need to model this exactly right? just equate a line, mod by grid size...
# i guess for 100 steps it would've been better to just model it...
quad_counts: list[int] = [0] * 4
for line in input.splitlines():
nums = re.findall(r"-?\d+", line)
assert len(nums) == 4
x = int(nums[0]) + int(nums[2]) * seconds
y = int(nums[1]) + int(nums[3]) * seconds
x %= tall
y %= wide
if x < tall // 2 and y < wide // 2:
# print("top left")
quad_counts[0] += 1
elif x < tall // 2 and y > wide // 2:
# print("top right")
quad_counts[1] += 1
elif x > tall // 2 and y < wide // 2:
# print("bottom left")
quad_counts[2] += 1
elif x > tall // 2 and y > wide // 2:
# print("bottom right")
quad_counts[3] += 1
# else:
# print("on mid line")
# multiply robot count in each quadrant
# does not include robots on mid-lines
return quad_counts[0] * quad_counts[1] * quad_counts[2] * quad_counts[3]
def part2(input: str, wide: int, tall: int) -> int:
# i guess for 100 steps it would've been better to just model it...
# then i could have reused it for part 2...
robots: list[list[int]] = []
for line in input.splitlines():
nums = re.findall(r"-?\d+", line)
assert len(nums) == 4
robots.append([int(x) for x in nums])
for s in range(10_000):
neighbors: set[tuple[int, int]] = set()
matched: set[tuple[int, int]] = set()
for robot in robots:
robot[0] += robot[2]
robot[1] += robot[3]
robot[0] %= tall
robot[1] %= wide
# track how many neighboring robots have been placed so far
# when the number is high enough we probably have some image of a tree
coord = (robot[0], robot[1])
if coord in neighbors:
matched.add(coord)
for dx in [-1, 0, 1]:
for dy in [-1, 0, 1]:
neighbors.add((coord[0] + dx, coord[1] + dy))
if len(matched) >= 250:
print_grid(robots, wide, tall)
# print(len(matched))
return s + 1
raise Exception("should return from loop")
def print_grid(robots: list[list[int]], wide: int, tall: int):
grid: list[list[str]] = []
for _ in range(0, tall):
grid.append([" "] * wide)
for robot in robots:
grid[robot[0]][robot[1]] = "X"
for line in grid:
print("".join(line))
example = """p=0,4 v=3,-3
p=6,3 v=-1,-3
p=10,3 v=-1,2
p=2,0 v=2,-1
p=0,0 v=1,3
p=3,0 v=-2,-2
p=7,6 v=-1,-3
p=3,0 v=-1,-2
p=9,3 v=2,3
p=7,3 v=-1,2
p=2,4 v=2,-3
p=9,5 v=-3,-3"""
input = open("input.txt").read().strip()
print(f"part1 example: {part1(example, 7, 11, 100)} want 12")
print(f"part1 example: {part1(input, 103, 101, 100)} want 218965032")
print(f"part2: {part2(input, 103, 101)} want 7037")
+214
View File
@@ -0,0 +1,214 @@
diff_map: dict[str, tuple[int, int]] = {
"^": (-1, 0),
"v": (1, 0),
"<": (0, -1),
">": (0, 1),
}
def part1(input: str) -> int:
input_parts = input.split("\n\n")
grid = [list(line) for line in input_parts[0].splitlines()]
row: int = 0
col: int = 0
for r in range(len(grid)):
for c in range(len(grid[0])):
if grid[r][c] == "@":
row = r
col = c
instructions = "".join(input_parts[1].splitlines())
for inst in instructions:
diff = diff_map[inst]
next_row, next_col = row + diff[0], col + diff[1]
match grid[next_row][next_col]:
case "#":
# blocked
continue
case ".":
grid[row][col] = "."
grid[next_row][next_col] = "@"
row, col = next_row, next_col
case "O":
# attempt push, keep moving in direction of diff until a . or # is hit...
not_obstacle_row, not_obstacle_col = next_row, next_col
while grid[not_obstacle_row][not_obstacle_col] == "O":
not_obstacle_row += diff[0]
not_obstacle_col += diff[1]
# if it's a wall "#", nothing moves, so only check for empty spaces "."
if grid[not_obstacle_row][not_obstacle_col] == ".":
grid[not_obstacle_row][not_obstacle_col] = "O"
grid[next_row][next_col] = "@"
grid[row][col] = "."
row, col = next_row, next_col
case _:
raise Exception("unhandled grid type: ", grid[next_row][next_col])
# print("\n".join(["".join(row) for row in grid]))
# 100 times its distance from the top edge of the map plus its distance from the left edge of the map
# 0-indexed so same as indices in 2D array
gps_sum: int = 0
for r in range(len(grid)):
for c in range(len(grid[0])):
if grid[r][c] == "O":
gps_sum += r * 100 + c
return gps_sum
def part2(input: str) -> int:
input_parts = input.split("\n\n")
input_parts[0] = input_parts[0].replace("O", "[]")
input_parts[0] = input_parts[0].replace(".", "..")
input_parts[0] = input_parts[0].replace("#", "##")
input_parts[0] = input_parts[0].replace("@", "@.")
grid = [list(line) for line in input_parts[0].splitlines()]
row: int = 0
col: int = 0
for r in range(len(grid)):
for c in range(len(grid[0])):
if grid[r][c] == "@":
row = r
col = c
# print("\n".join(["".join(row) for row in grid]))
instructions = "".join(input_parts[1].splitlines())
for inst in instructions:
# first see what's in the next space we want to move into...
diff = diff_map[inst]
next_row, next_col = row + diff[0], col + diff[1]
match grid[next_row][next_col]:
case "#":
continue
case ".":
grid[row][col] = "."
row += diff[0]
col += diff[1]
grid[row][col] = "@"
case "[" | "]":
# handle left and right separately because they only push one row of boxes
# at this point row == next_row
if inst in "<>":
# copy-pasta from part 1
not_obstacle_col = next_col
while grid[next_row][not_obstacle_col] in "[]":
not_obstacle_col += diff[1]
# if it's a wall "#", nothing moves, so only check for empty spaces "."
# move everything over towards the diff direction
if grid[next_row][not_obstacle_col] == ".":
for c in range(not_obstacle_col, col, -1 * diff[1]):
grid[row][c] = grid[row][c - diff[1]]
# move robot..
grid[next_row][next_col] = "@"
grid[row][col] = "."
row, col = next_row, next_col
else:
# push boxes up or down, use a stack to maintain the reverse order of boxes that
# MAY get moved
coords_to_check: list[tuple[int, int]] = [
(row, col),
]
to_check_index: int = 0
is_blocked: bool = False
while to_check_index < len(coords_to_check):
front = coords_to_check[to_check_index]
to_check_index += 1
# variable masking is no bueno, but i guess it's fine here
next_row, next_col = front[0] + diff[0], front[1] + diff[1]
next_value = grid[next_row][next_col]
if next_value == "#":
is_blocked = True
break
if next_value == "[":
coords_to_check.append((next_row, next_col))
coords_to_check.append((next_row, next_col + 1))
if next_value == "]":
coords_to_check.append((next_row, next_col))
coords_to_check.append((next_row, next_col - 1))
# if "." then do nothing... no need to check
# move everything towards diff if nothing was blocked
# in reverse order to avoid overwrites
if not is_blocked:
# didn't prevent duplicate adds in the "checking" stage, so just no-op them here
moved_set: set[tuple[int, int]] = set()
for coord in reversed(coords_to_check):
if coord in moved_set:
continue
moved_set.add(coord)
next_row, next_col = coord[0] + diff[0], coord[1] + diff[1]
grid[next_row][next_col], grid[coord[0]][coord[1]] = (
grid[coord[0]][coord[1]],
grid[next_row][next_col],
)
row, col = next_row, next_col
case _:
raise Exception("unexpected grid value: ", grid[next_row][next_col])
# print("\n".join(["".join(row) for row in grid]))
gps_sum: int = 0
for r in range(len(grid)):
for c in range(len(grid[0])):
if grid[r][c] == "[":
gps_sum += r * 100 + c
return gps_sum
small_example = """########
#..O.O.#
##@.O..#
#...O..#
#.#.O..#
#...O..#
#......#
########
<^^>>>vv<v>>v<<"""
example = """##########
#..O..O.O#
#......O.#
#.OO..O.O#
#..O@..O.#
#O#..O...#
#O..O..O.#
#.OO.O.OO#
#....O...#
##########
<vv>^<v^>v>^vv^v>v<>v^v<v<^vv<<<^><<><>>v<vvv<>^v^>^<<<><<v<<<v^vv^v>^
vvv<<^>^v^^><<>>><>^<<><^vv^^<>vvv<>><^^v>^>vv<>v<<<<v<^v>^<^^>>>^<v<v
><>vv>v^v^<>><>>>><^^>vv>v<^^^>>v^v^<^^>v^^>v^<^v>v<>>v^v^<v>v^^<^^vv<
<<v<^>>^^^^>>>v^<>vvv^><v<<<>^^^vv^<vvv>^>v<^^^^v<>^>vvvv><>>v^<<^^^^^
^><^><>>><>^^<<^^v>>><^<v>^<vv>>v>>>^v><>^v><<<<v>>v<v<v>vvv>^<><<>^><
^>><>^v<><^vvv<^^<><v<<<<<><^v<<<><<<^^<v<^^^><^>>^<v^><<<^>>^v<v^v<v^
>^>>^v>vv>^<<^v<>><<><<v<<v><>v<^vv<<<>^^v^>^^>>><<^v>>v^v><^^>>^<>vv^
<><^^>^^^<><vvvvv^v<v<<>^v<v>v<<^><<><<><<<^^<<<^<<>><<><^^^>^^<>^>v<>
^^>vv<^v^v<vv>^<><v<^v>^^^>>>^^vvv^>vvv<>>>^<^>>>>>^<<^v>^vvv<>^<><<v>
v^^>>><<^^<>>^v^<v^vv<>v^<<>^<^v^v><^<<<><<^<v><v<>vv>>v><v^<vv<>v^<<^"""
input = open("input.txt").read().strip()
print(f"part1 small_example: {part1(small_example)} want 2028")
print(f"part1 example: {part1(example)} want 10092")
print(f"part1: {part1(input)} want 1413675")
print(f"part2 example: {part2(example)} want 9021")
print(f"part2: {part2(input)} want 1399772")
+135
View File
@@ -0,0 +1,135 @@
import heapq
diffs: list[tuple[int, int]] = [
(0, 1), # start at index 0 facing "east"/right
(-1, 0), # up
(0, -1), # left
(1, 0), # down
]
def day16(input: str, part: int) -> int:
grid = [list(line) for line in input.splitlines()]
start_row: int = 0
start_col: int = 0
for r in range(len(grid)):
for c in range(len(grid[0])):
if grid[r][c] == "S":
start_row = r
start_col = c
# i think i'd rather write a struct in go... these tuples are easy to make
# but annoying to maintain in my head...
# score, row, col, dir_index
node: tuple[int, int, int, int, set[tuple[int, int]]] = (
int(0),
start_row,
start_col,
int(0),
set(),
)
min_heap = [node]
heapq.heapify(min_heap)
coord_to_min_score: dict[tuple[int, int, int], int] = {}
best_score: int = 1_000_000 # big enough to not interfere with actual input answer
final_path_coords: set[tuple[int, int]] = set()
while len(min_heap) > 0:
popped = heapq.heappop(min_heap)
# part2 exit once best_score is set down to the actual min value
if popped[0] > best_score:
continue
# row, col, dir_index
coord = (popped[1], popped[2], popped[3])
if coord in coord_to_min_score:
prev_score = coord_to_min_score[coord]
# if previous score at this coord and direction is better then continue
if popped[0] > prev_score:
continue
# update best score to reach this coord
coord_to_min_score[coord] = popped[0]
# end reached
if grid[popped[1]][popped[2]] == "E":
best_score = popped[0]
final_path_coords |= popped[4]
final_path_coords.add((popped[1], popped[2]))
continue
# in same direction
diff = diffs[popped[3]]
next = (
popped[0] + 1,
popped[1] + diff[0],
popped[2] + diff[1],
popped[3],
popped[4] | {(popped[1], popped[2])},
)
if grid[next[1]][next[2]] in ".E":
heapq.heappush(min_heap, next)
# 90 deg turns
heapq.heappush(
min_heap,
(popped[0] + 1000, popped[1], popped[2], (popped[3] + 1) % 4, popped[4]),
)
heapq.heappush(
min_heap,
(popped[0] + 1000, popped[1], popped[2], (popped[3] - 1) % 4, popped[4]),
)
if part == 1:
return best_score
return len(final_path_coords)
example = """###############
#.......#....E#
#.#.###.#.###.#
#.....#.#...#.#
#.###.#####.#.#
#.#.#.......#.#
#.#.#####.###.#
#...........#.#
###.#.#####.#.#
#...#.....#.#.#
#.#.#.###.#.#.#
#.....#...#.#.#
#.###.#.#.#.#.#
#S..#.....#...#
###############"""
example2 = """#################
#...#...#...#..E#
#.#.#.#.#.#.#.#.#
#.#.#.#...#...#.#
#.#.#.#.###.#.#.#
#...#.#.#.....#.#
#.#.#.#.#.#####.#
#.#...#.#.#.....#
#.#.#####.#.###.#
#.#.#.......#...#
#.#.###.#####.###
#.#.#...#.....#.#
#.#.#.#####.###.#
#.#.#.........#.#
#.#.#.#########.#
#S#.............#
#################"""
input = open("input.txt").read().strip()
print(f"day16 example: {day16(example, 1)} want 7036")
print(f"day16 example2: {day16(example2,1)} want 11048")
print(f"day16 actual: {day16(input,1)} want 83444")
print(f"part2 example: {day16(example,2)} want 45")
print(f"part2 example2: {day16(example2,2)} want 64")
print(f"part2 actual: {day16(input,2)} want 483")
+131
View File
@@ -0,0 +1,131 @@
import re
def part1(input: str) -> str:
matches = re.findall(r"\d+", input)
reg_a: int = int(matches[0])
reg_b: int = int(matches[1])
reg_c: int = int(matches[2])
program: list[int] = [int(x) for x in matches[3:]]
out = run(program, reg_a, reg_b, reg_c)
return ",".join([str(v) for v in out])
def run(program: list[int], reg_a: int, reg_b: int, reg_c: int) -> list[int]:
def get_combo_operand(combo: int) -> int:
if 0 <= combo <= 3:
return combo
if combo == 4:
return reg_a
if combo == 5:
return reg_b
if combo == 6:
return reg_c
raise Exception("unexpected combo value: ", combo)
i: int = 0
out: list[int] = []
while i < len(program):
opcode = program[i]
operand = program[i + 1]
match opcode:
case 0:
reg_a = reg_a // (2 ** get_combo_operand(operand))
i += 2
case 1:
reg_b = reg_b ^ operand
i += 2
case 2:
reg_b = get_combo_operand(operand) % 8
i += 2
case 3:
if reg_a != 0:
i = operand
else:
i += 2
case 4:
reg_b = reg_b ^ reg_c
i += 2
case 5:
out.append(get_combo_operand(operand) % 8)
i += 2
case 6:
reg_b = reg_a // (2 ** get_combo_operand(operand))
i += 2
case 7:
reg_c = reg_a // (2 ** get_combo_operand(operand))
i += 2
case _:
raise Exception("unhandled opcode", opcode)
return out
# only works on actual input
def part2() -> int:
matches = re.findall(r"\d+", input)
program: list[int] = [int(x) for x in matches[3:]]
# generate each element of the program one at a time, starting at the end
output = str
reg_as: list[int] = []
for i in range(1, 8):
output = run_optimized(i)
if output == program[-len(output) :]:
reg_as.append(i)
digit_count = 1
while digit_count < 16:
next_reg_As: list[int] = []
for a in reg_as:
a *= 8
for i in range(8):
output = run_optimized(a + i)
if output == program[-len(output) :]:
next_reg_As.append(a + i)
reg_as = next_reg_As
digit_count += 1
# first reg_as will be smallest
return reg_as[0]
def run_optimized(a: int) -> list[int]:
b: int = 0
output: list[int] = []
while a != 0:
# 2,4, 1,1, 7,5, 4,0, 0,3, 1,6, 5,5, 3,0
# pen and paper "algebra"
b = ((a % 8) ^ 1) ^ (a // (2 ** ((a % 8) ^ 1))) ^ 6
output.append(b % 8)
a = a // 8
return output
example = """Register A: 729
Register B: 0
Register C: 0
Program: 0,1,5,4,3,0"""
input = open("input.txt").read().strip()
print(f"part1 example: {part1(example)} want '4,6,3,5,6,3,5,2,1,0'")
print(f"part1 actual: {part1(input)} want '1,6,3,6,5,6,5,1,7'")
print(
f"optimized {",".join([str(x) for x in run_optimized(30899381)])} want {part1(input)}"
)
example_part2 = """Register A: 2024
Register B: 0
Register C: 0
Program: 0,3,5,4,3,0"""
# print(f"part2 example: {part1(example_part2, 2)} want 117440")
print(f"part2 actual: {part2()} want 247839653009594")