Poetry stuff.
This commit is contained in:
0
src/holt59/aoc/2023/__init__.py
Normal file
0
src/holt59/aoc/2023/__init__.py
Normal file
45
src/holt59/aoc/2023/day1.py
Normal file
45
src/holt59/aoc/2023/day1.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import sys
|
||||
|
||||
lines = sys.stdin.read().splitlines()
|
||||
|
||||
lookups_1 = {str(d): d for d in range(1, 10)}
|
||||
lookups_2 = lookups_1 | {
|
||||
d: i + 1
|
||||
for i, d in enumerate(
|
||||
(
|
||||
"one",
|
||||
"two",
|
||||
"three",
|
||||
"four",
|
||||
"five",
|
||||
"six",
|
||||
"seven",
|
||||
"eight",
|
||||
"nine",
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def find_values(lookups: dict[str, int]) -> list[int]:
|
||||
values: list[int] = []
|
||||
|
||||
for line in filter(bool, lines):
|
||||
first_digit = min(
|
||||
lookups,
|
||||
key=lambda lookup: index
|
||||
if (index := line.find(lookup)) >= 0
|
||||
else len(line),
|
||||
)
|
||||
last_digit = max(
|
||||
lookups,
|
||||
key=lambda lookup: index if (index := line.rfind(lookup)) >= 0 else -1,
|
||||
)
|
||||
|
||||
values.append(10 * lookups[first_digit] + lookups[last_digit])
|
||||
|
||||
return values
|
||||
|
||||
|
||||
print(f"answer 1 is {sum(find_values(lookups_1))}")
|
||||
print(f"answer 2 is {sum(find_values(lookups_2))}")
|
100
src/holt59/aoc/2023/day10.py
Normal file
100
src/holt59/aoc/2023/day10.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import os
|
||||
import sys
|
||||
from typing import Literal, cast
|
||||
|
||||
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
|
||||
|
||||
Symbol = Literal["|", "-", "L", "J", "7", "F", ".", "S"]
|
||||
|
||||
lines: list[list[Symbol]] = [
|
||||
[cast(Symbol, symbol) for symbol in line] for line in sys.stdin.read().splitlines()
|
||||
]
|
||||
|
||||
# find starting point
|
||||
si, sj = next(
|
||||
(i, j)
|
||||
for i in range(len(lines))
|
||||
for j in range(len(lines[0]))
|
||||
if lines[i][j] == "S"
|
||||
)
|
||||
|
||||
# find one of the two outputs
|
||||
ni, nj = si, sj
|
||||
for ni, nj, chars in (
|
||||
(si - 1, sj, "|7F"),
|
||||
(si + 1, sj, "|LJ"),
|
||||
(si, sj - 1, "-LF"),
|
||||
(si, sj + 1, "-J7"),
|
||||
):
|
||||
if lines[ni][nj] in chars:
|
||||
break
|
||||
|
||||
# part 1 - find the loop (re-used in part 2)
|
||||
loop = [(si, sj), (ni, nj)]
|
||||
while True:
|
||||
pi, pj = loop[-2]
|
||||
i, j = loop[-1]
|
||||
|
||||
sym = lines[i][j]
|
||||
|
||||
if sym == "|" and pi > i or sym in "JL" and pi == i:
|
||||
i -= 1
|
||||
elif sym == "|" and pi < i or sym in "7F" and pi == i:
|
||||
i += 1
|
||||
elif sym == "-" and pj > j or sym in "J7" and pj == j:
|
||||
j -= 1
|
||||
elif sym == "-" and pj < j or sym in "LF" and pj == j:
|
||||
j += 1
|
||||
|
||||
if (i, j) == (si, sj):
|
||||
break
|
||||
|
||||
loop.append((i, j))
|
||||
|
||||
answer_1 = len(loop) // 2
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
# part 2
|
||||
|
||||
# replace S by an appropriate character for the loop below
|
||||
di1, dj1 = loop[1][0] - loop[0][0], loop[1][1] - loop[0][1]
|
||||
di2, dj2 = loop[0][0] - loop[-1][0], loop[0][1] - loop[-1][1]
|
||||
mapping: dict[tuple[int, int], dict[tuple[int, int], Symbol]] = {
|
||||
(0, 1): {(0, 1): "-", (-1, 0): "F", (1, 0): "L"},
|
||||
(0, -1): {(0, -1): "-", (-1, 0): "7", (1, 0): "J"},
|
||||
(1, 0): {(1, 0): "|", (0, 1): "7", (0, -1): "F"},
|
||||
(-1, 0): {(-1, 0): "|", (0, -1): "L", (0, 1): "J"},
|
||||
}
|
||||
lines[si][sj] = mapping[di1, dj1][di2, dj2]
|
||||
|
||||
# find the points inside the loop using an adaptation of ray casting for a discrete
|
||||
# grid (https://stackoverflow.com/a/218081/2666289)
|
||||
#
|
||||
# use a set for faster '... in loop' check
|
||||
#
|
||||
loop_s = set(loop)
|
||||
inside: set[tuple[int, int]] = set()
|
||||
for i in range(len(lines)):
|
||||
cnt = 0
|
||||
for j in range(len(lines[0])):
|
||||
if (i, j) not in loop_s and cnt % 2 == 1:
|
||||
inside.add((i, j))
|
||||
|
||||
if (i, j) in loop_s and lines[i][j] in "|LJ":
|
||||
cnt += 1
|
||||
|
||||
if VERBOSE:
|
||||
for i in range(len(lines)):
|
||||
for j in range(len(lines[0])):
|
||||
if (i, j) == (si, sj):
|
||||
print("\033[91mS\033[0m", end="")
|
||||
elif (i, j) in loop:
|
||||
print(lines[i][j], end="")
|
||||
elif (i, j) in inside:
|
||||
print("\033[92mI\033[0m", end="")
|
||||
else:
|
||||
print(".", end="")
|
||||
print()
|
||||
|
||||
answer_2 = len(inside)
|
||||
print(f"answer 2 is {answer_2}")
|
41
src/holt59/aoc/2023/day11.py
Normal file
41
src/holt59/aoc/2023/day11.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
lines = sys.stdin.read().splitlines()
|
||||
|
||||
data = np.array([[c == "#" for c in line] for line in lines])
|
||||
|
||||
rows = {c for c in range(data.shape[0]) if not data[c, :].any()}
|
||||
columns = {c for c in range(data.shape[1]) if not data[:, c].any()}
|
||||
|
||||
galaxies_y, galaxies_x = np.where(data) # type: ignore
|
||||
|
||||
|
||||
def compute_total_distance(expansion: int) -> int:
|
||||
distances: list[int] = []
|
||||
for g1 in range(len(galaxies_y)):
|
||||
x1, y1 = int(galaxies_x[g1]), int(galaxies_y[g1])
|
||||
for g2 in range(g1 + 1, len(galaxies_y)):
|
||||
x2, y2 = int(galaxies_x[g2]), int(galaxies_y[g2])
|
||||
|
||||
dx = sum(
|
||||
1 + (expansion - 1) * (x in columns)
|
||||
for x in range(min(x1, x2), max(x1, x2))
|
||||
)
|
||||
dy = sum(
|
||||
1 + (expansion - 1) * (y in rows)
|
||||
for y in range(min(y1, y2), max(y1, y2))
|
||||
)
|
||||
|
||||
distances.append(dx + dy)
|
||||
return sum(distances)
|
||||
|
||||
|
||||
# part 1
|
||||
answer_1 = compute_total_distance(2)
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
# part 2
|
||||
answer_2 = compute_total_distance(1000000)
|
||||
print(f"answer 2 is {answer_2}")
|
107
src/holt59/aoc/2023/day12.py
Normal file
107
src/holt59/aoc/2023/day12.py
Normal file
@@ -0,0 +1,107 @@
|
||||
import os
|
||||
import sys
|
||||
from functools import lru_cache
|
||||
from typing import Iterable
|
||||
|
||||
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
|
||||
|
||||
|
||||
@lru_cache
|
||||
def compute_fitting_arrangements(pattern: str, counts: tuple[int, ...]) -> int:
|
||||
"""
|
||||
fn3p tries to fit ALL values in counts() inside the pattern.
|
||||
"""
|
||||
# no pattern -> ok if nothing to fit, otherwise ko
|
||||
if not pattern:
|
||||
count = 1 if not counts else 0
|
||||
|
||||
# no count -> ok if pattern has no mandatory entry, else ko
|
||||
elif not counts:
|
||||
count = 1 if pattern.find("#") == -1 else 0
|
||||
|
||||
# cannot fit all values -> ko
|
||||
elif len(pattern) < sum(counts) + len(counts) - 1:
|
||||
count = 0
|
||||
|
||||
elif len(pattern) < counts[0]:
|
||||
count = 0
|
||||
|
||||
else:
|
||||
count = 0
|
||||
|
||||
if pattern[0] == "?":
|
||||
count += compute_fitting_arrangements(pattern[1:], counts)
|
||||
|
||||
if len(pattern) == counts[0]:
|
||||
count += 1
|
||||
|
||||
elif pattern[counts[0]] != "#":
|
||||
count += compute_fitting_arrangements(pattern[counts[0] + 1 :], counts[1:])
|
||||
|
||||
return count
|
||||
|
||||
|
||||
@lru_cache
|
||||
def compute_possible_arrangements(
|
||||
patterns: tuple[str, ...], counts: tuple[int, ...]
|
||||
) -> int:
|
||||
if not patterns:
|
||||
return 1 if not counts else 0
|
||||
|
||||
with_hash = sum(1 for p in patterns[1:] if p.find("#") >= 0)
|
||||
|
||||
if with_hash > len(counts):
|
||||
return 0
|
||||
|
||||
to_fit = counts if with_hash == 0 else counts[:-with_hash]
|
||||
remaining = () if with_hash == 0 else counts[-with_hash:]
|
||||
|
||||
if not to_fit:
|
||||
if patterns[0].find("#") != -1:
|
||||
return 0
|
||||
return compute_possible_arrangements(patterns[1:], remaining)
|
||||
|
||||
elif patterns[0].find("#") != -1 and len(patterns[0]) < to_fit[0]:
|
||||
return 0
|
||||
|
||||
elif patterns[0].find("?") == -1:
|
||||
if len(patterns[0]) != to_fit[0]:
|
||||
return 0
|
||||
return compute_possible_arrangements(patterns[1:], counts[1:])
|
||||
|
||||
else:
|
||||
return sum(
|
||||
fp * compute_possible_arrangements(patterns[1:], to_fit[i:] + remaining)
|
||||
for i in range(len(to_fit) + 1)
|
||||
if (fp := compute_fitting_arrangements(patterns[0], to_fit[:i])) > 0
|
||||
)
|
||||
|
||||
|
||||
def compute_all_possible_arrangements(lines: Iterable[str], repeat: int) -> int:
|
||||
count = 0
|
||||
|
||||
if VERBOSE:
|
||||
from tqdm import tqdm
|
||||
|
||||
lines = tqdm(lines)
|
||||
|
||||
for line in lines:
|
||||
parts = line.split(" ")
|
||||
count += compute_possible_arrangements(
|
||||
tuple(filter(len, "?".join(parts[0] for _ in range(repeat)).split("."))),
|
||||
tuple(int(c) for c in parts[1].split(",")) * repeat,
|
||||
)
|
||||
|
||||
return count
|
||||
|
||||
|
||||
lines = sys.stdin.read().splitlines()
|
||||
|
||||
|
||||
# part 1
|
||||
answer_1 = compute_all_possible_arrangements(lines, 1)
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
# part 2
|
||||
answer_2 = compute_all_possible_arrangements(lines, 5)
|
||||
print(f"answer 2 is {answer_2}")
|
43
src/holt59/aoc/2023/day13.py
Normal file
43
src/holt59/aoc/2023/day13.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import sys
|
||||
from typing import Callable, Literal
|
||||
|
||||
|
||||
def split(block: list[str], axis: Literal[0, 1], count: int) -> int:
|
||||
n_iter = len(block) if axis == 0 else len(block[0])
|
||||
n_check = len(block) if axis == 1 else len(block[0])
|
||||
|
||||
at: Callable[[int, int], str] = (
|
||||
(lambda i, j: block[i][j]) if axis == 0 else (lambda i, j: block[j][i])
|
||||
)
|
||||
|
||||
for i in range(n_iter - 1):
|
||||
size = min(i + 1, n_iter - i - 1)
|
||||
if (
|
||||
sum(
|
||||
at(i - s, j) != at(i + 1 + s, j)
|
||||
for s in range(0, size)
|
||||
for j in range(n_check)
|
||||
)
|
||||
== count
|
||||
):
|
||||
return i + 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
blocks = [block.splitlines() for block in sys.stdin.read().split("\n\n")]
|
||||
|
||||
|
||||
# part 1
|
||||
answer_1 = sum(
|
||||
split(block, axis=1, count=0) + 100 * split(block, axis=0, count=0)
|
||||
for block in blocks
|
||||
)
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
# part 2
|
||||
answer_2 = sum(
|
||||
split(block, axis=1, count=1) + 100 * split(block, axis=0, count=1)
|
||||
for block in blocks
|
||||
)
|
||||
print(f"answer 2 is {answer_2}")
|
68
src/holt59/aoc/2023/day14.py
Normal file
68
src/holt59/aoc/2023/day14.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import sys
|
||||
from typing import TypeAlias
|
||||
|
||||
RockGrid: TypeAlias = list[list[str]]
|
||||
|
||||
rocks0 = [list(line) for line in sys.stdin.read().splitlines()]
|
||||
|
||||
|
||||
def slide_rocks_top(rocks: RockGrid) -> RockGrid:
|
||||
top = [0 if c == "." else 1 for c in rocks[0]]
|
||||
for row in range(1, len(rocks)):
|
||||
for col in range(len(rocks[0])):
|
||||
match rocks[row][col]:
|
||||
case "O":
|
||||
if top[col] != row:
|
||||
rocks[top[col]][col] = "O"
|
||||
rocks[row][col] = "."
|
||||
top[col] = top[col] + 1
|
||||
case "#":
|
||||
top[col] = row + 1
|
||||
case _:
|
||||
pass
|
||||
return rocks
|
||||
|
||||
|
||||
def cycle(rocks: RockGrid) -> RockGrid:
|
||||
for _ in range(4):
|
||||
rocks = slide_rocks_top(rocks)
|
||||
rocks = [
|
||||
[rocks[len(rocks) - j - 1][i] for j in range(len(rocks))]
|
||||
for i in range(len(rocks[0]))
|
||||
]
|
||||
|
||||
return rocks
|
||||
|
||||
|
||||
rocks = slide_rocks_top([[c for c in r] for r in rocks0])
|
||||
|
||||
# part 1
|
||||
answer_1 = sum(
|
||||
(len(rocks) - i) * sum(1 for c in row if c == "O") for i, row in enumerate(rocks)
|
||||
)
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
# part 2
|
||||
rocks = rocks0
|
||||
|
||||
N = 1000000000
|
||||
cycles: list[RockGrid] = []
|
||||
i_cycle: int = -1
|
||||
for i_cycle in range(N):
|
||||
rocks = cycle(rocks)
|
||||
|
||||
if any(rocks == c for c in cycles):
|
||||
break
|
||||
|
||||
cycles.append([[c for c in r] for r in rocks])
|
||||
|
||||
cycle_start = next(i for i in range(len(cycles)) if (rocks == cycles[i]))
|
||||
cycle_length = i_cycle - cycle_start
|
||||
|
||||
ci = cycle_start + (N - cycle_start) % cycle_length - 1
|
||||
|
||||
answer_2 = sum(
|
||||
(len(rocks) - i) * sum(1 for c in row if c == "O")
|
||||
for i, row in enumerate(cycles[ci])
|
||||
)
|
||||
print(f"answer 2 is {answer_2}")
|
31
src/holt59/aoc/2023/day15.py
Normal file
31
src/holt59/aoc/2023/day15.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import sys
|
||||
from functools import reduce
|
||||
|
||||
steps = sys.stdin.read().strip().split(",")
|
||||
|
||||
|
||||
def _hash(s: str) -> int:
|
||||
return reduce(lambda v, u: ((v + ord(u)) * 17) % 256, s, 0)
|
||||
|
||||
|
||||
# part 1
|
||||
answer_1 = sum(map(_hash, steps))
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
# part 2
|
||||
boxes: list[dict[str, int]] = [{} for _ in range(256)]
|
||||
|
||||
for step in steps:
|
||||
if (i := step.find("=")) >= 0:
|
||||
label, length = step[:i], int(step[i + 1 :])
|
||||
boxes[_hash(label)][label] = length
|
||||
else:
|
||||
label = step[:-1]
|
||||
boxes[_hash(label)].pop(label, None)
|
||||
|
||||
answer_2 = sum(
|
||||
i_box * i_lens * length
|
||||
for i_box, box in enumerate(boxes, start=1)
|
||||
for i_lens, length in enumerate(box.values(), start=1)
|
||||
)
|
||||
print(f"answer 2 is {answer_2}")
|
110
src/holt59/aoc/2023/day16.py
Normal file
110
src/holt59/aoc/2023/day16.py
Normal file
@@ -0,0 +1,110 @@
|
||||
import os
|
||||
import sys
|
||||
from typing import Literal, TypeAlias, cast
|
||||
|
||||
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
|
||||
|
||||
CellType: TypeAlias = Literal[".", "|", "-", "\\", "/"]
|
||||
Direction: TypeAlias = Literal["R", "L", "U", "D"]
|
||||
|
||||
Mappings: dict[
|
||||
CellType,
|
||||
dict[
|
||||
Direction,
|
||||
tuple[tuple[tuple[int, int, Direction], ...], tuple[Direction, ...]],
|
||||
],
|
||||
] = {
|
||||
".": {
|
||||
"R": (((0, +1, "R"),), ("R", "L")),
|
||||
"L": (((0, -1, "L"),), ("R", "L")),
|
||||
"U": (((-1, 0, "U"),), ("U", "D")),
|
||||
"D": (((+1, 0, "D"),), ("U", "D")),
|
||||
},
|
||||
"-": {
|
||||
"R": (((0, +1, "R"),), ("R", "L")),
|
||||
"L": (((0, -1, "L"),), ("R", "L")),
|
||||
"U": (((0, +1, "R"), (0, -1, "L")), ("U", "D")),
|
||||
"D": (((0, +1, "R"), (0, -1, "L")), ("U", "D")),
|
||||
},
|
||||
"|": {
|
||||
"U": (((-1, 0, "U"),), ("U", "D")),
|
||||
"D": (((+1, 0, "D"),), ("U", "D")),
|
||||
"R": (((-1, 0, "U"), (+1, 0, "D")), ("R", "L")),
|
||||
"L": (((-1, 0, "U"), (+1, 0, "D")), ("R", "L")),
|
||||
},
|
||||
"/": {
|
||||
"R": (((-1, 0, "U"),), ("R", "D")),
|
||||
"L": (((+1, 0, "D"),), ("L", "U")),
|
||||
"U": (((0, +1, "R"),), ("U", "L")),
|
||||
"D": (((0, -1, "L"),), ("R", "D")),
|
||||
},
|
||||
"\\": {
|
||||
"R": (((+1, 0, "D"),), ("R", "U")),
|
||||
"L": (((-1, 0, "U"),), ("L", "D")),
|
||||
"U": (((0, -1, "L"),), ("U", "R")),
|
||||
"D": (((0, +1, "R"),), ("L", "D")),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def propagate(
|
||||
layout: list[list[CellType]], start: tuple[int, int], direction: Direction
|
||||
) -> list[list[tuple[Direction, ...]]]:
|
||||
n_rows, n_cols = len(layout), len(layout[0])
|
||||
|
||||
beams: list[list[tuple[Direction, ...]]] = [
|
||||
[() for _ in range(len(layout[0]))] for _ in range(len(layout))
|
||||
]
|
||||
|
||||
queue = [(start, direction)]
|
||||
|
||||
while queue:
|
||||
(row, col), direction = queue.pop()
|
||||
|
||||
if (
|
||||
row not in range(0, n_rows)
|
||||
or col not in range(0, n_cols)
|
||||
or direction in beams[row][col]
|
||||
):
|
||||
continue
|
||||
|
||||
moves, update = Mappings[layout[row][col]][direction]
|
||||
|
||||
beams[row][col] += update
|
||||
|
||||
for move in moves:
|
||||
queue.append(((row + move[0], col + move[1]), move[2]))
|
||||
|
||||
return beams
|
||||
|
||||
|
||||
layout: list[list[CellType]] = [
|
||||
[cast(CellType, col) for col in row] for row in sys.stdin.read().splitlines()
|
||||
]
|
||||
|
||||
|
||||
beams = propagate(layout, (0, 0), "R")
|
||||
|
||||
if VERBOSE:
|
||||
print("\n".join(["".join("#" if col else "." for col in row) for row in beams]))
|
||||
|
||||
# part 1
|
||||
answer_1 = sum(sum(map(bool, row)) for row in beams)
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
# part 2
|
||||
n_rows, n_cols = len(layout), len(layout[0])
|
||||
cases: list[tuple[tuple[int, int], Direction]] = []
|
||||
|
||||
for row in range(n_rows):
|
||||
cases.append(((row, 0), "R"))
|
||||
cases.append(((row, n_cols - 1), "L"))
|
||||
for col in range(n_cols):
|
||||
cases.append(((0, col), "D"))
|
||||
cases.append(((n_rows - 1, col), "U"))
|
||||
|
||||
answer_2 = max(
|
||||
sum(sum(map(bool, row)) for row in propagate(layout, start, direction))
|
||||
for start, direction in cases
|
||||
)
|
||||
print(f"answer 2 is {answer_2}")
|
233
src/holt59/aoc/2023/day17.py
Normal file
233
src/holt59/aoc/2023/day17.py
Normal file
@@ -0,0 +1,233 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import heapq
|
||||
import os
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal, TypeAlias
|
||||
|
||||
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
|
||||
|
||||
Direction: TypeAlias = Literal[">", "<", "^", "v"]
|
||||
|
||||
|
||||
@dataclass(frozen=True, order=True)
|
||||
class Label:
|
||||
row: int
|
||||
col: int
|
||||
|
||||
direction: Direction
|
||||
count: int
|
||||
|
||||
parent: Label | None = None
|
||||
|
||||
|
||||
# mappings from direction to row shift / col shift / opposite direction
|
||||
MAPPINGS: dict[Direction, tuple[int, int, Direction]] = {
|
||||
">": (0, +1, "<"),
|
||||
"<": (0, -1, ">"),
|
||||
"v": (+1, 0, "^"),
|
||||
"^": (-1, 0, "v"),
|
||||
}
|
||||
|
||||
|
||||
def print_shortest_path(
|
||||
grid: list[list[int]],
|
||||
target: tuple[int, int],
|
||||
per_cell: dict[tuple[int, int], list[tuple[Label, int]]],
|
||||
):
|
||||
assert len(per_cell[target]) == 1
|
||||
label = per_cell[target][0][0]
|
||||
|
||||
path: list[Label] = []
|
||||
while True:
|
||||
path.insert(0, label)
|
||||
if label.parent is None:
|
||||
break
|
||||
label = label.parent
|
||||
|
||||
p_grid = [[str(c) for c in r] for r in grid]
|
||||
|
||||
for i in range(len(grid)):
|
||||
for j in range(len(grid[0])):
|
||||
if per_cell[i, j]:
|
||||
p_grid[i][j] = f"\033[94m{grid[i][j]}\033[0m"
|
||||
|
||||
prev_label = path[0]
|
||||
for label in path[1:]:
|
||||
for r in range(
|
||||
min(prev_label.row, label.row), max(prev_label.row, label.row) + 1
|
||||
):
|
||||
for c in range(
|
||||
min(prev_label.col, label.col),
|
||||
max(prev_label.col, label.col) + 1,
|
||||
):
|
||||
if (r, c) != (prev_label.row, prev_label.col):
|
||||
p_grid[r][c] = f"\033[93m{grid[r][c]}\033[0m"
|
||||
|
||||
p_grid[label.row][label.col] = f"\033[91m{grid[label.row][label.col]}\033[0m"
|
||||
|
||||
prev_label = label
|
||||
|
||||
p_grid[0][0] = f"\033[92m{grid[0][0]}\033[0m"
|
||||
|
||||
print("\n".join("".join(row) for row in p_grid))
|
||||
|
||||
|
||||
def shortest_many_paths(grid: list[list[int]]) -> dict[tuple[int, int], int]:
|
||||
n_rows, n_cols = len(grid), len(grid[0])
|
||||
|
||||
visited: dict[tuple[int, int], tuple[Label, int]] = {}
|
||||
|
||||
queue: list[tuple[int, Label]] = [
|
||||
(0, Label(row=n_rows - 1, col=n_cols - 1, direction="^", count=0))
|
||||
]
|
||||
|
||||
while queue and len(visited) != n_rows * n_cols:
|
||||
distance, label = heapq.heappop(queue)
|
||||
|
||||
if (label.row, label.col) in visited:
|
||||
continue
|
||||
|
||||
visited[label.row, label.col] = (label, distance)
|
||||
|
||||
for direction, (c_row, c_col, i_direction) in MAPPINGS.items():
|
||||
if label.direction == i_direction:
|
||||
continue
|
||||
else:
|
||||
row, col = (label.row + c_row, label.col + c_col)
|
||||
|
||||
# exclude labels outside the grid or with too many moves in the same
|
||||
# direction
|
||||
if row not in range(0, n_rows) or col not in range(0, n_cols):
|
||||
continue
|
||||
|
||||
heapq.heappush(
|
||||
queue,
|
||||
(
|
||||
distance
|
||||
+ sum(
|
||||
grid[r][c]
|
||||
for r in range(min(row, label.row), max(row, label.row) + 1)
|
||||
for c in range(min(col, label.col), max(col, label.col) + 1)
|
||||
)
|
||||
- grid[row][col],
|
||||
Label(
|
||||
row=row,
|
||||
col=col,
|
||||
direction=direction,
|
||||
count=0,
|
||||
parent=label,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
return {(r, c): visited[r, c][1] for r in range(n_rows) for c in range(n_cols)}
|
||||
|
||||
|
||||
def shortest_path(
|
||||
grid: list[list[int]],
|
||||
min_straight: int,
|
||||
max_straight: int,
|
||||
lower_bounds: dict[tuple[int, int], int],
|
||||
) -> int:
|
||||
n_rows, n_cols = len(grid), len(grid[0])
|
||||
|
||||
target = (len(grid) - 1, len(grid[0]) - 1)
|
||||
|
||||
# for each tuple (row, col, direction, count), the associated label when visited
|
||||
visited: dict[tuple[int, int, str, int], Label] = {}
|
||||
|
||||
# list of all visited labels for a cell (with associated distance)
|
||||
per_cell: dict[tuple[int, int], list[tuple[Label, int]]] = defaultdict(list)
|
||||
|
||||
# need to add two start labels, otherwise one of the two possible direction will
|
||||
# not be possible
|
||||
queue: list[tuple[int, int, Label]] = [
|
||||
(lower_bounds[0, 0], 0, Label(row=0, col=0, direction="^", count=0)),
|
||||
(lower_bounds[0, 0], 0, Label(row=0, col=0, direction="<", count=0)),
|
||||
]
|
||||
|
||||
while queue:
|
||||
_, distance, label = heapq.heappop(queue)
|
||||
|
||||
if (label.row, label.col, label.direction, label.count) in visited:
|
||||
continue
|
||||
|
||||
visited[label.row, label.col, label.direction, label.count] = label
|
||||
per_cell[label.row, label.col].append((label, distance))
|
||||
|
||||
if (label.row, label.col) == target:
|
||||
break
|
||||
|
||||
for direction, (c_row, c_col, i_direction) in MAPPINGS.items():
|
||||
# cannot move in the opposite direction
|
||||
if label.direction == i_direction:
|
||||
continue
|
||||
|
||||
# other direction, move 'min_straight' in the new direction
|
||||
elif label.direction != direction:
|
||||
row, col, count = (
|
||||
label.row + min_straight * c_row,
|
||||
label.col + min_straight * c_col,
|
||||
min_straight,
|
||||
)
|
||||
|
||||
# same direction, too many count
|
||||
elif label.count == max_straight:
|
||||
continue
|
||||
|
||||
# same direction, keep going and increment count
|
||||
else:
|
||||
row, col, count = (
|
||||
label.row + c_row,
|
||||
label.col + c_col,
|
||||
label.count + 1,
|
||||
)
|
||||
# exclude labels outside the grid or with too many moves in the same
|
||||
# direction
|
||||
if row not in range(0, n_rows) or col not in range(0, n_cols):
|
||||
continue
|
||||
|
||||
distance_to = (
|
||||
distance
|
||||
+ sum(
|
||||
grid[r][c]
|
||||
for r in range(min(row, label.row), max(row, label.row) + 1)
|
||||
for c in range(min(col, label.col), max(col, label.col) + 1)
|
||||
)
|
||||
- grid[label.row][label.col]
|
||||
)
|
||||
|
||||
heapq.heappush(
|
||||
queue,
|
||||
(
|
||||
distance_to + lower_bounds[row, col],
|
||||
distance_to,
|
||||
Label(
|
||||
row=row,
|
||||
col=col,
|
||||
direction=direction,
|
||||
count=count,
|
||||
parent=label,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
if VERBOSE:
|
||||
print_shortest_path(grid, target, per_cell)
|
||||
|
||||
return per_cell[target][0][1]
|
||||
|
||||
|
||||
data = [[int(c) for c in r] for r in sys.stdin.read().splitlines()]
|
||||
estimates = shortest_many_paths(data)
|
||||
|
||||
# part 1
|
||||
answer_1 = shortest_path(data, 1, 3, lower_bounds=estimates)
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
# part 2
|
||||
answer_2 = shortest_path(data, 4, 10, lower_bounds=estimates)
|
||||
print(f"answer 2 is {answer_2}")
|
54
src/holt59/aoc/2023/day18.py
Normal file
54
src/holt59/aoc/2023/day18.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import sys
|
||||
from typing import Literal, TypeAlias, cast
|
||||
|
||||
Direction: TypeAlias = Literal["R", "L", "U", "D"]
|
||||
|
||||
DIRECTIONS: list[Direction] = ["R", "D", "L", "U"]
|
||||
MOVES: dict[Direction, tuple[int, int]] = {
|
||||
"R": (0, +1),
|
||||
"L": (0, -1),
|
||||
"U": (-1, 0),
|
||||
"D": (+1, 0),
|
||||
}
|
||||
|
||||
|
||||
def area(corners: list[tuple[int, int]], perimeter: int) -> int:
|
||||
area = abs(
|
||||
sum(c0[0] * c1[1] - c0[1] * c1[0] for c0, c1 in zip(corners, corners[1::])) // 2
|
||||
)
|
||||
return 1 + area - perimeter // 2 + perimeter
|
||||
|
||||
|
||||
def polygon(values: list[tuple[Direction, int]]) -> tuple[list[tuple[int, int]], int]:
|
||||
perimeter = 0
|
||||
corners: list[tuple[int, int]] = [(0, 0)]
|
||||
for direction, amount in values:
|
||||
perimeter += amount
|
||||
corners.append(
|
||||
(
|
||||
corners[-1][0] + amount * MOVES[direction][0],
|
||||
corners[-1][1] + amount * MOVES[direction][1],
|
||||
)
|
||||
)
|
||||
return corners, perimeter
|
||||
|
||||
|
||||
lines = sys.stdin.read().splitlines()
|
||||
|
||||
|
||||
# part 1
|
||||
answer_1 = area(
|
||||
*polygon([(cast(Direction, (p := line.split())[0]), int(p[1])) for line in lines])
|
||||
)
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
# part 2
|
||||
answer_2 = area(
|
||||
*polygon(
|
||||
[
|
||||
(DIRECTIONS[int((h := line.split()[-1])[-2])], int(h[2:-2], 16))
|
||||
for line in lines
|
||||
]
|
||||
)
|
||||
)
|
||||
print(f"answer 2 is {answer_2}")
|
132
src/holt59/aoc/2023/day19.py
Normal file
132
src/holt59/aoc/2023/day19.py
Normal file
@@ -0,0 +1,132 @@
|
||||
import logging
|
||||
import operator
|
||||
import os
|
||||
import sys
|
||||
from math import prod
|
||||
from typing import Literal, TypeAlias, cast
|
||||
|
||||
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
|
||||
|
||||
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
|
||||
|
||||
Category: TypeAlias = Literal["x", "m", "a", "s"]
|
||||
Part: TypeAlias = dict[Category, int]
|
||||
PartWithBounds: TypeAlias = dict[Category, tuple[int, int]]
|
||||
|
||||
OPERATORS = {"<": operator.lt, ">": operator.gt}
|
||||
|
||||
# None if there is no check (last entry), otherwise (category, sense, value)
|
||||
Check: TypeAlias = tuple[Category, Literal["<", ">"], int] | None
|
||||
|
||||
# workflow as a list of check, in specified order, with target
|
||||
Workflow: TypeAlias = list[tuple[Check, str]]
|
||||
|
||||
|
||||
def accept(workflows: dict[str, Workflow], part: Part) -> bool:
|
||||
workflow = "in"
|
||||
decision: bool | None = None
|
||||
|
||||
while decision is None:
|
||||
for check, target in workflows[workflow]:
|
||||
ok = check is None
|
||||
if check is not None:
|
||||
category, sense, value = check
|
||||
ok = OPERATORS[sense](part[category], value)
|
||||
|
||||
if ok:
|
||||
if target in workflows:
|
||||
workflow = target
|
||||
else:
|
||||
decision = target == "A"
|
||||
break
|
||||
|
||||
return decision
|
||||
|
||||
|
||||
def propagate(workflows: dict[str, Workflow], start: PartWithBounds) -> int:
|
||||
def transfer_or_accept(
|
||||
target: str, meta: PartWithBounds, queue: list[tuple[PartWithBounds, str]]
|
||||
) -> int:
|
||||
if target in workflows:
|
||||
logging.info(f" transfer to {target}")
|
||||
queue.append((meta, target))
|
||||
return 0
|
||||
elif target == "A":
|
||||
logging.info(" accepted")
|
||||
return prod((high - low + 1) for low, high in meta.values())
|
||||
else:
|
||||
logging.info(" rejected")
|
||||
return 0
|
||||
|
||||
accepted = 0
|
||||
queue: list[tuple[PartWithBounds, str]] = [(start, "in")]
|
||||
|
||||
while queue:
|
||||
meta, workflow = queue.pop()
|
||||
logging.info(f"{workflow}: {meta}")
|
||||
for check, target in workflows[workflow]:
|
||||
if check is None:
|
||||
accepted += transfer_or_accept(target, meta, queue)
|
||||
continue
|
||||
|
||||
category, sense, value = check
|
||||
bounds, op = meta[category], OPERATORS[sense]
|
||||
|
||||
logging.info(f" splitting {meta} into {category} {sense} {value}")
|
||||
|
||||
if not op(bounds[0], value) and not op(bounds[1], value):
|
||||
logging.info(" reject, always false")
|
||||
continue
|
||||
|
||||
if op(meta[category][0], value) and op(meta[category][1], value):
|
||||
logging.info(" accept, always true")
|
||||
accepted += transfer_or_accept(target, meta, queue)
|
||||
break
|
||||
|
||||
meta2 = meta.copy()
|
||||
if sense == "<":
|
||||
meta2[category] = (meta[category][0], value - 1)
|
||||
meta[category] = (value, meta[category][1])
|
||||
else:
|
||||
meta2[category] = (value + 1, meta[category][1])
|
||||
meta[category] = (meta[category][0], value)
|
||||
logging.info(f" split {meta2} ({target}), {meta}")
|
||||
|
||||
accepted += transfer_or_accept(target, meta2, queue)
|
||||
|
||||
return accepted
|
||||
|
||||
|
||||
workflows_s, parts_s = sys.stdin.read().strip().split("\n\n")
|
||||
|
||||
workflows: dict[str, Workflow] = {}
|
||||
for workflow_s in workflows_s.split("\n"):
|
||||
name, block_s = workflow_s.split("{")
|
||||
workflows[name] = []
|
||||
|
||||
for block in block_s[:-1].split(","):
|
||||
check: Check
|
||||
if (i := block.find(":")) >= 0:
|
||||
check, target = (
|
||||
cast(Category, block[0]),
|
||||
cast(Literal["<", ">"], block[1]),
|
||||
int(block[2:i]),
|
||||
), block[i + 1 :]
|
||||
else:
|
||||
check, target = None, block
|
||||
workflows[name].append((check, target))
|
||||
|
||||
# part 1
|
||||
parts: list[Part] = [
|
||||
{cast(Category, s[0]): int(s[2:]) for s in part_s[1:-1].split(",")}
|
||||
for part_s in parts_s.split("\n")
|
||||
]
|
||||
answer_1 = sum(sum(part.values()) for part in parts if accept(workflows, part))
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
|
||||
# part 2
|
||||
answer_2 = propagate(
|
||||
workflows, {cast(Category, c): (1, 4000) for c in ["x", "m", "a", "s"]}
|
||||
)
|
||||
print(f"answer 2 is {answer_2}")
|
43
src/holt59/aoc/2023/day2.py
Normal file
43
src/holt59/aoc/2023/day2.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import math
|
||||
import sys
|
||||
from typing import Literal, TypeAlias, cast
|
||||
|
||||
CubeType: TypeAlias = Literal["red", "blue", "green"]
|
||||
|
||||
MAX_CUBES: dict[CubeType, int] = {"red": 12, "green": 13, "blue": 14}
|
||||
|
||||
# parse games
|
||||
lines = sys.stdin.read().splitlines()
|
||||
games: dict[int, list[dict[CubeType, int]]] = {}
|
||||
for line in filter(bool, lines):
|
||||
id_part, sets_part = line.split(":")
|
||||
|
||||
games[int(id_part.split(" ")[-1])] = [
|
||||
{
|
||||
cast(CubeType, s[1]): int(s[0])
|
||||
for cube_draw in cube_set_s.strip().split(", ")
|
||||
if (s := cube_draw.split(" "))
|
||||
}
|
||||
for cube_set_s in sets_part.strip().split(";")
|
||||
]
|
||||
|
||||
# part 1
|
||||
answer_1 = sum(
|
||||
id
|
||||
for id, set_of_cubes in games.items()
|
||||
if all(
|
||||
n_cubes <= MAX_CUBES[cube]
|
||||
for cube_set in set_of_cubes
|
||||
for cube, n_cubes in cube_set.items()
|
||||
)
|
||||
)
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
# part 2
|
||||
answer_2 = sum(
|
||||
math.prod(
|
||||
max(cube_set.get(cube, 0) for cube_set in set_of_cubes) for cube in MAX_CUBES
|
||||
)
|
||||
for set_of_cubes in games.values()
|
||||
)
|
||||
print(f"answer 2 is {answer_2}")
|
13
src/holt59/aoc/2023/day20.py
Normal file
13
src/holt59/aoc/2023/day20.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
|
||||
lines = sys.stdin.read().splitlines()
|
||||
|
||||
# part 1
|
||||
answer_1 = ...
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
# part 2
|
||||
answer_2 = ...
|
||||
print(f"answer 2 is {answer_2}")
|
13
src/holt59/aoc/2023/day21.py
Normal file
13
src/holt59/aoc/2023/day21.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
|
||||
lines = sys.stdin.read().splitlines()
|
||||
|
||||
# part 1
|
||||
answer_1 = ...
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
# part 2
|
||||
answer_2 = ...
|
||||
print(f"answer 2 is {answer_2}")
|
13
src/holt59/aoc/2023/day22.py
Normal file
13
src/holt59/aoc/2023/day22.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
|
||||
lines = sys.stdin.read().splitlines()
|
||||
|
||||
# part 1
|
||||
answer_1 = ...
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
# part 2
|
||||
answer_2 = ...
|
||||
print(f"answer 2 is {answer_2}")
|
13
src/holt59/aoc/2023/day23.py
Normal file
13
src/holt59/aoc/2023/day23.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
|
||||
lines = sys.stdin.read().splitlines()
|
||||
|
||||
# part 1
|
||||
answer_1 = ...
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
# part 2
|
||||
answer_2 = ...
|
||||
print(f"answer 2 is {answer_2}")
|
13
src/holt59/aoc/2023/day24.py
Normal file
13
src/holt59/aoc/2023/day24.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
|
||||
lines = sys.stdin.read().splitlines()
|
||||
|
||||
# part 1
|
||||
answer_1 = ...
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
# part 2
|
||||
answer_2 = ...
|
||||
print(f"answer 2 is {answer_2}")
|
13
src/holt59/aoc/2023/day25.py
Normal file
13
src/holt59/aoc/2023/day25.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
|
||||
lines = sys.stdin.read().splitlines()
|
||||
|
||||
# part 1
|
||||
answer_1 = ...
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
# part 2
|
||||
answer_2 = ...
|
||||
print(f"answer 2 is {answer_2}")
|
53
src/holt59/aoc/2023/day3.py
Normal file
53
src/holt59/aoc/2023/day3.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import string
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
NOT_A_SYMBOL = "." + string.digits
|
||||
|
||||
lines = sys.stdin.read().splitlines()
|
||||
|
||||
values: list[int] = []
|
||||
gears: dict[tuple[int, int], list[int]] = defaultdict(list)
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
j = 0
|
||||
while j < len(line):
|
||||
# skip everything until a digit is found (start of a number)
|
||||
if line[j] not in string.digits:
|
||||
j += 1
|
||||
continue
|
||||
|
||||
# extract the range of the number and its value
|
||||
k = j + 1
|
||||
while k < len(line) and line[k] in string.digits:
|
||||
k += 1
|
||||
|
||||
value = int(line[j:k])
|
||||
|
||||
# lookup around the number if there is a symbol - we go through the number
|
||||
# itself but that should not matter since it only contains digits
|
||||
found = False
|
||||
for i2 in range(max(0, i - 1), min(i + 1, len(lines) - 1) + 1):
|
||||
for j2 in range(max(0, j - 1), min(k, len(line) - 1) + 1):
|
||||
assert i2 >= 0 and i2 < len(lines)
|
||||
assert j2 >= 0 and j2 < len(line)
|
||||
|
||||
if lines[i2][j2] not in NOT_A_SYMBOL:
|
||||
found = True
|
||||
|
||||
if lines[i2][j2] == "*":
|
||||
gears[i2, j2].append(value)
|
||||
|
||||
if found:
|
||||
values.append(value)
|
||||
|
||||
# continue starting from the end of the number
|
||||
j = k
|
||||
|
||||
# part 1
|
||||
answer_1 = sum(values)
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
# part 2
|
||||
answer_2 = sum(v1 * v2 for v1, v2 in filter(lambda vs: len(vs) == 2, gears.values()))
|
||||
print(f"answer 2 is {answer_2}")
|
41
src/holt59/aoc/2023/day4.py
Normal file
41
src/holt59/aoc/2023/day4.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Card:
|
||||
id: int
|
||||
numbers: list[int]
|
||||
values: list[int]
|
||||
|
||||
|
||||
lines = sys.stdin.read().splitlines()
|
||||
|
||||
cards: list[Card] = []
|
||||
for line in lines:
|
||||
id_part, e_part = line.split(":")
|
||||
numbers_s, values_s = e_part.split("|")
|
||||
cards.append(
|
||||
Card(
|
||||
id=int(id_part.split()[1]),
|
||||
numbers=[int(v.strip()) for v in numbers_s.strip().split()],
|
||||
values=[int(v.strip()) for v in values_s.strip().split()],
|
||||
)
|
||||
)
|
||||
|
||||
winnings = [sum(1 for n in card.values if n in card.numbers) for card in cards]
|
||||
|
||||
# part 1
|
||||
answer_1 = sum(2 ** (winning - 1) for winning in winnings if winning > 0)
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
# part 2
|
||||
card2cards = {i: list(range(i + 1, i + w + 1)) for i, w in enumerate(winnings)}
|
||||
card2values = {i: 0 for i in range(len(cards))}
|
||||
|
||||
for i in range(len(cards)):
|
||||
card2values[i] += 1
|
||||
for j in card2cards[i]:
|
||||
card2values[j] += card2values[i]
|
||||
|
||||
print(f"answer 2 is {sum(card2values.values())}")
|
129
src/holt59/aoc/2023/day5.py
Normal file
129
src/holt59/aoc/2023/day5.py
Normal file
@@ -0,0 +1,129 @@
|
||||
import sys
|
||||
from typing import Sequence
|
||||
|
||||
MAP_ORDER = [
|
||||
"seed",
|
||||
"soil",
|
||||
"fertilizer",
|
||||
"water",
|
||||
"light",
|
||||
"temperature",
|
||||
"humidity",
|
||||
"location",
|
||||
]
|
||||
|
||||
lines = sys.stdin.read().splitlines()
|
||||
|
||||
# mappings from one category to another, each list contains
|
||||
# ranges stored as (source, target, length), ordered by start and
|
||||
# completed to have no "hole"
|
||||
maps: dict[tuple[str, str], list[tuple[int, int, int]]] = {}
|
||||
|
||||
# parsing
|
||||
index = 2
|
||||
while index < len(lines):
|
||||
p1, _, p2 = lines[index].split()[0].split("-")
|
||||
|
||||
# extract the existing ranges from the file - we store as (source, target, length)
|
||||
# whereas the file is in order (target, source, length)
|
||||
index += 1
|
||||
values: list[tuple[int, int, int]] = []
|
||||
while index < len(lines) and lines[index]:
|
||||
n1, n2, n3 = lines[index].split()
|
||||
values.append((int(n2), int(n1), int(n3)))
|
||||
index += 1
|
||||
|
||||
# sort by source value
|
||||
values.sort()
|
||||
|
||||
# add a 'fake' interval starting at 0 if missing
|
||||
if values[0][0] != 0:
|
||||
values.insert(0, (0, 0, values[0][0]))
|
||||
|
||||
# fill gaps between intervals
|
||||
for i in range(len(values) - 1):
|
||||
next_start = values[i + 1][0]
|
||||
end = values[i][0] + values[i][2]
|
||||
if next_start != end:
|
||||
values.insert(
|
||||
i + 1,
|
||||
(end, end, next_start - end),
|
||||
)
|
||||
|
||||
# add an interval covering values up to at least 2**32 at the end
|
||||
last_start, _, last_length = values[-1]
|
||||
values.append((last_start + last_length, last_start + last_length, 2**32))
|
||||
|
||||
assert all(v1[0] + v1[2] == v2[0] for v1, v2 in zip(values[:-1], values[1:]))
|
||||
assert values[0][0] == 0
|
||||
assert values[-1][0] + values[-1][-1] >= 2**32
|
||||
|
||||
maps[p1, p2] = values
|
||||
index += 1
|
||||
|
||||
|
||||
def find_range(
|
||||
values: tuple[int, int], map: list[tuple[int, int, int]]
|
||||
) -> list[tuple[int, int]]:
|
||||
"""
|
||||
Given an input range, use the given mapping to find the corresponding list of
|
||||
ranges in the target domain.
|
||||
"""
|
||||
r_start, r_length = values
|
||||
ranges: list[tuple[int, int]] = []
|
||||
|
||||
# find index of the first and last intervals in map that overlaps the input
|
||||
# interval
|
||||
index_start, index_end = -1, -1
|
||||
|
||||
for index_start, (start, _, length) in enumerate(map):
|
||||
if start <= r_start and start + length > r_start:
|
||||
break
|
||||
|
||||
for index_end, (start, _, length) in enumerate(
|
||||
map[index_start:], start=index_start
|
||||
):
|
||||
if r_start + r_length >= start and r_start + r_length < start + length:
|
||||
break
|
||||
|
||||
assert index_start >= 0 and index_end >= 0
|
||||
|
||||
# special case if one interval contains everything
|
||||
if index_start == index_end:
|
||||
start, target, length = map[index_start]
|
||||
ranges.append((target + r_start - start, r_length))
|
||||
else:
|
||||
# add the start interval part
|
||||
start, target, length = map[index_start]
|
||||
ranges.append((target + r_start - start, start + length - r_start))
|
||||
|
||||
# add all intervals between the first and last (excluding both)
|
||||
index = index_start + 1
|
||||
while index < index_end:
|
||||
start, target, length = map[index]
|
||||
ranges.append((target, length))
|
||||
index += 1
|
||||
|
||||
# add the last interval
|
||||
start, target, length = map[index_end]
|
||||
ranges.append((target, r_start + r_length - start))
|
||||
|
||||
return ranges
|
||||
|
||||
|
||||
def find_location_ranges(seeds: Sequence[tuple[int, int]]) -> Sequence[tuple[int, int]]:
|
||||
for map1, map2 in zip(MAP_ORDER[:-1], MAP_ORDER[1:]):
|
||||
seeds = [s2 for s1 in seeds for s2 in find_range(s1, maps[map1, map2])]
|
||||
return seeds
|
||||
|
||||
|
||||
# part 1 - use find_range() with range of length 1
|
||||
seeds_p1 = [(int(s), 1) for s in lines[0].split(":")[1].strip().split()]
|
||||
answer_1 = min(start for start, _ in find_location_ranges(seeds_p1))
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
# # part 2
|
||||
parts = lines[0].split(":")[1].strip().split()
|
||||
seeds_p2 = [(int(s), int(e)) for s, e in zip(parts[::2], parts[1::2])]
|
||||
answer_2 = min(start for start, _ in find_location_ranges(seeds_p2))
|
||||
print(f"answer 2 is {answer_2}")
|
47
src/holt59/aoc/2023/day6.py
Normal file
47
src/holt59/aoc/2023/day6.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import math
|
||||
import sys
|
||||
|
||||
|
||||
def extreme_times_to_beat(time: int, distance: int) -> tuple[int, int]:
|
||||
# formula (T = total time, D = target distance)
|
||||
# - speed(t) = t,
|
||||
# - distance(t) = (T - t) * speed(t)
|
||||
# - distance(t) > D
|
||||
# <=> (T - t) * t > D
|
||||
# <=> -t^2 + T * t - D >= 0
|
||||
|
||||
a, b, c = -1, time, -distance
|
||||
d = b * b - 4 * a * c
|
||||
|
||||
t1 = math.ceil(-(b - math.sqrt(d)) / (2 * a))
|
||||
t2 = math.floor(-(b + math.sqrt(d)) / (2 * a))
|
||||
|
||||
if (time - t1) * t1 == distance:
|
||||
t1 += 1
|
||||
|
||||
if (time - t2) * t2 == distance:
|
||||
t2 -= 1
|
||||
|
||||
return t1, t2
|
||||
|
||||
|
||||
lines = sys.stdin.read().splitlines()
|
||||
|
||||
# part 1
|
||||
times = list(map(int, lines[0].split()[1:]))
|
||||
distances = list(map(int, lines[1].split()[1:]))
|
||||
answer_1 = math.prod(
|
||||
t2 - t1 + 1
|
||||
for t1, t2 in (
|
||||
extreme_times_to_beat(time, distance)
|
||||
for time, distance in zip(times, distances)
|
||||
)
|
||||
)
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
# part 2
|
||||
time = int(lines[0].split(":")[1].strip().replace(" ", ""))
|
||||
distance = int(lines[1].split(":")[1].strip().replace(" ", ""))
|
||||
t1, t2 = extreme_times_to_beat(time, distance)
|
||||
answer_2 = t2 - t1 + 1
|
||||
print(f"answer 2 is {answer_2}")
|
49
src/holt59/aoc/2023/day7.py
Normal file
49
src/holt59/aoc/2023/day7.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
|
||||
class HandTypes:
|
||||
HIGH_CARD = 0
|
||||
ONE_PAIR = 1
|
||||
TWO_PAIR = 2
|
||||
THREE_OF_A_KIND = 3
|
||||
FULL_HOUSE = 4
|
||||
FOUR_OF_A_KIND = 5
|
||||
FIVE_OF_A_KIND = 6
|
||||
|
||||
|
||||
# mapping from number of different cards + highest count of card to the hand type
|
||||
LEN_LAST_TO_TYPE: dict[int, dict[int, int]] = {
|
||||
1: defaultdict(lambda: HandTypes.FIVE_OF_A_KIND),
|
||||
2: defaultdict(lambda: HandTypes.FULL_HOUSE, {4: HandTypes.FOUR_OF_A_KIND}),
|
||||
3: defaultdict(lambda: HandTypes.TWO_PAIR, {3: HandTypes.THREE_OF_A_KIND}),
|
||||
4: defaultdict(lambda: HandTypes.ONE_PAIR),
|
||||
5: defaultdict(lambda: HandTypes.HIGH_CARD),
|
||||
}
|
||||
|
||||
|
||||
def extract_key(hand: str, values: dict[str, int], joker: str = "0") -> tuple[int, ...]:
|
||||
# get count of each cards, in increasing count - the 'or' part handles the JJJJJ
|
||||
# case when joker is True
|
||||
cnt = sorted(Counter(hand.replace(joker, "")).values()) or [0]
|
||||
|
||||
return (LEN_LAST_TO_TYPE[len(cnt)][cnt[-1] + hand.count(joker)],) + tuple(
|
||||
values[c] for c in hand
|
||||
)
|
||||
|
||||
|
||||
lines = sys.stdin.read().splitlines()
|
||||
cards = [(t[0], int(t[1])) for line in lines if (t := line.split())]
|
||||
|
||||
|
||||
# part 1
|
||||
values = {card: value for value, card in enumerate("23456789TJQKA")}
|
||||
cards.sort(key=lambda cv: extract_key(cv[0], values=values))
|
||||
answer_1 = sum(rank * value for rank, (_, value) in enumerate(cards, start=1))
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
# part 2
|
||||
values = {card: value for value, card in enumerate("J23456789TQKA")}
|
||||
cards.sort(key=lambda cv: extract_key(cv[0], values=values, joker="J"))
|
||||
answer_2 = sum(rank * value for rank, (_, value) in enumerate(cards, start=1))
|
||||
print(f"answer 2 is {answer_2}")
|
29
src/holt59/aoc/2023/day8.py
Normal file
29
src/holt59/aoc/2023/day8.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import itertools
|
||||
import math
|
||||
import sys
|
||||
|
||||
lines = sys.stdin.read().splitlines()
|
||||
|
||||
sequence = lines[0]
|
||||
nodes = {
|
||||
p[0]: {d: n for d, n in zip("LR", p[1].strip("()").split(", "))}
|
||||
for line in lines[2:]
|
||||
if (p := line.split(" = "))
|
||||
}
|
||||
|
||||
|
||||
def path(start: str):
|
||||
path = [start]
|
||||
it_seq = iter(itertools.cycle(sequence))
|
||||
while not path[-1].endswith("Z"):
|
||||
path.append(nodes[path[-1]][next(it_seq)])
|
||||
return path
|
||||
|
||||
|
||||
# part 1
|
||||
answer_1 = len(path(next(node for node in nodes if node.endswith("A")))) - 1
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
# part 2
|
||||
answer_2 = math.lcm(*(len(path(node)) - 1 for node in nodes if node.endswith("A")))
|
||||
print(f"answer 2 is {answer_2}")
|
29
src/holt59/aoc/2023/day9.py
Normal file
29
src/holt59/aoc/2023/day9.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import sys
|
||||
|
||||
lines = sys.stdin.read().splitlines()
|
||||
|
||||
data = [[int(c) for c in line.split()] for line in lines]
|
||||
|
||||
right_values: list[int] = []
|
||||
left_values: list[int] = []
|
||||
for values in data:
|
||||
diffs = [values]
|
||||
while any(d != 0 for d in diffs[-1]):
|
||||
diffs.append([rhs - lhs for lhs, rhs in zip(diffs[-1][:-1], diffs[-1][1:])])
|
||||
|
||||
rhs: list[int] = [0]
|
||||
lhs: list[int] = [0]
|
||||
for cx in range(len(diffs) - 1):
|
||||
rhs.append(diffs[-cx - 2][-1] + rhs[cx])
|
||||
lhs.append(diffs[-cx - 2][0] - lhs[cx])
|
||||
|
||||
right_values.append(rhs[-1])
|
||||
left_values.append(lhs[-1])
|
||||
|
||||
# part 1
|
||||
answer_1 = sum(right_values)
|
||||
print(f"answer 1 is {answer_1}")
|
||||
|
||||
# part 2
|
||||
answer_2 = sum(left_values)
|
||||
print(f"answer 2 is {answer_2}")
|
Reference in New Issue
Block a user