Compare commits
164 Commits
day22
...
dev/2024/1
Author | SHA1 | Date | |
---|---|---|---|
|
24580fdfd8 | ||
|
7c0a124a5d | ||
|
11e32ddfda | ||
|
4dcdab9931 | ||
|
f965eea33a | ||
|
c3c73ee517 | ||
|
8308940674 | ||
|
bc06f86fdc | ||
|
2c25b33bcc | ||
|
8651884ca6 | ||
|
7447c7b536 | ||
|
3e8d796b2e | ||
|
fcd4b47951 | ||
|
b15131bf1e | ||
|
2c5c51e05f | ||
|
51275dd539 | ||
|
f1ae1c598f | ||
|
91ba8ec86f | ||
|
323f810fcd | ||
|
8969ea895f | ||
|
67f7eef636 | ||
|
4f8b50577a | ||
|
67e41503c9 | ||
|
30e0bb3665 | ||
|
291c627c79 | ||
|
89306f4a04 | ||
|
721d69e766 | ||
|
356fd35b08 | ||
|
92bd85e1dd | ||
|
22129048e7 | ||
|
781e4cd6e1 | ||
|
46558672e8 | ||
|
3c544c559b | ||
|
4367a5183a | ||
|
03e4e75978 | ||
|
98eb515c19 | ||
|
dd3f332870 | ||
|
9d7ef94fa6 | ||
ce315b8778 | |||
|
ab4e3e199c | ||
|
2c1a0b919b | ||
|
cd6f97cd7e | ||
|
5312755f32 | ||
|
55cb5ed745 | ||
|
f0d8e156a9 | ||
|
c19279fad3 | ||
0d50b44c37 | |||
|
b32d46b641 | ||
|
5c43eb2c73 | ||
|
540fe37b9d | ||
|
2a4f923552 | ||
|
4821db89cc | ||
|
d1733a5888 | ||
|
dd8458fa96 | ||
|
850c66cd8d | ||
|
2597235d0c | ||
|
db9a3b3ed3 | ||
|
31b0e9f195 | ||
|
5b07e73382 | ||
|
c1732baa0d | ||
|
de96ab0e25 | ||
|
cd58b7861b | ||
|
8d2f61fa65 | ||
|
3d7dd37c11 | ||
|
f373528b06 | ||
|
3fe9555cb1 | ||
|
f94e2bd831 | ||
|
685f1e56d7 | ||
|
ea0c9e7812 | ||
|
52cb793d06 | ||
|
4a2a63b0b0 | ||
|
9f96abbd43 | ||
|
57bf025622 | ||
|
bcadb68189 | ||
|
d7d7837c1f | ||
|
82fab771ab | ||
|
85fff24cc1 | ||
|
9326d6c76c | ||
|
eefb3ceb44 | ||
|
2959387bcd | ||
|
41b07cfe83 | ||
|
981e745eb0 | ||
|
8760e47283 | ||
|
1db3ab9090 | ||
|
9a1769e200 | ||
|
8c150c0bb1 | ||
|
69929b28dd | ||
f41b50c9e0 | |||
|
d6b99454d2 | ||
|
1fc3c1632d | ||
|
9141029557 | ||
|
9d5b57fd56 | ||
|
0756cf74c4 | ||
|
bd5727c758 | ||
|
0ebd823656 | ||
|
7c6c9e5995 | ||
|
f4cd8318b0 | ||
|
e06f3da2bd | ||
|
fb8a911d4d | ||
|
4e1c71b221 | ||
|
0f8a272b71 | ||
|
508c8cdc42 | ||
|
ee55c807ef | ||
|
10a5b92740 | ||
|
d3eacd48aa | ||
|
53f05058f3 | ||
|
c0ea724d4c | ||
|
57c15270dc | ||
|
7533dd0b11 | ||
|
dd80b30e26 | ||
|
0508d95e33 | ||
|
f2cc3e4d16 | ||
|
9526c383f3 | ||
|
dd88a1838d | ||
|
72ffd399b3 | ||
|
020ad7c6d1 | ||
|
1fc4d6531d | ||
|
d8bb659e78 | ||
|
6ade69ac35 | ||
|
ed7149e5f4 | ||
|
e2df3c4825 | ||
|
266703cdc0 | ||
|
f635ea3c97 | ||
|
304aeb16bd | ||
|
ed00c72e59 | ||
|
835458e0f3 | ||
|
3b9e9a2c8f | ||
|
fadb2a71c2 | ||
|
91434051c6 | ||
|
c1161f6c1f | ||
|
3b4efce02f | ||
|
2aaf55b72f | ||
|
d493856d20 | ||
|
67647c7923 | ||
|
3c61e5cb7f | ||
|
37e0e1ef06 | ||
|
725e18d480 | ||
|
3d383527e4 | ||
|
48e8dff52b | ||
|
0b53406b52 | ||
|
232d019b40 | ||
|
35190adcdf | ||
|
d4199b2810 | ||
|
971c1b0dda | ||
|
4fe1f521b7 | ||
|
38b1d86514 | ||
|
4899583b15 | ||
|
792951afa8 | ||
|
5004aa3376 | ||
|
9ba338a4f1 | ||
|
8795d7a276 | ||
|
89adfb151a | ||
|
9fd8a5866a | ||
|
7564fac345 | ||
|
6bd27e4bca | ||
|
26cfe60f16 | ||
|
32a1220072 | ||
|
9852aea94b | ||
|
f76767ca6d | ||
|
1b60725e9c | ||
|
b8cb2cb0b9 | ||
|
228f7501bb | ||
|
f4ef0a2666 | ||
|
60e68ed31c |
12
.drone.yml
Normal file
12
.drone.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: default
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: tests
|
||||||
|
image: python:3.10-slim
|
||||||
|
commands:
|
||||||
|
- pip install poetry
|
||||||
|
- poetry install
|
||||||
|
- poetry run poe lint
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1 +1,6 @@
|
|||||||
|
# python / VS Code
|
||||||
venv
|
venv
|
||||||
|
__pycache__
|
||||||
|
.ruff_cache
|
||||||
|
.vscode
|
||||||
|
build
|
||||||
|
52
2021/day5.py
52
2021/day5.py
@@ -1,52 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
lines: list[str] = sys.stdin.read().splitlines()
|
|
||||||
|
|
||||||
sections: list[tuple[tuple[int, int], tuple[int, int]]] = [
|
|
||||||
(
|
|
||||||
(
|
|
||||||
int(line.split(" -> ")[0].split(",")[0]),
|
|
||||||
int(line.split(" -> ")[0].split(",")[1]),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
int(line.split(" -> ")[1].split(",")[0]),
|
|
||||||
int(line.split(" -> ")[1].split(",")[1]),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
for line in lines
|
|
||||||
]
|
|
||||||
|
|
||||||
np_sections = np.array(sections).reshape(-1, 4)
|
|
||||||
|
|
||||||
x_min, x_max, y_min, y_max = (
|
|
||||||
min(np_sections[:, 0].min(), np_sections[:, 2].min()),
|
|
||||||
max(np_sections[:, 0].max(), np_sections[:, 2].max()),
|
|
||||||
min(np_sections[:, 1].min(), np_sections[:, 3].min()),
|
|
||||||
max(np_sections[:, 1].max(), np_sections[:, 3].max()),
|
|
||||||
)
|
|
||||||
|
|
||||||
counts_1 = np.zeros((y_max + 1, x_max + 1), dtype=int)
|
|
||||||
counts_2 = counts_1.copy()
|
|
||||||
|
|
||||||
for (x1, y1), (x2, y2) in sections:
|
|
||||||
|
|
||||||
x_rng = range(x1, x2 + 1, 1) if x2 >= x1 else range(x1, x2 - 1, -1)
|
|
||||||
y_rng = range(y1, y2 + 1, 1) if y2 >= y1 else range(y1, y2 - 1, -1)
|
|
||||||
|
|
||||||
if x1 == x2 or y1 == y2:
|
|
||||||
counts_1[list(y_rng), list(x_rng)] += 1
|
|
||||||
counts_2[list(y_rng), list(x_rng)] += 1
|
|
||||||
elif abs(x2 - x1) == abs(y2 - y1):
|
|
||||||
for i, j in zip(y_rng, x_rng):
|
|
||||||
counts_2[i, j] += 1
|
|
||||||
|
|
||||||
answer_1 = (counts_1 >= 2).sum()
|
|
||||||
print(f"answer 1 is {answer_1}")
|
|
||||||
|
|
||||||
answer_2 = (counts_2 >= 2).sum()
|
|
||||||
print(f"answer 2 is {answer_2}")
|
|
20
2022/day1.py
20
2022/day1.py
@@ -1,20 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
lines = sys.stdin.readlines()
|
|
||||||
|
|
||||||
# we store the list of calories for each elf in values, and we use the last element
|
|
||||||
# of values to accumulate
|
|
||||||
values: list[int] = [0]
|
|
||||||
for line in lines:
|
|
||||||
if not line.strip():
|
|
||||||
values = values + [0]
|
|
||||||
else:
|
|
||||||
values[-1] += int(line.strip())
|
|
||||||
|
|
||||||
# part 1
|
|
||||||
print(f"answer 1 is {max(values)}")
|
|
||||||
|
|
||||||
# part 2
|
|
||||||
print(f"answer 2 is {sum(sorted(values)[-3:])}")
|
|
@@ -1,40 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
lines = sys.stdin.read().splitlines()
|
|
||||||
|
|
||||||
cycle = 1
|
|
||||||
x = 1
|
|
||||||
|
|
||||||
values = {cycle: x}
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
cycle += 1
|
|
||||||
|
|
||||||
if line == "noop":
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
r = int(line.split()[1])
|
|
||||||
|
|
||||||
values[cycle] = x
|
|
||||||
|
|
||||||
cycle += 1
|
|
||||||
x += r
|
|
||||||
|
|
||||||
values[cycle] = x
|
|
||||||
|
|
||||||
answer_1 = sum(c * values[c] for c in range(20, max(values.keys()) + 1, 40))
|
|
||||||
print(f"answer 1 is {answer_1}")
|
|
||||||
|
|
||||||
|
|
||||||
for i in range(6):
|
|
||||||
for j in range(40):
|
|
||||||
v = values[1 + i * 40 + j]
|
|
||||||
|
|
||||||
if j >= v - 1 and j <= v + 1:
|
|
||||||
print("#", end="")
|
|
||||||
else:
|
|
||||||
print(".", end="")
|
|
||||||
|
|
||||||
print()
|
|
165
2022/day12.py
165
2022/day12.py
@@ -1,165 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import heapq
|
|
||||||
import sys
|
|
||||||
from typing import Callable, Iterator, TypeVar
|
|
||||||
|
|
||||||
Node = TypeVar("Node")
|
|
||||||
|
|
||||||
|
|
||||||
def dijkstra(
|
|
||||||
start: Node,
|
|
||||||
neighbors: Callable[[Node], Iterator[Node]],
|
|
||||||
cost: Callable[[Node, Node], float],
|
|
||||||
) -> tuple[dict[Node, float], dict[Node, Node]]:
|
|
||||||
"""
|
|
||||||
Compute shortest paths from one node to all reachable ones.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
start: Starting node.
|
|
||||||
neighbors: Function returning the neighbors of a node.
|
|
||||||
cost: Function to compute the cost of an edge.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A tuple (lengths, parents) where lengths is a mapping from Node to distance
|
|
||||||
(from the starting node) and parents a mapping from parents Node (in the
|
|
||||||
shortest path). If keyset of lengths and parents is the same. If a Node is not
|
|
||||||
in the mapping, it cannot be reached from the starting node.
|
|
||||||
"""
|
|
||||||
|
|
||||||
queue: list[tuple[float, Node]] = []
|
|
||||||
|
|
||||||
visited: set[Node] = set()
|
|
||||||
lengths: dict[Node, float] = {start: 0}
|
|
||||||
parents: dict[Node, Node] = {}
|
|
||||||
|
|
||||||
heapq.heappush(queue, (0, start))
|
|
||||||
|
|
||||||
while queue:
|
|
||||||
length, current = heapq.heappop(queue)
|
|
||||||
|
|
||||||
if current in visited:
|
|
||||||
continue
|
|
||||||
|
|
||||||
visited.add(current)
|
|
||||||
|
|
||||||
for neighbor in neighbors(current):
|
|
||||||
|
|
||||||
if neighbor in visited:
|
|
||||||
continue
|
|
||||||
|
|
||||||
neighbor_cost = length + cost(current, neighbor)
|
|
||||||
|
|
||||||
if neighbor_cost < lengths.get(neighbor, float("inf")):
|
|
||||||
lengths[neighbor] = neighbor_cost
|
|
||||||
parents[neighbor] = current
|
|
||||||
|
|
||||||
heapq.heappush(queue, (neighbor_cost, neighbor))
|
|
||||||
|
|
||||||
return lengths, parents
|
|
||||||
|
|
||||||
|
|
||||||
def make_path(parents: dict[Node, Node], start: Node, end: Node) -> list[Node] | None:
|
|
||||||
|
|
||||||
if end not in parents:
|
|
||||||
return None
|
|
||||||
|
|
||||||
path: list[Node] = [end]
|
|
||||||
|
|
||||||
while path[-1] is not start:
|
|
||||||
path.append(parents[path[-1]])
|
|
||||||
|
|
||||||
return list(reversed(path))
|
|
||||||
|
|
||||||
|
|
||||||
def print_path(path: list[tuple[int, int]], n_rows: int, n_cols: int) -> None:
|
|
||||||
end = path[-1]
|
|
||||||
|
|
||||||
graph = [["." for _c in range(n_cols)] for _r in range(n_rows)]
|
|
||||||
graph[end[0]][end[1]] = "E"
|
|
||||||
|
|
||||||
for i in range(0, len(path) - 1):
|
|
||||||
cr, cc = path[i]
|
|
||||||
nr, nc = path[i + 1]
|
|
||||||
|
|
||||||
if cr == nr and nc == cc - 1:
|
|
||||||
graph[cr][cc] = "<"
|
|
||||||
elif cr == nr and nc == cc + 1:
|
|
||||||
graph[cr][cc] = ">"
|
|
||||||
elif cr == nr - 1 and nc == cc:
|
|
||||||
graph[cr][cc] = "v"
|
|
||||||
elif cr == nr + 1 and nc == cc:
|
|
||||||
graph[cr][cc] = "^"
|
|
||||||
else:
|
|
||||||
assert False, "{} -> {} infeasible".format(path[i], path[i + 1])
|
|
||||||
|
|
||||||
print("\n".join("".join(row) for row in graph))
|
|
||||||
|
|
||||||
|
|
||||||
def neighbors(
|
|
||||||
grid: list[list[int]], node: tuple[int, int], up: bool
|
|
||||||
) -> Iterator[tuple[int, int]]:
|
|
||||||
n_rows = len(grid)
|
|
||||||
n_cols = len(grid[0])
|
|
||||||
|
|
||||||
c_row, c_col = node
|
|
||||||
for n_row, n_col in (
|
|
||||||
(c_row - 1, c_col),
|
|
||||||
(c_row + 1, c_col),
|
|
||||||
(c_row, c_col - 1),
|
|
||||||
(c_row, c_col + 1),
|
|
||||||
):
|
|
||||||
|
|
||||||
if not (n_row >= 0 and n_row < n_rows and n_col >= 0 and n_col < n_cols):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if up and grid[n_row][n_col] > grid[c_row][c_col] + 1:
|
|
||||||
continue
|
|
||||||
elif not up and grid[n_row][n_col] < grid[c_row][c_col] - 1:
|
|
||||||
continue
|
|
||||||
|
|
||||||
yield n_row, n_col
|
|
||||||
|
|
||||||
|
|
||||||
# === main code ===
|
|
||||||
|
|
||||||
lines = sys.stdin.read().splitlines()
|
|
||||||
|
|
||||||
grid = [[ord(cell) - ord("a") for cell in line] for line in lines]
|
|
||||||
|
|
||||||
start: tuple[int, int]
|
|
||||||
end: tuple[int, int]
|
|
||||||
|
|
||||||
# for part 2
|
|
||||||
start_s: list[tuple[int, int]] = []
|
|
||||||
|
|
||||||
for i_row, row in enumerate(grid):
|
|
||||||
for i_col, col in enumerate(row):
|
|
||||||
if chr(col + ord("a")) == "S":
|
|
||||||
start = (i_row, i_col)
|
|
||||||
start_s.append(start)
|
|
||||||
elif chr(col + ord("a")) == "E":
|
|
||||||
end = (i_row, i_col)
|
|
||||||
elif col == 0:
|
|
||||||
start_s.append((i_row, i_col))
|
|
||||||
|
|
||||||
# fix values
|
|
||||||
grid[start[0]][start[1]] = 0
|
|
||||||
grid[end[0]][end[1]] = ord("z") - ord("a")
|
|
||||||
|
|
||||||
|
|
||||||
lengths_1, parents_1 = dijkstra(
|
|
||||||
start=start, neighbors=lambda n: neighbors(grid, n, True), cost=lambda lhs, rhs: 1
|
|
||||||
)
|
|
||||||
path_1 = make_path(parents_1, start, end)
|
|
||||||
assert path_1 is not None
|
|
||||||
|
|
||||||
print_path(path_1, n_rows=len(grid), n_cols=len(grid[0]))
|
|
||||||
|
|
||||||
print(f"answer 1 is {lengths_1[end] - 1}")
|
|
||||||
|
|
||||||
lengths_2, parents_2 = dijkstra(
|
|
||||||
start=end, neighbors=lambda n: neighbors(grid, n, False), cost=lambda lhs, rhs: 1
|
|
||||||
)
|
|
||||||
answer_2 = min(lengths_2.get(start, float("inf")) for start in start_s)
|
|
||||||
print(f"answer 2 is {answer_2}")
|
|
@@ -1,41 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
from functools import cmp_to_key
|
|
||||||
|
|
||||||
blocks = sys.stdin.read().strip().split("\n\n")
|
|
||||||
|
|
||||||
pairs = [tuple(json.loads(p) for p in block.split("\n")) for block in blocks]
|
|
||||||
|
|
||||||
|
|
||||||
def compare(lhs: list[int | list], rhs: list[int | list]) -> int:
|
|
||||||
|
|
||||||
for lhs_a, rhs_a in zip(lhs, rhs):
|
|
||||||
if isinstance(lhs_a, int) and isinstance(rhs_a, int):
|
|
||||||
if lhs_a != rhs_a:
|
|
||||||
return rhs_a - lhs_a
|
|
||||||
else:
|
|
||||||
if not isinstance(lhs_a, list):
|
|
||||||
lhs_a = [lhs_a]
|
|
||||||
elif not isinstance(rhs_a, list):
|
|
||||||
rhs_a = [rhs_a]
|
|
||||||
assert isinstance(rhs_a, list) and isinstance(lhs_a, list)
|
|
||||||
r = compare(lhs_a, rhs_a)
|
|
||||||
if r != 0:
|
|
||||||
return r
|
|
||||||
|
|
||||||
return len(rhs) - len(lhs)
|
|
||||||
|
|
||||||
|
|
||||||
answer_1 = sum(i + 1 for i, (lhs, rhs) in enumerate(pairs) if compare(lhs, rhs) > 0)
|
|
||||||
print(f"answer_1 is {answer_1}")
|
|
||||||
|
|
||||||
dividers = [[[2]], [[6]]]
|
|
||||||
|
|
||||||
packets = [packet for packets in pairs for packet in packets]
|
|
||||||
packets.extend(dividers)
|
|
||||||
packets = list(reversed(sorted(packets, key=cmp_to_key(compare))))
|
|
||||||
|
|
||||||
d_index = [packets.index(d) + 1 for d in dividers]
|
|
||||||
print(f"answer 2 is {d_index[0] * d_index[1]}")
|
|
144
2022/day14.py
144
2022/day14.py
@@ -1,144 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from collections import defaultdict
|
|
||||||
from enum import Enum, auto
|
|
||||||
from typing import Callable, cast
|
|
||||||
|
|
||||||
|
|
||||||
class Cell(Enum):
|
|
||||||
AIR = auto()
|
|
||||||
ROCK = auto()
|
|
||||||
SAND = auto()
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return {Cell.AIR: ".", Cell.ROCK: "#", Cell.SAND: "O"}[self]
|
|
||||||
|
|
||||||
|
|
||||||
def print_blocks(blocks: dict[tuple[int, int], Cell]):
|
|
||||||
"""
|
|
||||||
Print the given set of blocks on a grid.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
blocks: Set of blocks to print.
|
|
||||||
"""
|
|
||||||
x_min, y_min, x_max, y_max = (
|
|
||||||
min(x for x, y in blocks),
|
|
||||||
0,
|
|
||||||
max(x for x, y in blocks),
|
|
||||||
max(y for x, y in blocks),
|
|
||||||
)
|
|
||||||
|
|
||||||
for y in range(y_min, y_max + 1):
|
|
||||||
print(
|
|
||||||
"".join(str(blocks.get((x, y), Cell.AIR)) for x in range(x_min, x_max + 1))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def flow(
|
|
||||||
blocks: dict[tuple[int, int], Cell],
|
|
||||||
stop_fn: Callable[[int, int], bool],
|
|
||||||
fill_fn: Callable[[int, int], Cell],
|
|
||||||
) -> dict[tuple[int, int], Cell]:
|
|
||||||
"""
|
|
||||||
Flow sands onto the given set of blocks
|
|
||||||
|
|
||||||
Args:
|
|
||||||
blocks: Blocks containing ROCK position. Modified in-place.
|
|
||||||
stop_fn: Function called with the last (assumed) position of a grain of
|
|
||||||
sand BEFORE adding it to blocks. If the function returns True, the grain
|
|
||||||
is added and a new one is flowed, otherwise, the whole procedure stops
|
|
||||||
and the function returns (without adding the final grain).
|
|
||||||
fill_fn: Function called when the target position of a grain (during the
|
|
||||||
flowing process) is missing from blocks.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The input blocks.
|
|
||||||
"""
|
|
||||||
|
|
||||||
y_max = max(y for x, y in blocks)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
x, y = 500, 0
|
|
||||||
|
|
||||||
while y <= y_max:
|
|
||||||
|
|
||||||
moved = False
|
|
||||||
for cx, cy in ((x, y + 1), (x - 1, y + 1), (x + 1, y + 1)):
|
|
||||||
if (cx, cy) not in blocks and fill_fn(cx, cy) == Cell.AIR:
|
|
||||||
x, y = cx, cy
|
|
||||||
moved = True
|
|
||||||
elif blocks[cx, cy] == Cell.AIR:
|
|
||||||
x, y = cx, cy
|
|
||||||
moved = True
|
|
||||||
|
|
||||||
if moved:
|
|
||||||
break
|
|
||||||
|
|
||||||
if not moved:
|
|
||||||
break
|
|
||||||
|
|
||||||
if stop_fn(x, y):
|
|
||||||
break
|
|
||||||
|
|
||||||
blocks[x, y] = Cell.SAND
|
|
||||||
|
|
||||||
return blocks
|
|
||||||
|
|
||||||
|
|
||||||
# === inputs ===
|
|
||||||
|
|
||||||
lines = sys.stdin.read().splitlines()
|
|
||||||
|
|
||||||
paths: list[list[tuple[int, int]]] = []
|
|
||||||
for line in lines:
|
|
||||||
parts = line.split(" -> ")
|
|
||||||
paths.append(
|
|
||||||
[
|
|
||||||
cast(tuple[int, int], tuple(int(c.strip()) for c in part.split(",")))
|
|
||||||
for part in parts
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
blocks: dict[tuple[int, int], Cell] = {}
|
|
||||||
for path in paths:
|
|
||||||
for start, end in zip(path[:-1], path[1:]):
|
|
||||||
x_start = min(start[0], end[0])
|
|
||||||
x_end = max(start[0], end[0]) + 1
|
|
||||||
y_start = min(start[1], end[1])
|
|
||||||
y_end = max(start[1], end[1]) + 1
|
|
||||||
|
|
||||||
for x in range(x_start, x_end):
|
|
||||||
for y in range(y_start, y_end):
|
|
||||||
blocks[x, y] = Cell.ROCK
|
|
||||||
|
|
||||||
print_blocks(blocks)
|
|
||||||
print()
|
|
||||||
|
|
||||||
x_min, y_min, x_max, y_max = (
|
|
||||||
min(x for x, y in blocks),
|
|
||||||
0,
|
|
||||||
max(x for x, y in blocks),
|
|
||||||
max(y for x, y in blocks),
|
|
||||||
)
|
|
||||||
|
|
||||||
# === part 1 ===
|
|
||||||
|
|
||||||
blocks_1 = flow(
|
|
||||||
blocks.copy(), stop_fn=lambda x, y: y > y_max, fill_fn=lambda x, y: Cell.AIR
|
|
||||||
)
|
|
||||||
print_blocks(blocks_1)
|
|
||||||
print(f"answer 1 is {sum(v == Cell.SAND for v in blocks_1.values())}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# === part 2 ===
|
|
||||||
|
|
||||||
blocks_2 = flow(
|
|
||||||
blocks.copy(),
|
|
||||||
stop_fn=lambda x, y: x == 500 and y == 0,
|
|
||||||
fill_fn=lambda x, y: Cell.AIR if y < y_max + 2 else Cell.ROCK,
|
|
||||||
)
|
|
||||||
blocks_2[500, 0] = Cell.SAND
|
|
||||||
print_blocks(blocks_2)
|
|
||||||
print(f"answer 2 is {sum(v == Cell.SAND for v in blocks_2.values())}")
|
|
@@ -1,90 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import parse
|
|
||||||
|
|
||||||
|
|
||||||
def part1(sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], row: int) -> int:
|
|
||||||
|
|
||||||
no_beacons_row_l: list[np.ndarray] = []
|
|
||||||
|
|
||||||
for (sx, sy), (bx, by) in sensor_to_beacon.items():
|
|
||||||
d = abs(sx - bx) + abs(sy - by) # closest
|
|
||||||
|
|
||||||
no_beacons_row_l.append(sx - np.arange(0, d - abs(sy - row) + 1))
|
|
||||||
no_beacons_row_l.append(sx + np.arange(0, d - abs(sy - row) + 1))
|
|
||||||
|
|
||||||
beacons_at_row = set(bx for (bx, by) in sensor_to_beacon.values() if by == row)
|
|
||||||
no_beacons_row = set(np.concatenate(no_beacons_row_l)).difference(beacons_at_row)
|
|
||||||
|
|
||||||
return len(no_beacons_row)
|
|
||||||
|
|
||||||
|
|
||||||
def part2_intervals(
|
|
||||||
sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], xy_max: int
|
|
||||||
) -> tuple[int, int, int]:
|
|
||||||
from tqdm import trange
|
|
||||||
|
|
||||||
for y in trange(xy_max + 1):
|
|
||||||
its: list[tuple[int, int]] = []
|
|
||||||
for (sx, sy), (bx, by) in sensor_to_beacon.items():
|
|
||||||
d = abs(sx - bx) + abs(sy - by)
|
|
||||||
dx = d - abs(sy - y)
|
|
||||||
|
|
||||||
if dx >= 0:
|
|
||||||
its.append((max(0, sx - dx), min(sx + dx, xy_max)))
|
|
||||||
|
|
||||||
its = sorted(its)
|
|
||||||
s, e = its[0]
|
|
||||||
|
|
||||||
for si, ei in its[1:]:
|
|
||||||
if si > e + 1:
|
|
||||||
return si - 1, y, 4_000_000 * (si - 1) + y
|
|
||||||
if ei > e:
|
|
||||||
e = ei
|
|
||||||
|
|
||||||
return (0, 0, 0)
|
|
||||||
|
|
||||||
|
|
||||||
def part2_cplex(
|
|
||||||
sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], xy_max: int
|
|
||||||
) -> tuple[int, int, int]:
|
|
||||||
from docplex.mp.model import Model
|
|
||||||
|
|
||||||
m = Model()
|
|
||||||
|
|
||||||
x, y = m.continuous_var_list(2, ub=xy_max, name=["x", "y"])
|
|
||||||
|
|
||||||
for (sx, sy), (bx, by) in sensor_to_beacon.items():
|
|
||||||
d = abs(sx - bx) + abs(sy - by)
|
|
||||||
m.add_constraint(m.abs(x - sx) + m.abs(y - sy) >= d + 1, ctname=f"ct_{sx}_{sy}")
|
|
||||||
|
|
||||||
m.set_objective("min", x + y)
|
|
||||||
|
|
||||||
s = m.solve()
|
|
||||||
|
|
||||||
vx = int(s.get_value(x))
|
|
||||||
vy = int(s.get_value(y))
|
|
||||||
return vx, vy, 4_000_000 * vx + vy
|
|
||||||
|
|
||||||
|
|
||||||
lines = sys.stdin.read().splitlines()
|
|
||||||
|
|
||||||
sensor_to_beacon: dict[tuple[int, int], tuple[int, int]] = {}
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
r = parse.parse(
|
|
||||||
"Sensor at x={sx}, y={sy}: closest beacon is at x={bx}, y={by}", line
|
|
||||||
)
|
|
||||||
sensor_to_beacon[int(r["sx"]), int(r["sy"])] = (int(r["bx"]), int(r["by"]))
|
|
||||||
|
|
||||||
xy_max = 4_000_000 if max(sensor_to_beacon) > (1_000, 0) else 20
|
|
||||||
row = 2_000_000 if max(sensor_to_beacon) > (1_000, 0) else 10
|
|
||||||
|
|
||||||
print(f"answer 1 is {part1(sensor_to_beacon, row)}")
|
|
||||||
|
|
||||||
# x, y, a2 = part2_cplex(sensor_to_beacon, xy_max)
|
|
||||||
x, y, a2 = part2_intervals(sensor_to_beacon, xy_max)
|
|
||||||
print(f"answer 2 is {a2} (x={x}, y={y})")
|
|
162
2022/day16.py
162
2022/day16.py
@@ -1,162 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import heapq
|
|
||||||
import itertools
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from collections import defaultdict
|
|
||||||
from typing import FrozenSet, NamedTuple
|
|
||||||
|
|
||||||
from tqdm import tqdm
|
|
||||||
|
|
||||||
|
|
||||||
class Pipe(NamedTuple):
|
|
||||||
name: str
|
|
||||||
flow: int
|
|
||||||
tunnels: list[str]
|
|
||||||
|
|
||||||
def __lt__(self, other: object) -> bool:
|
|
||||||
return isinstance(other, Pipe) and other.name < self.name
|
|
||||||
|
|
||||||
def __eq__(self, other: object) -> bool:
|
|
||||||
return isinstance(other, Pipe) and other.name == self.name
|
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
|
||||||
return hash(self.name)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
def breadth_first_search(pipes: dict[str, Pipe], pipe: Pipe) -> dict[Pipe, int]:
|
|
||||||
"""
|
|
||||||
Runs a BFS from the given pipe and return the shortest distance (in term of hops)
|
|
||||||
to all other pipes.
|
|
||||||
"""
|
|
||||||
queue = [(0, pipe_1)]
|
|
||||||
visited = set()
|
|
||||||
distances: dict[Pipe, int] = {}
|
|
||||||
|
|
||||||
while len(distances) < len(pipes):
|
|
||||||
distance, current = heapq.heappop(queue)
|
|
||||||
|
|
||||||
if current in visited:
|
|
||||||
continue
|
|
||||||
|
|
||||||
visited.add(current)
|
|
||||||
distances[current] = distance
|
|
||||||
|
|
||||||
for tunnel in current.tunnels:
|
|
||||||
heapq.heappush(queue, (distance + 1, pipes[tunnel]))
|
|
||||||
|
|
||||||
return distances
|
|
||||||
|
|
||||||
|
|
||||||
def update_with_better(
|
|
||||||
node_at_times: dict[FrozenSet[Pipe], int], flow: int, flowing: FrozenSet[Pipe]
|
|
||||||
) -> None:
|
|
||||||
node_at_times[flowing] = max(node_at_times[flowing], flow)
|
|
||||||
|
|
||||||
|
|
||||||
def part_1(
|
|
||||||
start_pipe: Pipe,
|
|
||||||
max_time: int,
|
|
||||||
distances: dict[tuple[Pipe, Pipe], int],
|
|
||||||
relevant_pipes: FrozenSet[Pipe],
|
|
||||||
):
|
|
||||||
|
|
||||||
node_at_times: dict[int, dict[Pipe, dict[FrozenSet[Pipe], int]]] = defaultdict(
|
|
||||||
lambda: defaultdict(lambda: defaultdict(lambda: 0))
|
|
||||||
)
|
|
||||||
node_at_times[0] = {start_pipe: {frozenset(): 0}}
|
|
||||||
|
|
||||||
for time in range(max_time):
|
|
||||||
for c_pipe, nodes in node_at_times[time].items():
|
|
||||||
for flowing, flow in nodes.items():
|
|
||||||
for target in relevant_pipes:
|
|
||||||
|
|
||||||
distance = distances[c_pipe, target] + 1
|
|
||||||
if time + distance >= max_time or target in flowing:
|
|
||||||
continue
|
|
||||||
|
|
||||||
update_with_better(
|
|
||||||
node_at_times[time + distance][target],
|
|
||||||
flow + sum(pipe.flow for pipe in flowing) * distance,
|
|
||||||
flowing | {target},
|
|
||||||
)
|
|
||||||
|
|
||||||
update_with_better(
|
|
||||||
node_at_times[max_time][c_pipe],
|
|
||||||
flow + sum(pipe.flow for pipe in flowing) * (max_time - time),
|
|
||||||
flowing,
|
|
||||||
)
|
|
||||||
|
|
||||||
return max(
|
|
||||||
flow
|
|
||||||
for nodes_of_pipe in node_at_times[max_time].values()
|
|
||||||
for flow in nodes_of_pipe.values()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def part_2(
|
|
||||||
start_pipe: Pipe,
|
|
||||||
max_time: int,
|
|
||||||
distances: dict[tuple[Pipe, Pipe], int],
|
|
||||||
relevant_pipes: FrozenSet[Pipe],
|
|
||||||
):
|
|
||||||
def compute(pipes_for_me: FrozenSet[Pipe]) -> int:
|
|
||||||
return part_1(start_pipe, max_time, distances, pipes_for_me) + part_1(
|
|
||||||
start_pipe, max_time, distances, relevant_pipes - pipes_for_me
|
|
||||||
)
|
|
||||||
|
|
||||||
combs = [
|
|
||||||
frozenset(relevant_pipes_1)
|
|
||||||
for r in range(2, len(relevant_pipes) // 2 + 1)
|
|
||||||
for relevant_pipes_1 in itertools.combinations(relevant_pipes, r)
|
|
||||||
]
|
|
||||||
|
|
||||||
return max(compute(comb) for comb in tqdm(combs))
|
|
||||||
|
|
||||||
|
|
||||||
# === MAIN ===
|
|
||||||
|
|
||||||
|
|
||||||
lines = sys.stdin.read().splitlines()
|
|
||||||
|
|
||||||
|
|
||||||
pipes: dict[str, Pipe] = {}
|
|
||||||
for line in lines:
|
|
||||||
r = re.match(
|
|
||||||
R"Valve ([A-Z]+) has flow rate=([0-9]+); tunnels? leads? to valves? (.+)",
|
|
||||||
line,
|
|
||||||
)
|
|
||||||
assert r
|
|
||||||
|
|
||||||
g = r.groups()
|
|
||||||
|
|
||||||
pipes[g[0]] = Pipe(g[0], int(g[1]), g[2].split(", "))
|
|
||||||
|
|
||||||
# compute distances from one valve to any other
|
|
||||||
distances: dict[tuple[Pipe, Pipe], int] = {}
|
|
||||||
for pipe_1 in pipes.values():
|
|
||||||
distances.update(
|
|
||||||
{
|
|
||||||
(pipe_1, pipe_2): distance
|
|
||||||
for pipe_2, distance in breadth_first_search(pipes, pipe_1).items()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# valves with flow
|
|
||||||
relevant_pipes = frozenset(pipe for pipe in pipes.values() if pipe.flow > 0)
|
|
||||||
|
|
||||||
|
|
||||||
# 1651, 1653
|
|
||||||
print(part_1(pipes["AA"], 30, distances, relevant_pipes))
|
|
||||||
|
|
||||||
# 1707, 2223
|
|
||||||
print(part_2(pipes["AA"], 26, distances, relevant_pipes))
|
|
@@ -1,53 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from typing import FrozenSet
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
xyz = np.asarray(
|
|
||||||
[
|
|
||||||
tuple(int(x) for x in row.split(",")) # type: ignore
|
|
||||||
for row in sys.stdin.read().splitlines()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
xyz = xyz - xyz.min(axis=0) + 1
|
|
||||||
|
|
||||||
cubes = np.zeros(xyz.max(axis=0) + 3, dtype=bool)
|
|
||||||
cubes[xyz[:, 0], xyz[:, 1], xyz[:, 2]] = True
|
|
||||||
|
|
||||||
n_dims = len(cubes.shape)
|
|
||||||
|
|
||||||
faces = [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1)]
|
|
||||||
|
|
||||||
answer_1 = sum(
|
|
||||||
1 for x, y, z in xyz for dx, dy, dz in faces if not cubes[x + dx, y + dy, z + dz]
|
|
||||||
)
|
|
||||||
print(f"answer 1 is {answer_1}")
|
|
||||||
|
|
||||||
visited = np.zeros_like(cubes, dtype=bool)
|
|
||||||
queue = [(0, 0, 0)]
|
|
||||||
|
|
||||||
n_faces = 0
|
|
||||||
while queue:
|
|
||||||
x, y, z = queue.pop(0)
|
|
||||||
|
|
||||||
if visited[x, y, z]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
visited[x, y, z] = True
|
|
||||||
|
|
||||||
for dx, dy, dz in faces:
|
|
||||||
nx, ny, nz = x + dx, y + dy, z + dz
|
|
||||||
if not all(n >= 0 and n < cubes.shape[i] for i, n in enumerate((nx, ny, nz))):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if visited[nx, ny, nz]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if cubes[nx, ny, nz]:
|
|
||||||
n_faces += 1
|
|
||||||
else:
|
|
||||||
queue.append((nx, ny, nz))
|
|
||||||
print(f"answer 2 is {n_faces}")
|
|
225
2022/day22.py
225
2022/day22.py
@@ -1,225 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
VOID, EMPTY, WALL = 0, 1, 2
|
|
||||||
TILE_FROM_CHAR = {" ": VOID, ".": EMPTY, "#": WALL}
|
|
||||||
|
|
||||||
SCORES = {"E": 0, "S": 1, "W": 2, "N": 3}
|
|
||||||
|
|
||||||
|
|
||||||
board_map_s, direction_s = sys.stdin.read().split("\n\n")
|
|
||||||
|
|
||||||
# board
|
|
||||||
board_lines = board_map_s.splitlines()
|
|
||||||
max_line = max(len(line) for line in board_lines)
|
|
||||||
board = np.array(
|
|
||||||
[
|
|
||||||
[TILE_FROM_CHAR[c] for c in row] + [VOID] * (max_line - len(row))
|
|
||||||
for row in board_map_s.splitlines()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
directions = [
|
|
||||||
int(p1) if p2 else p1 for p1, p2 in re.findall(R"(([0-9])+|L|R)", direction_s)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# find on each row and column the first and last non-void
|
|
||||||
row_first_non_void = np.argmax(board != VOID, axis=1)
|
|
||||||
row_last_non_void = board.shape[1] - np.argmax(board[:, ::-1] != VOID, axis=1) - 1
|
|
||||||
col_first_non_void = np.argmax(board != VOID, axis=0)
|
|
||||||
col_last_non_void = board.shape[0] - np.argmax(board[::-1, :] != VOID, axis=0) - 1
|
|
||||||
|
|
||||||
|
|
||||||
faces = np.zeros_like(board)
|
|
||||||
size = np.gcd(board.shape[0], board.shape[1])
|
|
||||||
for row in range(0, board.shape[0], size):
|
|
||||||
for col in range(row_first_non_void[row], row_last_non_void[row], size):
|
|
||||||
faces[row : row + size, col : col + size] = faces.max() + 1
|
|
||||||
|
|
||||||
SIZE = np.gcd(*board.shape)
|
|
||||||
|
|
||||||
# TODO: deduce this from the actual cube...
|
|
||||||
faces_wrap: dict[int, dict[str, Callable[[int, int], tuple[int, int, str]]]]
|
|
||||||
if board.shape == (12, 16): # example
|
|
||||||
faces_wrap = {
|
|
||||||
1: {
|
|
||||||
"W": lambda y, x: (4, 4 + y, "S"), # 3N
|
|
||||||
"N": lambda y, x: (4, 11 - x, "S"), # 2N
|
|
||||||
"E": lambda y, x: (11 - y, 15, "W"), # 6E
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
"W": lambda y, x: (11, 19 - y, "N"), # 6S
|
|
||||||
"N": lambda y, x: (0, 11 - y, "S"), # 1N
|
|
||||||
"S": lambda y, x: (11, 11 - x, "N"), # 5S
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
"N": lambda y, x: (x - 4, 8, "E"), # 1W
|
|
||||||
"S": lambda y, x: (15 - x, 8, "E"), # 5W
|
|
||||||
},
|
|
||||||
4: {"E": lambda y, x: (8, 19 - y, "S")}, # 6N
|
|
||||||
5: {
|
|
||||||
"W": lambda y, x: (7, 15 - y, "N"), # 3S
|
|
||||||
"S": lambda y, x: (7, 11 - x, "N"), # 2S
|
|
||||||
},
|
|
||||||
6: {
|
|
||||||
"N": lambda y, x: (19 - x, 11, "W"), # 4E
|
|
||||||
"E": lambda y, x: (11 - y, 11, "W"), # 1E
|
|
||||||
"S": lambda y, x: (19 - x, 0, "E"), # 2W
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
else:
|
|
||||||
faces_wrap = {
|
|
||||||
1: {
|
|
||||||
"W": lambda y, x: (3 * SIZE - y - 1, 0, "E"), # 4W
|
|
||||||
"N": lambda y, x: (2 * SIZE + x, 0, "E"), # 6W
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
"N": lambda y, x: (4 * SIZE - 1, x - 2 * SIZE, "N"), # 6S
|
|
||||||
"E": lambda y, x: (3 * SIZE - y - 1, 2 * SIZE - 1, "W"), # 5E
|
|
||||||
"S": lambda y, x: (x - SIZE, 2 * SIZE - 1, "W"), # 3E
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
"W": lambda y, x: (2 * SIZE, y - SIZE, "S"), # 4N
|
|
||||||
"E": lambda y, x: (SIZE - 1, SIZE + y, "N"), # 2S
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
"W": lambda y, x: (3 * SIZE - y - 1, SIZE, "E"), # 1W
|
|
||||||
"N": lambda y, x: (SIZE + x, SIZE, "E"), # 3W
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
"E": lambda y, x: (3 * SIZE - y - 1, 3 * SIZE - 1, "W"), # 2E
|
|
||||||
"S": lambda y, x: (2 * SIZE + x, SIZE - 1, "W"), # 6E
|
|
||||||
},
|
|
||||||
6: {
|
|
||||||
"W": lambda y, x: (0, y - 2 * SIZE, "S"), # 1N
|
|
||||||
"E": lambda y, x: (3 * SIZE - 1, y - 2 * SIZE, "N"), # 5S
|
|
||||||
"S": lambda y, x: (0, x + 2 * SIZE, "S"), # 2N
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def wrap_part_1(y0: int, x0: int, r0: str) -> tuple[int, int, str]:
|
|
||||||
if r0 == "E":
|
|
||||||
return y0, row_first_non_void[y0], r0
|
|
||||||
elif r0 == "S":
|
|
||||||
return col_first_non_void[x0], x0, r0
|
|
||||||
elif r0 == "W":
|
|
||||||
return y0, row_last_non_void[y0], r0
|
|
||||||
elif r0 == "N":
|
|
||||||
return col_last_non_void[x0], x0, r0
|
|
||||||
|
|
||||||
assert False
|
|
||||||
|
|
||||||
|
|
||||||
def wrap_part_2(y0: int, x0: int, r0: str) -> tuple[int, int, str]:
|
|
||||||
cube = faces[y0, x0]
|
|
||||||
assert r0 in faces_wrap[cube]
|
|
||||||
return faces_wrap[cube][r0](y0, x0)
|
|
||||||
|
|
||||||
|
|
||||||
def run(wrap: Callable[[int, int, str], tuple[int, int, str]]) -> tuple[int, int, str]:
|
|
||||||
|
|
||||||
y0 = 0
|
|
||||||
x0 = np.where(board[0] == EMPTY)[0][0]
|
|
||||||
r0 = "E"
|
|
||||||
|
|
||||||
for direction in directions:
|
|
||||||
if isinstance(direction, int):
|
|
||||||
while direction > 0:
|
|
||||||
if r0 == "E":
|
|
||||||
xi = np.where(board[y0, x0 + 1 : x0 + direction + 1] == WALL)[0]
|
|
||||||
if len(xi):
|
|
||||||
x0 = x0 + xi[0]
|
|
||||||
direction = 0
|
|
||||||
elif (
|
|
||||||
x0 + direction < board.shape[1]
|
|
||||||
and board[y0, x0 + direction] == EMPTY
|
|
||||||
):
|
|
||||||
x0 = x0 + direction
|
|
||||||
direction = 0
|
|
||||||
else:
|
|
||||||
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
|
|
||||||
if board[y0_t, x0_t] == WALL:
|
|
||||||
x0 = row_last_non_void[y0]
|
|
||||||
direction = 0
|
|
||||||
else:
|
|
||||||
direction = direction - (row_last_non_void[y0] - x0) - 1
|
|
||||||
y0, x0, r0 = y0_t, x0_t, r0_t
|
|
||||||
elif r0 == "S":
|
|
||||||
yi = np.where(board[y0 + 1 : y0 + direction + 1, x0] == WALL)[0]
|
|
||||||
if len(yi):
|
|
||||||
y0 = y0 + yi[0]
|
|
||||||
direction = 0
|
|
||||||
elif (
|
|
||||||
y0 + direction < board.shape[0]
|
|
||||||
and board[y0 + direction, x0] == EMPTY
|
|
||||||
):
|
|
||||||
y0 = y0 + direction
|
|
||||||
direction = 0
|
|
||||||
else:
|
|
||||||
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
|
|
||||||
if board[y0_t, x0_t] == WALL:
|
|
||||||
y0 = col_last_non_void[x0]
|
|
||||||
direction = 0
|
|
||||||
else:
|
|
||||||
direction = direction - (col_last_non_void[x0] - y0) - 1
|
|
||||||
y0, x0, r0 = y0_t, x0_t, r0_t
|
|
||||||
elif r0 == "W":
|
|
||||||
left = max(x0 - direction - 1, 0)
|
|
||||||
xi = np.where(board[y0, left:x0] == WALL)[0]
|
|
||||||
if len(xi):
|
|
||||||
x0 = left + xi[-1] + 1
|
|
||||||
direction = 0
|
|
||||||
elif x0 - direction >= 0 and board[y0, x0 - direction] == EMPTY:
|
|
||||||
x0 = x0 - direction
|
|
||||||
direction = 0
|
|
||||||
else:
|
|
||||||
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
|
|
||||||
if board[y0_t, x0_t] == WALL:
|
|
||||||
x0 = row_first_non_void[y0]
|
|
||||||
direction = 0
|
|
||||||
else:
|
|
||||||
direction = direction - (x0 - row_first_non_void[y0]) - 1
|
|
||||||
y0, x0, r0 = y0_t, x0_t, r0_t
|
|
||||||
elif r0 == "N":
|
|
||||||
top = max(y0 - direction - 1, 0)
|
|
||||||
yi = np.where(board[top:y0, x0] == WALL)[0]
|
|
||||||
if len(yi):
|
|
||||||
y0 = top + yi[-1] + 1
|
|
||||||
direction = 0
|
|
||||||
elif y0 - direction >= 0 and board[y0 - direction, x0] == EMPTY:
|
|
||||||
y0 = y0 - direction
|
|
||||||
direction = 0
|
|
||||||
else:
|
|
||||||
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
|
|
||||||
if board[y0_t, x0_t] == WALL:
|
|
||||||
y0 = col_first_non_void[x0]
|
|
||||||
direction = 0
|
|
||||||
else:
|
|
||||||
direction = direction - (y0 - col_first_non_void[x0]) - 1
|
|
||||||
y0, x0, r0 = y0_t, x0_t, r0_t
|
|
||||||
else:
|
|
||||||
r0 = {
|
|
||||||
"E": {"L": "N", "R": "S"},
|
|
||||||
"N": {"L": "W", "R": "E"},
|
|
||||||
"W": {"L": "S", "R": "N"},
|
|
||||||
"S": {"L": "E", "R": "W"},
|
|
||||||
}[r0][direction]
|
|
||||||
|
|
||||||
return y0, x0, r0
|
|
||||||
|
|
||||||
|
|
||||||
y1, x1, r1 = run(wrap_part_1)
|
|
||||||
answer_1 = 1000 * (1 + y1) + 4 * (1 + x1) + SCORES[r1]
|
|
||||||
print(f"answer 1 is {answer_1}")
|
|
||||||
|
|
||||||
y2, x2, r2 = run(wrap_part_2)
|
|
||||||
answer_2 = 1000 * (1 + y2) + 4 * (1 + x2) + SCORES[r2]
|
|
||||||
print(f"answer 2 is {answer_2}")
|
|
@@ -1,3 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import sys
|
|
@@ -1,3 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import sys
|
|
@@ -1,3 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import sys
|
|
25
2022/day3.py
25
2022/day3.py
@@ -1,25 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import string
|
|
||||||
import sys
|
|
||||||
|
|
||||||
lines = [line.strip() for line in sys.stdin.readlines()]
|
|
||||||
|
|
||||||
# extract content of each part
|
|
||||||
parts = [(set(line[: len(line) // 2]), set(line[len(line) // 2 :])) for line in lines]
|
|
||||||
|
|
||||||
# priorities
|
|
||||||
priorities = {c: i + 1 for i, c in enumerate(string.ascii_letters)}
|
|
||||||
|
|
||||||
# part 1
|
|
||||||
part1 = sum(priorities[c] for p1, p2 in parts for c in p1.intersection(p2))
|
|
||||||
print(f"score 1 is {part1}")
|
|
||||||
|
|
||||||
# part 2
|
|
||||||
n_per_group = 3
|
|
||||||
part2 = sum(
|
|
||||||
priorities[c]
|
|
||||||
for i in range(0, len(lines), n_per_group)
|
|
||||||
for c in set.intersection(*map(set, (lines[i : i + n_per_group])))
|
|
||||||
)
|
|
||||||
print(f"score 2 is {part2}")
|
|
19
2022/day4.py
19
2022/day4.py
@@ -1,19 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
lines = [line.strip() for line in sys.stdin.readlines()]
|
|
||||||
|
|
||||||
|
|
||||||
def make_range(value: str) -> set[int]:
|
|
||||||
parts = value.split("-")
|
|
||||||
return set(range(int(parts[0]), int(parts[1]) + 1))
|
|
||||||
|
|
||||||
|
|
||||||
sections = [tuple(make_range(part) for part in line.split(",")) for line in lines]
|
|
||||||
|
|
||||||
score_1 = sum(s1.issubset(s2) or s2.issubset(s1) for s1, s2 in sections)
|
|
||||||
print(f"score 1 is {score_1}")
|
|
||||||
|
|
||||||
score_2 = sum(bool(s1.intersection(s2)) for s1, s2 in sections)
|
|
||||||
print(f"score 1 is {score_2}")
|
|
43
2022/day5.py
43
2022/day5.py
@@ -1,43 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import copy
|
|
||||||
import sys
|
|
||||||
|
|
||||||
blocks_s, moves_s = (part.splitlines() for part in sys.stdin.read().split("\n\n"))
|
|
||||||
|
|
||||||
blocks: dict[str, list[str]] = {stack: [] for stack in blocks_s[-1].split()}
|
|
||||||
|
|
||||||
# this codes assumes that the lines are regular, i.e., 4 characters per "crate" in the
|
|
||||||
# form of '[X] ' (including the trailing space)
|
|
||||||
#
|
|
||||||
for block in blocks_s[-2::-1]:
|
|
||||||
for stack, index in zip(blocks, range(0, len(block), 4)):
|
|
||||||
crate = block[index + 1 : index + 2].strip()
|
|
||||||
|
|
||||||
if crate:
|
|
||||||
blocks[stack].append(crate)
|
|
||||||
|
|
||||||
# part 1 - deep copy for part 2
|
|
||||||
blocks_1 = copy.deepcopy(blocks)
|
|
||||||
|
|
||||||
for move in moves_s:
|
|
||||||
_, count_s, _, from_, _, to_ = move.strip().split()
|
|
||||||
|
|
||||||
for _i in range(int(count_s)):
|
|
||||||
blocks_1[to_].append(blocks_1[from_].pop())
|
|
||||||
|
|
||||||
# part 2
|
|
||||||
blocks_2 = copy.deepcopy(blocks)
|
|
||||||
|
|
||||||
for move in moves_s:
|
|
||||||
_, count_s, _, from_, _, to_ = move.strip().split()
|
|
||||||
count = int(count_s)
|
|
||||||
|
|
||||||
blocks_2[to_].extend(blocks_2[from_][-count:])
|
|
||||||
del blocks_2[from_][-count:]
|
|
||||||
|
|
||||||
answer_1 = "".join(s[-1] for s in blocks_1.values())
|
|
||||||
print(f"answer 1 is {answer_1}")
|
|
||||||
|
|
||||||
answer_2 = "".join(s[-1] for s in blocks_2.values())
|
|
||||||
print(f"answer 2 is {answer_2}")
|
|
16
2022/day6.py
16
2022/day6.py
@@ -1,16 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
data = sys.stdin.read().strip()
|
|
||||||
|
|
||||||
|
|
||||||
def index_of_first_n_differents(data: str, n: int) -> int:
|
|
||||||
for i in range(len(data)):
|
|
||||||
if len(set(data[i : i + n])) == n:
|
|
||||||
return i + n
|
|
||||||
return -1
|
|
||||||
|
|
||||||
|
|
||||||
print(f"answer 1 is {index_of_first_n_differents(data, 4)}")
|
|
||||||
print(f"answer 2 is {index_of_first_n_differents(data, 14)}")
|
|
82
2022/day7.py
82
2022/day7.py
@@ -1,82 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
lines = sys.stdin.read().splitlines()
|
|
||||||
|
|
||||||
# we are going to use Path to create path and go up/down in the file tree since it
|
|
||||||
# implements everything we need
|
|
||||||
#
|
|
||||||
# we can use .resolve() to get normalized path, although this will add C:\ to all paths
|
|
||||||
# on Windows but that is not an issue since only the sizes matter
|
|
||||||
#
|
|
||||||
|
|
||||||
# mapping from path to list of files or directories
|
|
||||||
trees: dict[Path, list[Path]] = {}
|
|
||||||
|
|
||||||
# mapping from paths to either size (for file) or -1 for directory
|
|
||||||
sizes: dict[Path, int] = {}
|
|
||||||
|
|
||||||
# first line must be a cd otherwise we have no idea where we are
|
|
||||||
assert lines[0].startswith("$ cd")
|
|
||||||
base_path = Path(lines[0].strip("$").split()[1]).resolve()
|
|
||||||
cur_path = base_path
|
|
||||||
|
|
||||||
trees[cur_path] = []
|
|
||||||
sizes[cur_path] = -1
|
|
||||||
|
|
||||||
for line in lines[1:]:
|
|
||||||
# command
|
|
||||||
if line.startswith("$"):
|
|
||||||
parts = line.strip("$").strip().split()
|
|
||||||
command = parts[0]
|
|
||||||
|
|
||||||
if command == "cd":
|
|
||||||
cur_path = cur_path.joinpath(parts[1]).resolve()
|
|
||||||
|
|
||||||
# just initialize the lis of files if not already done
|
|
||||||
if cur_path not in trees:
|
|
||||||
trees[cur_path] = []
|
|
||||||
else:
|
|
||||||
# nothing to do here
|
|
||||||
pass
|
|
||||||
|
|
||||||
# fill the current path
|
|
||||||
else:
|
|
||||||
parts = line.split()
|
|
||||||
name: str = parts[1]
|
|
||||||
if line.startswith("dir"):
|
|
||||||
size = -1
|
|
||||||
else:
|
|
||||||
size = int(parts[0])
|
|
||||||
|
|
||||||
path = cur_path.joinpath(name)
|
|
||||||
trees[cur_path].append(path)
|
|
||||||
sizes[path] = size
|
|
||||||
|
|
||||||
|
|
||||||
def compute_size(path: Path) -> int:
|
|
||||||
size = sizes[path]
|
|
||||||
|
|
||||||
if size >= 0:
|
|
||||||
return size
|
|
||||||
|
|
||||||
return sum(compute_size(sub) for sub in trees[path])
|
|
||||||
|
|
||||||
|
|
||||||
acc_sizes = {path: compute_size(path) for path in trees}
|
|
||||||
|
|
||||||
# part 1
|
|
||||||
answer_1 = sum(size for size in acc_sizes.values() if size <= 100_000)
|
|
||||||
print(f"answer 1 is {answer_1}")
|
|
||||||
|
|
||||||
# part 2
|
|
||||||
total_space = 70_000_000
|
|
||||||
update_space = 30_000_000
|
|
||||||
free_space = total_space - acc_sizes[base_path]
|
|
||||||
|
|
||||||
to_free_space = update_space - free_space
|
|
||||||
|
|
||||||
answer_2 = min(size for size in acc_sizes.values() if size >= to_free_space)
|
|
||||||
print(f"answer 2 is {answer_2}")
|
|
54
2022/day8.py
54
2022/day8.py
@@ -1,54 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
lines = sys.stdin.read().splitlines()
|
|
||||||
|
|
||||||
trees = np.array([[int(x) for x in row] for row in lines])
|
|
||||||
|
|
||||||
# answer 1
|
|
||||||
highest_trees = np.ones(trees.shape + (4,), dtype=int) * -1
|
|
||||||
highest_trees[1:-1, 1:-1] = [
|
|
||||||
[
|
|
||||||
[
|
|
||||||
trees[:i, j].max(),
|
|
||||||
trees[i + 1 :, j].max(),
|
|
||||||
trees[i, :j].max(),
|
|
||||||
trees[i, j + 1 :].max(),
|
|
||||||
]
|
|
||||||
for j in range(1, trees.shape[1] - 1)
|
|
||||||
]
|
|
||||||
for i in range(1, trees.shape[0] - 1)
|
|
||||||
]
|
|
||||||
|
|
||||||
answer_1 = (highest_trees.min(axis=2) < trees).sum()
|
|
||||||
print(f"answer 1 is {answer_1}")
|
|
||||||
|
|
||||||
|
|
||||||
def viewing_distance(row_of_trees: np.ndarray, value: int) -> int:
|
|
||||||
w = np.where(row_of_trees >= value)[0]
|
|
||||||
|
|
||||||
if not w.size:
|
|
||||||
return len(row_of_trees)
|
|
||||||
|
|
||||||
return w[0] + 1
|
|
||||||
|
|
||||||
|
|
||||||
# answer 2
|
|
||||||
v_distances = np.zeros(trees.shape + (4,), dtype=int)
|
|
||||||
v_distances[1:-1, 1:-1, :] = [
|
|
||||||
[
|
|
||||||
[
|
|
||||||
viewing_distance(trees[i - 1 :: -1, j], trees[i, j]),
|
|
||||||
viewing_distance(trees[i, j - 1 :: -1], trees[i, j]),
|
|
||||||
viewing_distance(trees[i, j + 1 :], trees[i, j]),
|
|
||||||
viewing_distance(trees[i + 1 :, j], trees[i, j]),
|
|
||||||
]
|
|
||||||
for j in range(1, trees.shape[1] - 1)
|
|
||||||
]
|
|
||||||
for i in range(1, trees.shape[0] - 1)
|
|
||||||
]
|
|
||||||
answer_2 = np.prod(v_distances, axis=2).max()
|
|
||||||
print(f"answer 2 is {answer_2}")
|
|
35
README.md
35
README.md
@@ -1,7 +1,36 @@
|
|||||||
# Advent Of Code
|
# Holt59 - Advent Of Code
|
||||||
|
|
||||||
To run any script, you need to pipe the input:
|
Installation (with [`poetry`](https://python-poetry.org/)):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cat 2022/inputs/day2.txt | python 2022/day2.py
|
poetry install
|
||||||
|
```
|
||||||
|
|
||||||
|
To run any day:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
holt59-aoc $day
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use `-v` / `--verbose` for extra outputs in some case, `-t` / `--test` to run
|
||||||
|
the code on the test data (one of the test data if multiple are present) or even
|
||||||
|
`-u XXX` / `--user XXX` to run the code on a specific input after putting the input
|
||||||
|
file under `src/holt59/aoc/inputs/XXX/$year/$day`.
|
||||||
|
|
||||||
|
Full usage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
usage: Holt59 Advent-Of-Code Runner [-h] [-v] [-t] [-u USER] [-i INPUT] [-y YEAR] day
|
||||||
|
|
||||||
|
positional arguments:
|
||||||
|
day day to run
|
||||||
|
|
||||||
|
options:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-v, --verbose verbose mode
|
||||||
|
-t, --test test mode
|
||||||
|
-u USER, --user USER user input to use
|
||||||
|
-i INPUT, --input INPUT
|
||||||
|
input to use (override user and test)
|
||||||
|
-y YEAR, --year YEAR year to run
|
||||||
```
|
```
|
||||||
|
1530
poetry.lock
generated
Normal file
1530
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
52
pyproject.toml
Normal file
52
pyproject.toml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "holt59-advent-of-code"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = ["Mikael CAPELLE <capelle.mikael@gmail.com>"]
|
||||||
|
license = "MIT"
|
||||||
|
readme = "README.md"
|
||||||
|
packages = [{ include = "holt59", from = "src" }]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.10"
|
||||||
|
numpy = "^2.1.3"
|
||||||
|
tqdm = "^4.67.1"
|
||||||
|
parse = "^1.20.2"
|
||||||
|
sympy = "^1.13.3"
|
||||||
|
networkx = "^3.4.2"
|
||||||
|
pillow = "^11.0.0"
|
||||||
|
imageio = "^2.36.1"
|
||||||
|
pygifsicle = "^1.1.0"
|
||||||
|
opencv-python = "^4.10.0.84"
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
pyright = "^1.1.389"
|
||||||
|
ruff = "^0.8.1"
|
||||||
|
poethepoet = "^0.31.1"
|
||||||
|
ipykernel = "^6.29.5"
|
||||||
|
networkx-stubs = "^0.0.1"
|
||||||
|
types-networkx = "^3.4.2.20241115"
|
||||||
|
|
||||||
|
[tool.poetry.group.cplex.dependencies]
|
||||||
|
docplex = "^2.28.240"
|
||||||
|
cplex = "^22.1.1.2"
|
||||||
|
|
||||||
|
[tool.poetry.group.ortools.dependencies]
|
||||||
|
ortools = "^9.11.4210"
|
||||||
|
|
||||||
|
[tool.poetry.scripts]
|
||||||
|
holt59-aoc = "holt59.aoc.__main__:main"
|
||||||
|
|
||||||
|
[tool.poe.tasks]
|
||||||
|
format-imports = "ruff check --select I src --fix"
|
||||||
|
format-ruff = "ruff format src"
|
||||||
|
format.sequence = ["format-imports", "format-ruff"]
|
||||||
|
lint-ruff = "ruff check src"
|
||||||
|
lint-ruff-format = "ruff format --check src"
|
||||||
|
lint-pyright = "pyright src"
|
||||||
|
lint.sequence = ["lint-ruff", "lint-ruff-format", "lint-pyright"]
|
||||||
|
lint.ignore_fail = "return_non_zero"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
5
run.ps1
5
run.ps1
@@ -1,5 +0,0 @@
|
|||||||
param([switch]$Test, $day)
|
|
||||||
|
|
||||||
$folder = $Test ? "tests" : "inputs"
|
|
||||||
|
|
||||||
Get-Content ".\2022\$folder\day$day.txt" | python ".\2022\day$day.py"
|
|
12
src/holt59/aoc/2015/day1.py
Normal file
12
src/holt59/aoc/2015/day1.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
floor = 0
|
||||||
|
floors = [(floor := floor + (1 if c == "(" else -1)) for c in input]
|
||||||
|
|
||||||
|
yield floors[-1]
|
||||||
|
yield floors.index(-1)
|
147
src/holt59/aoc/2015/day10.py
Normal file
147
src/holt59/aoc/2015/day10.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import itertools
|
||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
# see http://www.se16.info/js/lands2.htm for the explanation of 'atoms' (or elements)
|
||||||
|
#
|
||||||
|
# see also https://www.youtube.com/watch?v=ea7lJkEhytA (video link from AOC) and this
|
||||||
|
# CodeGolf answer https://codegolf.stackexchange.com/a/8479/42148
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
ATOMS: list[tuple[str, tuple[int, ...]]] = [
|
||||||
|
("22", (0, )), # 0
|
||||||
|
("13112221133211322112211213322112", (71, 90, 0, 19, 2, )), # 1
|
||||||
|
("312211322212221121123222112", (1, )), # 2
|
||||||
|
("111312211312113221133211322112211213322112", (31, 19, 2, )), # 3
|
||||||
|
("1321132122211322212221121123222112", (3, )), # 4
|
||||||
|
("3113112211322112211213322112", (4, )), # 5
|
||||||
|
("111312212221121123222112", (5, )), # 6
|
||||||
|
("132112211213322112", (6, )), # 7
|
||||||
|
("31121123222112", (7, )), # 8
|
||||||
|
("111213322112", (8, )), # 9
|
||||||
|
("123222112", (9, )), # 10
|
||||||
|
("3113322112", (60, 10, )), # 11
|
||||||
|
("1113222112", (11, )), # 12
|
||||||
|
("1322112", (12, )), # 13
|
||||||
|
("311311222112", (66, 13, )), # 14
|
||||||
|
("1113122112", (14, )), # 15
|
||||||
|
("132112", (15, )), # 16
|
||||||
|
("3112", (16, )), # 17
|
||||||
|
("1112", (17, )), # 18
|
||||||
|
("12", (18, )), # 19
|
||||||
|
("3113112221133112", (66, 90, 0, 19, 26, )), # 20
|
||||||
|
("11131221131112", (20, )), # 21
|
||||||
|
("13211312", (21, )), # 22
|
||||||
|
("31132", (22, )), # 23
|
||||||
|
("111311222112", (23, 13, )), # 24
|
||||||
|
("13122112", (24, )), # 25
|
||||||
|
("32112", (25, )), # 26
|
||||||
|
("11133112", (29, 26, )), # 27
|
||||||
|
("131112", (27, )), # 28
|
||||||
|
("312", (28, )), # 29
|
||||||
|
("13221133122211332", (62, 19, 88, 0, 19, 29, )), # 30
|
||||||
|
("31131122211311122113222", (66, 30, )), # 31
|
||||||
|
("11131221131211322113322112", (31, 10, )), # 32
|
||||||
|
("13211321222113222112", (32, )), # 33
|
||||||
|
("3113112211322112", (33, )), # 34
|
||||||
|
("11131221222112", (34, )), # 35
|
||||||
|
("1321122112", (35, )), # 36
|
||||||
|
("3112112", (36, )), # 37
|
||||||
|
("1112133", (37, 91, )), # 38
|
||||||
|
("12322211331222113112211", (38, 0, 19, 42, )), # 39
|
||||||
|
("1113122113322113111221131221", (67, 39, )), # 40
|
||||||
|
("13211322211312113211", (40, )), # 41
|
||||||
|
("311322113212221", (41, )), # 42
|
||||||
|
("132211331222113112211", (62, 19, 42, )), # 43
|
||||||
|
("311311222113111221131221", (66, 43, )), # 44
|
||||||
|
("111312211312113211", (44, )), # 45
|
||||||
|
("132113212221", (45, )), # 46
|
||||||
|
("3113112211", (46, )), # 47
|
||||||
|
("11131221", (47, )), # 48
|
||||||
|
("13211", (48, )), # 49
|
||||||
|
("3112221", (60, 49, )), # 50
|
||||||
|
("1322113312211", (62, 19, 50, )), # 51
|
||||||
|
("311311222113111221", (66, 51, )), # 52
|
||||||
|
("11131221131211", (52, )), # 53
|
||||||
|
("13211321", (53, )), # 54
|
||||||
|
("311311", (54, )), # 55
|
||||||
|
("11131", (55, )), # 56
|
||||||
|
("1321133112", (56, 0, 19, 26, )), # 57
|
||||||
|
("31131112", (57, )), # 58
|
||||||
|
("111312", (58, )), # 59
|
||||||
|
("132", (59, )), # 60
|
||||||
|
("311332", (60, 19, 29, )), # 61
|
||||||
|
("1113222", (61, )), # 62
|
||||||
|
("13221133112", (62, 19, 26, )), # 63
|
||||||
|
("3113112221131112", (66, 63, )), # 64
|
||||||
|
("111312211312", (64, )), # 65
|
||||||
|
("1321132", (65, )), # 66
|
||||||
|
("311311222", (66, 60, )), # 67
|
||||||
|
("11131221133112", (67, 19, 26, )), # 68
|
||||||
|
("1321131112", (68, )), # 69
|
||||||
|
("311312", (69, )), # 70
|
||||||
|
("11132", (70, )), # 71
|
||||||
|
("13112221133211322112211213322113", (71, 90, 0, 19, 73, )), # 72
|
||||||
|
("312211322212221121123222113", (72, )), # 73
|
||||||
|
("111312211312113221133211322112211213322113", (31, 19, 73, )), # 74
|
||||||
|
("1321132122211322212221121123222113", (74, )), # 75
|
||||||
|
("3113112211322112211213322113", (75, )), # 76
|
||||||
|
("111312212221121123222113", (76, )), # 77
|
||||||
|
("132112211213322113", (77, )), # 78
|
||||||
|
("31121123222113", (78, )), # 79
|
||||||
|
("111213322113", (79, )), # 80
|
||||||
|
("123222113", (80, )), # 81
|
||||||
|
("3113322113", (60, 81, )), # 82
|
||||||
|
("1113222113", (82, )), # 83
|
||||||
|
("1322113", (83, )), # 84
|
||||||
|
("311311222113", (66, 84, )), # 85
|
||||||
|
("1113122113", (85, )), # 86
|
||||||
|
("132113", (86, )), # 87
|
||||||
|
("3113", (87, )), # 88
|
||||||
|
("1113", (88, )), # 89
|
||||||
|
("13", (89, )), # 90
|
||||||
|
("3", (90, )), # 91
|
||||||
|
]
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
STARTERS = [
|
||||||
|
"1",
|
||||||
|
"11",
|
||||||
|
"21",
|
||||||
|
"1211",
|
||||||
|
"111221",
|
||||||
|
"312211",
|
||||||
|
"13112221",
|
||||||
|
"1113213211",
|
||||||
|
"31131211131221",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def look_and_say_length(s: str, n: int) -> int:
|
||||||
|
if n == 0:
|
||||||
|
return len(s)
|
||||||
|
|
||||||
|
if s in STARTERS:
|
||||||
|
return look_and_say_length(
|
||||||
|
"".join(f"{len(list(g))}{k}" for k, g in itertools.groupby(s)), n - 1
|
||||||
|
)
|
||||||
|
|
||||||
|
counts = {i: 0 for i in range(len(ATOMS))}
|
||||||
|
idx = next(i for i, (a, _) in enumerate(ATOMS) if s == a)
|
||||||
|
counts[idx] = 1
|
||||||
|
|
||||||
|
for _ in range(n):
|
||||||
|
c2 = {i: 0 for i in range(len(ATOMS))}
|
||||||
|
for i in counts:
|
||||||
|
for j in ATOMS[i][1]:
|
||||||
|
c2[j] += counts[i]
|
||||||
|
counts = c2
|
||||||
|
|
||||||
|
return sum(counts[i] * len(a[0]) for i, a in enumerate(ATOMS))
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any] | None:
|
||||||
|
yield look_and_say_length(input, 40)
|
||||||
|
yield look_and_say_length(input, 50)
|
49
src/holt59/aoc/2015/day11.py
Normal file
49
src/holt59/aoc/2015/day11.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import itertools
|
||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid(p: str) -> bool:
|
||||||
|
if any(c in "iol" for c in p):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not any(
|
||||||
|
ord(a) + 1 == ord(b) and ord(b) + 1 == ord(c)
|
||||||
|
for a, b, c in zip(p, p[1:], p[2:])
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if sum(len(list(g)) >= 2 for _, g in itertools.groupby(p)) < 2:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
assert not is_valid("hijklmmn")
|
||||||
|
assert not is_valid("abbceffg")
|
||||||
|
assert not is_valid("abbcegjk")
|
||||||
|
assert is_valid("abcdffaa")
|
||||||
|
assert is_valid("ghjaabcc")
|
||||||
|
|
||||||
|
|
||||||
|
def increment(p: str) -> str:
|
||||||
|
if p[-1] == "z":
|
||||||
|
return increment(p[:-1]) + "a"
|
||||||
|
elif p[-1] in "iol":
|
||||||
|
return p[:-1] + chr(ord(p[-1]) + 2)
|
||||||
|
else:
|
||||||
|
return p[:-1] + chr(ord(p[-1]) + 1)
|
||||||
|
|
||||||
|
|
||||||
|
def find_next_password(p: str) -> str:
|
||||||
|
while not is_valid(p):
|
||||||
|
p = increment(p)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
answer_1 = find_next_password(input)
|
||||||
|
yield answer_1
|
||||||
|
yield find_next_password(increment(answer_1))
|
27
src/holt59/aoc/2015/day12.py
Normal file
27
src/holt59/aoc/2015/day12.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import json
|
||||||
|
from typing import Any, Iterator, TypeAlias
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
JsonObject: TypeAlias = dict[str, "JsonObject"] | list["JsonObject"] | int | str
|
||||||
|
|
||||||
|
|
||||||
|
def json_sum(value: JsonObject, ignore: str | None = None) -> int:
|
||||||
|
if isinstance(value, str):
|
||||||
|
return 0
|
||||||
|
elif isinstance(value, int):
|
||||||
|
return value
|
||||||
|
elif isinstance(value, list):
|
||||||
|
return sum(json_sum(v, ignore=ignore) for v in value)
|
||||||
|
elif ignore not in value.values():
|
||||||
|
return sum(json_sum(v, ignore=ignore) for v in value.values())
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
data: JsonObject = json.loads(input)
|
||||||
|
|
||||||
|
yield json_sum(data)
|
||||||
|
yield json_sum(data, "red")
|
40
src/holt59/aoc/2015/day13.py
Normal file
40
src/holt59/aoc/2015/day13.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import itertools
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import Any, Iterator, Literal, cast
|
||||||
|
|
||||||
|
import parse # type: ignore
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
def max_change_in_happiness(happiness: dict[str, dict[str, int]]) -> int:
|
||||||
|
guests = list(happiness)
|
||||||
|
return max(
|
||||||
|
sum(
|
||||||
|
happiness[o][d] + happiness[d][o]
|
||||||
|
for o, d in zip((guests[0],) + order, order + (guests[0],))
|
||||||
|
)
|
||||||
|
for order in map(tuple, itertools.permutations(guests[1:]))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
|
||||||
|
happiness: dict[str, dict[str, int]] = defaultdict(dict)
|
||||||
|
for line in lines:
|
||||||
|
u1, gain_or_loose, hap, u2 = cast(
|
||||||
|
tuple[str, Literal["gain", "lose"], int, str],
|
||||||
|
parse.parse( # type: ignore
|
||||||
|
"{} would {} {:d} happiness units by sitting next to {}.", line
|
||||||
|
),
|
||||||
|
)
|
||||||
|
happiness[u1][u2] = hap if gain_or_loose == "gain" else -hap
|
||||||
|
|
||||||
|
yield max_change_in_happiness(happiness)
|
||||||
|
for guest in list(happiness):
|
||||||
|
happiness["me"][guest] = 0
|
||||||
|
happiness[guest]["me"] = 0
|
||||||
|
|
||||||
|
yield max_change_in_happiness(happiness)
|
63
src/holt59/aoc/2015/day14.py
Normal file
63
src/holt59/aoc/2015/day14.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Iterator, Literal, cast
|
||||||
|
|
||||||
|
import parse # type: ignore
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Reindeer:
|
||||||
|
name: str
|
||||||
|
speed: int
|
||||||
|
fly_time: int
|
||||||
|
rest_time: int
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
|
||||||
|
reindeers: list[Reindeer] = []
|
||||||
|
for line in lines:
|
||||||
|
reindeer, speed, speed_time, rest_time = cast(
|
||||||
|
tuple[str, int, int, int],
|
||||||
|
parse.parse( # type: ignore
|
||||||
|
"{} can fly {:d} km/s for {:d} seconds, "
|
||||||
|
"but then must rest for {:d} seconds.",
|
||||||
|
line,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
reindeers.append(
|
||||||
|
Reindeer(
|
||||||
|
name=reindeer, speed=speed, fly_time=speed_time, rest_time=rest_time
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
target = 1000 if len(reindeers) <= 2 else 2503
|
||||||
|
|
||||||
|
states: dict[Reindeer, tuple[Literal["resting", "flying"], int]] = {
|
||||||
|
reindeer: ("resting", 0) for reindeer in reindeers
|
||||||
|
}
|
||||||
|
distances: dict[Reindeer, int] = {reindeer: 0 for reindeer in reindeers}
|
||||||
|
points: dict[Reindeer, int] = {reindeer: 0 for reindeer in reindeers}
|
||||||
|
|
||||||
|
for time in self.progress.wrap(range(target)):
|
||||||
|
for reindeer in reindeers:
|
||||||
|
if states[reindeer][0] == "flying":
|
||||||
|
distances[reindeer] += reindeer.speed
|
||||||
|
|
||||||
|
top_distance = max(distances.values())
|
||||||
|
for reindeer in reindeers:
|
||||||
|
if distances[reindeer] == top_distance:
|
||||||
|
points[reindeer] += 1
|
||||||
|
|
||||||
|
for reindeer in reindeers:
|
||||||
|
if states[reindeer][1] == time:
|
||||||
|
if states[reindeer][0] == "resting":
|
||||||
|
states[reindeer] = ("flying", time + reindeer.fly_time)
|
||||||
|
else:
|
||||||
|
states[reindeer] = ("resting", time + reindeer.rest_time)
|
||||||
|
|
||||||
|
yield max(distances.values())
|
||||||
|
yield max(points.values()) - 1
|
56
src/holt59/aoc/2015/day15.py
Normal file
56
src/holt59/aoc/2015/day15.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import math
|
||||||
|
from typing import Any, Iterator, Sequence, cast
|
||||||
|
|
||||||
|
import parse # type: ignore
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
def score(ingredients: list[list[int]], teaspoons: Sequence[int]) -> int:
|
||||||
|
return math.prod(
|
||||||
|
max(
|
||||||
|
0,
|
||||||
|
sum(
|
||||||
|
ingredient[prop] * teaspoon
|
||||||
|
for ingredient, teaspoon in zip(ingredients, teaspoons)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for prop in range(len(ingredients[0]) - 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
|
||||||
|
ingredients: list[list[int]] = []
|
||||||
|
for line in lines:
|
||||||
|
_, *scores = cast(
|
||||||
|
tuple[str, int, int, int, int, int],
|
||||||
|
parse.parse( # type: ignore
|
||||||
|
"{}: capacity {:d}, durability {:d}, flavor {:d}, "
|
||||||
|
"texture {:d}, calories {:d}",
|
||||||
|
line,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
ingredients.append(scores)
|
||||||
|
|
||||||
|
total_teaspoons = 100
|
||||||
|
calories: list[int] = []
|
||||||
|
scores: list[int] = []
|
||||||
|
|
||||||
|
for a in range(total_teaspoons + 1):
|
||||||
|
for b in range(total_teaspoons + 1 - a):
|
||||||
|
for c in range(total_teaspoons + 1 - a - b):
|
||||||
|
teaspoons = (a, b, c, total_teaspoons - a - b - c)
|
||||||
|
|
||||||
|
scores.append(score(ingredients, teaspoons))
|
||||||
|
calories.append(
|
||||||
|
sum(
|
||||||
|
ingredient[-1] * teaspoon
|
||||||
|
for ingredient, teaspoon in zip(ingredients, teaspoons)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
yield max(scores)
|
||||||
|
yield max(score for score, calory in zip(scores, calories) if calory == 500)
|
57
src/holt59/aoc/2015/day16.py
Normal file
57
src/holt59/aoc/2015/day16.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import operator as op
|
||||||
|
import re
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import Any, Callable, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
MFCSAM: dict[str, int] = {
|
||||||
|
"children": 3,
|
||||||
|
"cats": 7,
|
||||||
|
"samoyeds": 2,
|
||||||
|
"pomeranians": 3,
|
||||||
|
"akitas": 0,
|
||||||
|
"vizslas": 0,
|
||||||
|
"goldfish": 5,
|
||||||
|
"trees": 3,
|
||||||
|
"cars": 2,
|
||||||
|
"perfumes": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def match(
|
||||||
|
aunts: list[dict[str, int]], operators: dict[str, Callable[[int, int], bool]]
|
||||||
|
) -> int:
|
||||||
|
return next(
|
||||||
|
i
|
||||||
|
for i, aunt in enumerate(aunts, start=1)
|
||||||
|
if all(operators[k](aunt[k], MFCSAM[k]) for k in aunt)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
|
||||||
|
aunts: list[dict[str, int]] = [
|
||||||
|
{
|
||||||
|
match[1]: int(match[2])
|
||||||
|
for match in re.findall(
|
||||||
|
R"((?P<compound>[^:, ]+): (?P<quantity>\d+))", line
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for line in lines
|
||||||
|
]
|
||||||
|
|
||||||
|
yield match(aunts, defaultdict(lambda: op.eq))
|
||||||
|
|
||||||
|
yield match(
|
||||||
|
aunts,
|
||||||
|
defaultdict(
|
||||||
|
lambda: op.eq,
|
||||||
|
trees=op.gt,
|
||||||
|
cats=op.gt,
|
||||||
|
pomeranians=op.lt,
|
||||||
|
goldfish=op.lt,
|
||||||
|
),
|
||||||
|
)
|
34
src/holt59/aoc/2015/day17.py
Normal file
34
src/holt59/aoc/2015/day17.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
def iter_combinations(value: int, containers: list[int]) -> Iterator[tuple[int, ...]]:
|
||||||
|
if value < 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
if value == 0:
|
||||||
|
yield ()
|
||||||
|
|
||||||
|
for i in range(len(containers)):
|
||||||
|
for combination in iter_combinations(
|
||||||
|
value - containers[i], containers[i + 1 :]
|
||||||
|
):
|
||||||
|
yield (containers[i],) + combination
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
containers = [int(c) for c in input.split()]
|
||||||
|
total = 25 if len(containers) <= 5 else 150
|
||||||
|
|
||||||
|
combinations = [
|
||||||
|
combination for combination in iter_combinations(total, containers)
|
||||||
|
]
|
||||||
|
|
||||||
|
yield len(combinations)
|
||||||
|
|
||||||
|
min_containers = min(len(combination) for combination in combinations)
|
||||||
|
yield sum(
|
||||||
|
1 for combination in combinations if len(combination) == min_containers
|
||||||
|
)
|
66
src/holt59/aoc/2015/day18.py
Normal file
66
src/holt59/aoc/2015/day18.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import itertools
|
||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
grid0 = np.array([[c == "#" for c in line] for line in input.splitlines()])
|
||||||
|
|
||||||
|
# add an always off circle around
|
||||||
|
grid0 = np.concatenate(
|
||||||
|
[
|
||||||
|
np.zeros((grid0.shape[0] + 2, 1), dtype=bool),
|
||||||
|
np.concatenate(
|
||||||
|
[
|
||||||
|
np.zeros((1, grid0.shape[1]), dtype=bool),
|
||||||
|
grid0,
|
||||||
|
np.zeros((1, grid0.shape[1]), dtype=bool),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
np.zeros((grid0.shape[0] + 2, 1), dtype=bool),
|
||||||
|
],
|
||||||
|
axis=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
moves = list(itertools.product([-1, 0, 1], repeat=2))
|
||||||
|
moves.remove((0, 0))
|
||||||
|
|
||||||
|
jjs, iis = np.meshgrid(
|
||||||
|
np.arange(1, grid0.shape[0] - 1, dtype=int),
|
||||||
|
np.arange(1, grid0.shape[1] - 1, dtype=int),
|
||||||
|
)
|
||||||
|
iis, jjs = iis.flatten(), jjs.flatten()
|
||||||
|
|
||||||
|
ins = iis[:, None] + np.array(moves)[:, 0]
|
||||||
|
jns = jjs[:, None] + np.array(moves)[:, 1]
|
||||||
|
|
||||||
|
def game_of_life(grid: NDArray[np.bool_]) -> NDArray[np.bool_]:
|
||||||
|
neighbors_on = grid[ins, jns].sum(axis=1)
|
||||||
|
cells_on = grid[iis, jjs]
|
||||||
|
|
||||||
|
grid = np.zeros_like(grid)
|
||||||
|
grid[iis, jjs] = (neighbors_on == 3) | (cells_on & (neighbors_on == 2))
|
||||||
|
|
||||||
|
return grid
|
||||||
|
|
||||||
|
grid = grid0
|
||||||
|
n_steps = 4 if len(grid) < 10 else 100
|
||||||
|
for _ in range(n_steps):
|
||||||
|
grid = game_of_life(grid)
|
||||||
|
|
||||||
|
yield grid.sum()
|
||||||
|
|
||||||
|
n_steps = 5 if len(grid) < 10 else 100
|
||||||
|
grid = grid0
|
||||||
|
for _ in range(n_steps):
|
||||||
|
grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True
|
||||||
|
grid = game_of_life(grid)
|
||||||
|
|
||||||
|
grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True
|
||||||
|
|
||||||
|
yield sum(cell for line in grid for cell in line)
|
57
src/holt59/aoc/2015/day19.py
Normal file
57
src/holt59/aoc/2015/day19.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
replacements_s, molecule = input.split("\n\n")
|
||||||
|
|
||||||
|
REPLACEMENTS: dict[str, list[str]] = defaultdict(list)
|
||||||
|
for replacement_s in replacements_s.splitlines():
|
||||||
|
p = replacement_s.split(" => ")
|
||||||
|
REPLACEMENTS[p[0]].append(p[1])
|
||||||
|
molecule = molecule.strip()
|
||||||
|
|
||||||
|
generated = [
|
||||||
|
molecule[:i] + replacement + molecule[i + len(symbol) :]
|
||||||
|
for symbol, replacements in REPLACEMENTS.items()
|
||||||
|
for replacement in replacements
|
||||||
|
for i in range(len(molecule))
|
||||||
|
if molecule[i:].startswith(symbol)
|
||||||
|
]
|
||||||
|
|
||||||
|
yield len(set(generated))
|
||||||
|
|
||||||
|
inversion: dict[str, str] = {
|
||||||
|
replacement: symbol
|
||||||
|
for symbol, replacements in REPLACEMENTS.items()
|
||||||
|
for replacement in replacements
|
||||||
|
}
|
||||||
|
|
||||||
|
# there is actually only one way to create the molecule, and we can greedily replace
|
||||||
|
# tokens with their replacements, e.g., if H => OH then we can replace OH by H directly
|
||||||
|
# without thinking
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
while molecule != "e":
|
||||||
|
i = 0
|
||||||
|
m2 = ""
|
||||||
|
while i < len(molecule):
|
||||||
|
found = False
|
||||||
|
for replacement in inversion:
|
||||||
|
if molecule[i:].startswith(replacement):
|
||||||
|
m2 += inversion[replacement]
|
||||||
|
i += len(replacement)
|
||||||
|
count += 1
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
m2 += molecule[i]
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
molecule = m2
|
||||||
|
|
||||||
|
yield count
|
24
src/holt59/aoc/2015/day2.py
Normal file
24
src/holt59/aoc/2015/day2.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
length, width, height = np.array(
|
||||||
|
[[int(c) for c in line.split("x")] for line in input.splitlines()]
|
||||||
|
).T
|
||||||
|
|
||||||
|
lw, wh, hl = (length * width, width * height, height * length)
|
||||||
|
|
||||||
|
yield np.sum(2 * (lw + wh + hl) + np.min(np.stack([lw, wh, hl]), axis=0))
|
||||||
|
|
||||||
|
yield np.sum(
|
||||||
|
length * width * height
|
||||||
|
+ 2
|
||||||
|
* np.min(
|
||||||
|
np.stack([length + width, length + height, height + width]), axis=0
|
||||||
|
)
|
||||||
|
)
|
29
src/holt59/aoc/2015/day20.py
Normal file
29
src/holt59/aoc/2015/day20.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import itertools
|
||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
def presents(n: int, elf: int, max: int) -> int:
|
||||||
|
count = 0
|
||||||
|
k = 1
|
||||||
|
while k * k < n:
|
||||||
|
if n % k == 0:
|
||||||
|
if n // k <= max:
|
||||||
|
count += elf * k
|
||||||
|
if k <= max:
|
||||||
|
count += elf * (n // k)
|
||||||
|
k += 1
|
||||||
|
|
||||||
|
if k * k == n and k <= max:
|
||||||
|
count += elf * k
|
||||||
|
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
target = int(input)
|
||||||
|
|
||||||
|
yield next(n for n in itertools.count(1) if presents(n, 10, target) >= target)
|
||||||
|
yield next(n for n in itertools.count(1) if presents(n, 11, 50) >= target)
|
64
src/holt59/aoc/2015/day21.py
Normal file
64
src/holt59/aoc/2015/day21.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import itertools
|
||||||
|
from math import ceil
|
||||||
|
from typing import Any, Iterator, TypeAlias
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
Modifier: TypeAlias = tuple[str, int, int, int]
|
||||||
|
|
||||||
|
WEAPONS: list[Modifier] = [
|
||||||
|
("Dagger", 8, 4, 0),
|
||||||
|
("Shortsword", 10, 5, 0),
|
||||||
|
("Warhammer", 25, 6, 0),
|
||||||
|
("Longsword", 40, 7, 0),
|
||||||
|
("Greataxe", 74, 8, 0),
|
||||||
|
]
|
||||||
|
|
||||||
|
ARMORS: list[Modifier] = [
|
||||||
|
("", 0, 0, 0),
|
||||||
|
("Leather", 13, 0, 1),
|
||||||
|
("Chainmail", 31, 0, 2),
|
||||||
|
("Splintmail", 53, 0, 3),
|
||||||
|
("Bandedmail", 75, 0, 4),
|
||||||
|
("Platemail", 102, 0, 5),
|
||||||
|
]
|
||||||
|
|
||||||
|
RINGS: list[Modifier] = [
|
||||||
|
("", 0, 0, 0),
|
||||||
|
("Damage +1", 25, 1, 0),
|
||||||
|
("Damage +2", 50, 2, 0),
|
||||||
|
("Damage +3", 100, 3, 0),
|
||||||
|
("Defense +1", 20, 0, 1),
|
||||||
|
("Defense +2", 40, 0, 2),
|
||||||
|
("Defense +3", 80, 0, 3),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
|
||||||
|
player_hp = 100
|
||||||
|
|
||||||
|
boss_attack = int(lines[1].split(":")[1].strip())
|
||||||
|
boss_armor = int(lines[2].split(":")[1].strip())
|
||||||
|
boss_hp = int(lines[0].split(":")[1].strip())
|
||||||
|
|
||||||
|
min_cost, max_cost = 1_000_000, 0
|
||||||
|
for equipments in itertools.product(WEAPONS, ARMORS, RINGS, RINGS):
|
||||||
|
if equipments[-1][0] != "" and equipments[-2] == equipments[-1]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
cost, player_attack, player_armor = (
|
||||||
|
sum(equipment[1:][k] for equipment in equipments) for k in range(3)
|
||||||
|
)
|
||||||
|
|
||||||
|
if ceil(boss_hp / max(1, player_attack - boss_armor)) <= ceil(
|
||||||
|
player_hp / max(1, boss_attack - player_armor)
|
||||||
|
):
|
||||||
|
min_cost = min(cost, min_cost)
|
||||||
|
else:
|
||||||
|
max_cost = max(cost, max_cost)
|
||||||
|
|
||||||
|
yield min_cost
|
||||||
|
yield max_cost
|
181
src/holt59/aoc/2015/day22.py
Normal file
181
src/holt59/aoc/2015/day22.py
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import heapq
|
||||||
|
from typing import Any, Iterator, Literal, TypeAlias, cast
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
PlayerType: TypeAlias = Literal["player", "boss"]
|
||||||
|
SpellType: TypeAlias = Literal["magic missile", "drain", "shield", "poison", "recharge"]
|
||||||
|
BuffType: TypeAlias = Literal["shield", "poison", "recharge"]
|
||||||
|
Node: TypeAlias = tuple[
|
||||||
|
PlayerType,
|
||||||
|
int,
|
||||||
|
int,
|
||||||
|
int,
|
||||||
|
int,
|
||||||
|
int,
|
||||||
|
tuple[tuple[BuffType, int], ...],
|
||||||
|
tuple[tuple[SpellType, int], ...],
|
||||||
|
]
|
||||||
|
|
||||||
|
ATTACK_SPELLS: list[tuple[SpellType, int, int, int]] = [
|
||||||
|
("magic missile", 53, 4, 0),
|
||||||
|
("drain", 73, 2, 2),
|
||||||
|
]
|
||||||
|
BUFF_SPELLS: list[tuple[BuffType, int, int]] = [
|
||||||
|
("shield", 113, 6),
|
||||||
|
("poison", 173, 6),
|
||||||
|
("recharge", 229, 5),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def play(
|
||||||
|
player_hp: int,
|
||||||
|
player_mana: int,
|
||||||
|
player_armor: int,
|
||||||
|
boss_hp: int,
|
||||||
|
boss_attack: int,
|
||||||
|
hard_mode: bool,
|
||||||
|
) -> tuple[tuple[SpellType, int], ...]:
|
||||||
|
winning_node: tuple[tuple[SpellType, int], ...] | None = None
|
||||||
|
|
||||||
|
visited: set[
|
||||||
|
tuple[PlayerType, int, int, int, int, tuple[tuple[BuffType, int], ...]]
|
||||||
|
] = set()
|
||||||
|
nodes: list[Node] = [
|
||||||
|
("player", 0, player_hp, player_mana, player_armor, boss_hp, (), ())
|
||||||
|
]
|
||||||
|
|
||||||
|
while winning_node is None:
|
||||||
|
(
|
||||||
|
player,
|
||||||
|
mana,
|
||||||
|
player_hp,
|
||||||
|
player_mana,
|
||||||
|
player_armor,
|
||||||
|
boss_hp,
|
||||||
|
buffs,
|
||||||
|
spells,
|
||||||
|
) = heapq.heappop(nodes)
|
||||||
|
|
||||||
|
if (player, player_hp, player_mana, player_armor, boss_hp, buffs) in visited:
|
||||||
|
continue
|
||||||
|
|
||||||
|
visited.add((player, player_hp, player_mana, player_armor, boss_hp, buffs))
|
||||||
|
new_buffs: list[tuple[BuffType, int]] = []
|
||||||
|
for buff, length in buffs:
|
||||||
|
length = length - 1
|
||||||
|
match buff:
|
||||||
|
case "poison":
|
||||||
|
boss_hp = max(boss_hp - 3, 0)
|
||||||
|
case "shield":
|
||||||
|
if length == 0:
|
||||||
|
player_armor -= 7
|
||||||
|
case "recharge":
|
||||||
|
player_mana += 101
|
||||||
|
|
||||||
|
if length > 0:
|
||||||
|
new_buffs.append((buff, length))
|
||||||
|
|
||||||
|
if hard_mode and player == "player":
|
||||||
|
player_hp = player_hp - 1
|
||||||
|
|
||||||
|
if player_hp <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if boss_hp <= 0:
|
||||||
|
winning_node = spells
|
||||||
|
continue
|
||||||
|
|
||||||
|
buffs = tuple(new_buffs)
|
||||||
|
|
||||||
|
if player == "boss":
|
||||||
|
heapq.heappush(
|
||||||
|
nodes,
|
||||||
|
(
|
||||||
|
"player",
|
||||||
|
mana,
|
||||||
|
max(0, player_hp - max(boss_attack - player_armor, 1)),
|
||||||
|
player_mana,
|
||||||
|
player_armor,
|
||||||
|
boss_hp,
|
||||||
|
buffs,
|
||||||
|
spells,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
buff_types = {b for b, _ in buffs}
|
||||||
|
|
||||||
|
for spell, cost, damage, regeneration in ATTACK_SPELLS:
|
||||||
|
if player_mana < cost:
|
||||||
|
continue
|
||||||
|
|
||||||
|
heapq.heappush(
|
||||||
|
nodes,
|
||||||
|
(
|
||||||
|
"boss",
|
||||||
|
mana + cost,
|
||||||
|
player_hp + regeneration,
|
||||||
|
player_mana - cost,
|
||||||
|
player_armor,
|
||||||
|
max(0, boss_hp - damage),
|
||||||
|
buffs,
|
||||||
|
spells + cast("tuple[tuple[SpellType, int]]", ((spell, cost),)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
for buff_type, buff_cost, buff_length in BUFF_SPELLS:
|
||||||
|
if buff_type in buff_types:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if player_mana < buff_cost:
|
||||||
|
continue
|
||||||
|
|
||||||
|
heapq.heappush(
|
||||||
|
nodes,
|
||||||
|
(
|
||||||
|
"boss",
|
||||||
|
mana + buff_cost,
|
||||||
|
player_hp,
|
||||||
|
player_mana - buff_cost,
|
||||||
|
player_armor + 7 * (buff_type == "shield"),
|
||||||
|
boss_hp,
|
||||||
|
buffs
|
||||||
|
+ cast(
|
||||||
|
"tuple[tuple[BuffType, int]]", ((buff_type, buff_length),)
|
||||||
|
),
|
||||||
|
spells
|
||||||
|
+ cast(
|
||||||
|
"tuple[tuple[SpellType, int]]", ((buff_type, buff_cost),)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return winning_node
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
|
||||||
|
player_hp = 50
|
||||||
|
player_mana = 500
|
||||||
|
player_armor = 0
|
||||||
|
|
||||||
|
boss_hp = int(lines[0].split(":")[1].strip())
|
||||||
|
boss_attack = int(lines[1].split(":")[1].strip())
|
||||||
|
|
||||||
|
yield sum(
|
||||||
|
c
|
||||||
|
for _, c in play(
|
||||||
|
player_hp, player_mana, player_armor, boss_hp, boss_attack, False
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
yield sum(
|
||||||
|
c
|
||||||
|
for _, c in play(
|
||||||
|
player_hp, player_mana, player_armor, boss_hp, boss_attack, True
|
||||||
|
)
|
||||||
|
)
|
107
src/holt59/aoc/2015/day23.py
Normal file
107
src/holt59/aoc/2015/day23.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import inspect
|
||||||
|
from typing import Any, Callable, Final, Iterator, Mapping
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Instruction:
|
||||||
|
def __init__(self, fn: Callable[..., None]):
|
||||||
|
self._fn = fn
|
||||||
|
|
||||||
|
args = inspect.getfullargspec(fn)
|
||||||
|
|
||||||
|
self._argtypes = [args.annotations[arg] for arg in args.args[1:]]
|
||||||
|
|
||||||
|
def __call__(self, args: tuple[str, ...]):
|
||||||
|
self._fn(
|
||||||
|
*(argtype(arg) for arg, argtype in zip(args, self._argtypes, strict=True))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Machine:
|
||||||
|
def __init__(
|
||||||
|
self, instructions: list[str], registers: dict[str, int] = {"a": 0, "b": 1}
|
||||||
|
):
|
||||||
|
self.instructions: Final = [
|
||||||
|
(part[0], tuple(arg.strip() for arg in " ".join(part[1:]).split(",")))
|
||||||
|
for instruction in instructions
|
||||||
|
if (part := instruction.split())
|
||||||
|
]
|
||||||
|
|
||||||
|
self._fns = {
|
||||||
|
name: Instruction(getattr(self, name))
|
||||||
|
for name in ("hlf", "tpl", "inc", "jmp", "jie", "jio")
|
||||||
|
}
|
||||||
|
|
||||||
|
self._registers = registers.copy()
|
||||||
|
self._ip = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def registers(self) -> Mapping[str, int]:
|
||||||
|
return self._registers
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ip(self) -> int:
|
||||||
|
return self._ip
|
||||||
|
|
||||||
|
def reset(self, registers: dict[str, int] = {"a": 0, "b": 0}):
|
||||||
|
self._registers = registers.copy()
|
||||||
|
self._ip = 0
|
||||||
|
|
||||||
|
def hlf(self, register: str):
|
||||||
|
self._registers[register] //= 2
|
||||||
|
self._ip += 1
|
||||||
|
|
||||||
|
def tpl(self, register: str):
|
||||||
|
self._registers[register] *= 3
|
||||||
|
self._ip += 1
|
||||||
|
|
||||||
|
def inc(self, register: str):
|
||||||
|
self._registers[register] += 1
|
||||||
|
self._ip += 1
|
||||||
|
|
||||||
|
def jmp(self, offset: int):
|
||||||
|
self._ip += offset
|
||||||
|
assert 0 <= self._ip < len(self.instructions)
|
||||||
|
|
||||||
|
def jie(self, register: str, offset: int):
|
||||||
|
if self._registers[register] % 2 == 0:
|
||||||
|
self._ip += offset
|
||||||
|
else:
|
||||||
|
self._ip += 1
|
||||||
|
|
||||||
|
def jio(self, register: str, offset: int):
|
||||||
|
if self._registers[register] == 1:
|
||||||
|
self._ip += offset
|
||||||
|
else:
|
||||||
|
self._ip += 1
|
||||||
|
|
||||||
|
def _exec(self) -> bool:
|
||||||
|
# execute next instruction
|
||||||
|
if self._ip >= len(self.instructions):
|
||||||
|
return False
|
||||||
|
|
||||||
|
ins, args = self.instructions[self._ip]
|
||||||
|
|
||||||
|
if ins not in self._fns:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._fns[ins](args)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while self._exec():
|
||||||
|
...
|
||||||
|
return self.registers
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
machine = Machine(input.splitlines())
|
||||||
|
|
||||||
|
registers = machine.run()
|
||||||
|
yield registers["b"]
|
||||||
|
|
||||||
|
machine.reset({"a": 1, "b": 0})
|
||||||
|
registers = machine.run()
|
||||||
|
yield registers["b"]
|
88
src/holt59/aoc/2015/day24.py
Normal file
88
src/holt59/aoc/2015/day24.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
from typing import Any, Iterator, TypeAlias
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
TupleOfInts: TypeAlias = tuple[int, ...]
|
||||||
|
|
||||||
|
|
||||||
|
def check_n_groups(
|
||||||
|
target: int, groups: tuple[TupleOfInts, ...], numbers: TupleOfInts
|
||||||
|
) -> bool:
|
||||||
|
n_groups = len(groups)
|
||||||
|
groups_s = tuple(sum(group) for group in groups)
|
||||||
|
|
||||||
|
if all(target == group_s for group_s in groups_s):
|
||||||
|
return not numbers
|
||||||
|
|
||||||
|
if not numbers:
|
||||||
|
return False
|
||||||
|
|
||||||
|
head, *tail_l = numbers
|
||||||
|
tail, tail_s = tuple(tail_l), sum(tail_l)
|
||||||
|
|
||||||
|
return any(
|
||||||
|
groups_s[i] + head <= target
|
||||||
|
and sum(groups_s[j] for j in range(len(groups)) if i != j) + tail_s
|
||||||
|
>= (n_groups - 1) * target
|
||||||
|
and check_n_groups(
|
||||||
|
target, groups[:i] + ((groups[i] + (head,)),) + groups[i + 1 :], tail
|
||||||
|
)
|
||||||
|
for i in range(len(groups))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def enumerate_single_subset(
|
||||||
|
target: int, numbers: TupleOfInts
|
||||||
|
) -> Iterator[tuple[int, TupleOfInts, TupleOfInts]]:
|
||||||
|
"""
|
||||||
|
Enumerate subset of numbers whose sum equals target.
|
||||||
|
|
||||||
|
Subset are enumerated in increasing order of length, then product (quantum value).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target: Target for the sum of the subset.
|
||||||
|
numbers: Tuple of integers to find the subset from.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A generator (quantum, subset, remaining) where subset if the subset of numbers
|
||||||
|
whose sum equals target, quantum the product of the subset, and remaining the
|
||||||
|
remaining numbers.
|
||||||
|
"""
|
||||||
|
groups: list[tuple[int, TupleOfInts, TupleOfInts]] = [(1, (), numbers)]
|
||||||
|
|
||||||
|
for _ in range(len(numbers)):
|
||||||
|
new_groups: list[tuple[int, TupleOfInts, TupleOfInts]] = []
|
||||||
|
|
||||||
|
for g_quantum, group, remaining in groups:
|
||||||
|
sg = sum(group)
|
||||||
|
for i in range(len(remaining)):
|
||||||
|
if group and remaining[i] <= group[-1]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
uv = remaining[:i] + remaining[i + 1 :]
|
||||||
|
kv = g_quantum * remaining[i], group + (remaining[i],), uv
|
||||||
|
|
||||||
|
if sg + remaining[i] == target:
|
||||||
|
yield kv
|
||||||
|
elif sg + remaining[i] < target:
|
||||||
|
new_groups.append(kv)
|
||||||
|
|
||||||
|
groups = new_groups
|
||||||
|
|
||||||
|
|
||||||
|
def find_min_quantum(numbers: tuple[int, ...], n_groups: int):
|
||||||
|
return next(
|
||||||
|
g_quantum
|
||||||
|
for g_quantum, group_1v2, group_234v2 in enumerate_single_subset(
|
||||||
|
sum(numbers) // n_groups, numbers
|
||||||
|
)
|
||||||
|
if check_n_groups(sum(group_1v2), ((),) * (n_groups - 1), group_234v2)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
numbers = tuple(map(int, input.split()))
|
||||||
|
|
||||||
|
yield find_min_quantum(numbers, 3)
|
||||||
|
yield find_min_quantum(numbers, 4)
|
16
src/holt59/aoc/2015/day25.py
Normal file
16
src/holt59/aoc/2015/day25.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import re
|
||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
from ..tools.math import pow_mod
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
m = re.search(r"row\s*([0-9]+)\s*,\s*column\s*([0-9]+)", input)
|
||||||
|
assert m is not None
|
||||||
|
|
||||||
|
row, col = int(m.group(1)), int(m.group(2))
|
||||||
|
n = (row * (row - 1)) // 2 + col * (col + 1) // 2 + (row - 1) * (col - 1)
|
||||||
|
|
||||||
|
yield (20151125 * pow_mod(252533, n - 1, 33554393)) % 33554393
|
33
src/holt59/aoc/2015/day3.py
Normal file
33
src/holt59/aoc/2015/day3.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
def process(directions: str) -> dict[tuple[int, int], int]:
|
||||||
|
counts: dict[tuple[int, int], int] = defaultdict(lambda: 0)
|
||||||
|
counts[0, 0] = 1
|
||||||
|
x, y = (0, 0)
|
||||||
|
|
||||||
|
for c in directions:
|
||||||
|
match c:
|
||||||
|
case ">":
|
||||||
|
x += 1
|
||||||
|
case "<":
|
||||||
|
x -= 1
|
||||||
|
case "^":
|
||||||
|
y -= 1
|
||||||
|
case "v":
|
||||||
|
y += 1
|
||||||
|
case _:
|
||||||
|
raise ValueError()
|
||||||
|
|
||||||
|
counts[x, y] += 1
|
||||||
|
|
||||||
|
return counts
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
yield len(process(input))
|
||||||
|
yield len(process(input[::2]) | process(input[1::2]))
|
20
src/holt59/aoc/2015/day4.py
Normal file
20
src/holt59/aoc/2015/day4.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import hashlib
|
||||||
|
import itertools
|
||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
it = iter(itertools.count(1))
|
||||||
|
yield next(
|
||||||
|
i
|
||||||
|
for i in it
|
||||||
|
if hashlib.md5(f"{input}{i}".encode()).hexdigest().startswith("00000")
|
||||||
|
)
|
||||||
|
yield next(
|
||||||
|
i
|
||||||
|
for i in it
|
||||||
|
if hashlib.md5(f"{input}{i}".encode()).hexdigest().startswith("000000")
|
||||||
|
)
|
36
src/holt59/aoc/2015/day5.py
Normal file
36
src/holt59/aoc/2015/day5.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
VOWELS = "aeiou"
|
||||||
|
FORBIDDEN = {"ab", "cd", "pq", "xy"}
|
||||||
|
|
||||||
|
|
||||||
|
def is_nice_1(s: str) -> bool:
|
||||||
|
if sum(c in VOWELS for c in s) < 3:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not any(a == b for a, b in zip(s[:-1:], s[1::])):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if any(s.find(f) >= 0 for f in FORBIDDEN):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def is_nice_2(s: str) -> bool:
|
||||||
|
if not any(s.find(s[i : i + 2], i + 2) >= 0 for i in range(len(s))):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not any(a == b for a, b in zip(s[:-1:], s[2::])):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
yield sum(map(is_nice_1, lines))
|
||||||
|
yield sum(map(is_nice_2, lines))
|
32
src/holt59/aoc/2015/day6.py
Normal file
32
src/holt59/aoc/2015/day6.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from typing import Any, Iterator, Literal, cast
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import parse # type: ignore
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lights_1 = np.zeros((1000, 1000), dtype=bool)
|
||||||
|
lights_2 = np.zeros((1000, 1000), dtype=int)
|
||||||
|
for line in input.splitlines():
|
||||||
|
action, sx, sy, ex, ey = cast(
|
||||||
|
tuple[Literal["turn on", "turn off", "toggle"], int, int, int, int],
|
||||||
|
parse.parse("{} {:d},{:d} through {:d},{:d}", line), # type: ignore
|
||||||
|
)
|
||||||
|
ex, ey = ex + 1, ey + 1
|
||||||
|
|
||||||
|
match action:
|
||||||
|
case "turn on":
|
||||||
|
lights_1[sx:ex, sy:ey] = True
|
||||||
|
lights_2[sx:ex, sy:ey] += 1
|
||||||
|
case "turn off":
|
||||||
|
lights_1[sx:ex, sy:ey] = False
|
||||||
|
lights_2[sx:ex, sy:ey] = np.maximum(lights_2[sx:ex, sy:ey] - 1, 0)
|
||||||
|
case "toggle":
|
||||||
|
lights_1[sx:ex, sy:ey] = ~lights_1[sx:ex, sy:ey]
|
||||||
|
lights_2[sx:ex, sy:ey] += 2
|
||||||
|
|
||||||
|
yield lights_1.sum()
|
||||||
|
yield lights_2.sum()
|
96
src/holt59/aoc/2015/day7.py
Normal file
96
src/holt59/aoc/2015/day7.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import operator
|
||||||
|
from typing import Any, Callable, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
OPERATORS = {
|
||||||
|
"AND": operator.and_,
|
||||||
|
"OR": operator.or_,
|
||||||
|
"LSHIFT": operator.lshift,
|
||||||
|
"RSHIFT": operator.rshift,
|
||||||
|
}
|
||||||
|
|
||||||
|
ValueGetter = Callable[[dict[str, int]], int]
|
||||||
|
Signals = dict[
|
||||||
|
str,
|
||||||
|
tuple[
|
||||||
|
tuple[str, str],
|
||||||
|
tuple[ValueGetter, ValueGetter],
|
||||||
|
Callable[[int, int], int],
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def zero_op(_a: int, _b: int) -> int:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def value_of(key: str) -> tuple[str, Callable[[dict[str, int]], int]]:
|
||||||
|
try:
|
||||||
|
return "", lambda _p, _v=int(key): _v
|
||||||
|
except ValueError:
|
||||||
|
return key, lambda values: values[key]
|
||||||
|
|
||||||
|
|
||||||
|
def process(
|
||||||
|
signals: Signals,
|
||||||
|
values: dict[str, int],
|
||||||
|
) -> dict[str, int]:
|
||||||
|
while signals:
|
||||||
|
signal = next(s for s in signals if all(p in values for p in signals[s][0]))
|
||||||
|
_, deps, command = signals[signal]
|
||||||
|
values[signal] = command(deps[0](values), deps[1](values)) % 65536
|
||||||
|
del signals[signal]
|
||||||
|
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any] | None:
|
||||||
|
lines = input.splitlines()
|
||||||
|
|
||||||
|
signals: Signals = {}
|
||||||
|
values: dict[str, int] = {"": 0}
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
command, signal = line.split(" -> ")
|
||||||
|
|
||||||
|
if command.startswith("NOT"):
|
||||||
|
name = command.split(" ")[1]
|
||||||
|
signals[signal] = (
|
||||||
|
(name, ""),
|
||||||
|
(lambda values, _n=name: values[_n], lambda _v: 0),
|
||||||
|
lambda a, _b: ~a,
|
||||||
|
)
|
||||||
|
|
||||||
|
elif not any(command.find(name) >= 0 for name in OPERATORS):
|
||||||
|
try:
|
||||||
|
values[signal] = int(command)
|
||||||
|
except ValueError:
|
||||||
|
signals[signal] = (
|
||||||
|
(command, ""),
|
||||||
|
(lambda values, _c=command: values[_c], lambda _v: 0),
|
||||||
|
lambda a, _b: a,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
op: Callable[[int, int], int] = zero_op
|
||||||
|
lhs_s, rhs_s = "", ""
|
||||||
|
|
||||||
|
for name in OPERATORS:
|
||||||
|
if command.find(name) >= 0:
|
||||||
|
op = OPERATORS[name]
|
||||||
|
lhs_s, rhs_s = command.split(f" {name} ")
|
||||||
|
break
|
||||||
|
|
||||||
|
lhs_s, lhs_fn = value_of(lhs_s)
|
||||||
|
rhs_s, rhs_fn = value_of(rhs_s)
|
||||||
|
|
||||||
|
signals[signal] = ((lhs_s, rhs_s), (lhs_fn, rhs_fn), op)
|
||||||
|
|
||||||
|
values_1 = process(signals.copy(), values.copy())
|
||||||
|
for k in sorted(values_1):
|
||||||
|
self.logger.info(f"{k}: {values_1[k]}")
|
||||||
|
yield values_1["a"]
|
||||||
|
|
||||||
|
yield process(signals.copy(), values | {"b": values_1["a"]})["a"]
|
32
src/holt59/aoc/2015/day8.py
Normal file
32
src/holt59/aoc/2015/day8.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
|
||||||
|
yield sum(
|
||||||
|
# left and right quotes (not in memory)
|
||||||
|
2
|
||||||
|
# each \\ adds one character in the literals (compared to memory)
|
||||||
|
+ line.count(R"\\")
|
||||||
|
# each \" adds one character in the literals (compared to memory)
|
||||||
|
+ line[1:-1].count(R"\"")
|
||||||
|
# each \xFF adds 3 characters in the literals (compared to memory), but we must not
|
||||||
|
# count A\\x (A != \), but we must count A\\\x (A != \) - in practice we should also
|
||||||
|
# avoid \\\\x, etc., but this does not occur in the examples and the actual input
|
||||||
|
+ 3 * (line.count(R"\x") - line.count(R"\\x") + line.count(R"\\\x"))
|
||||||
|
for line in lines
|
||||||
|
)
|
||||||
|
|
||||||
|
yield sum(
|
||||||
|
# needs to wrap in quotes (2 characters)
|
||||||
|
2
|
||||||
|
# needs to escape every \ with an extra \
|
||||||
|
+ line.count("\\")
|
||||||
|
# needs to escape every " with an extra \ (including the first and last ones)
|
||||||
|
+ line.count('"')
|
||||||
|
for line in lines
|
||||||
|
)
|
28
src/holt59/aoc/2015/day9.py
Normal file
28
src/holt59/aoc/2015/day9.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import itertools
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import Any, Iterator, cast
|
||||||
|
|
||||||
|
import parse # type: ignore
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
|
||||||
|
distances: dict[str, dict[str, int]] = defaultdict(dict)
|
||||||
|
for line in lines:
|
||||||
|
origin, destination, length = cast(
|
||||||
|
tuple[str, str, int],
|
||||||
|
parse.parse("{} to {} = {:d}", line), # type: ignore
|
||||||
|
)
|
||||||
|
distances[origin][destination] = distances[destination][origin] = length
|
||||||
|
|
||||||
|
distance_of_routes = {
|
||||||
|
route: sum(distances[o][d] for o, d in zip(route[:-1], route[1:]))
|
||||||
|
for route in map(tuple, itertools.permutations(distances))
|
||||||
|
}
|
||||||
|
|
||||||
|
yield min(distance_of_routes.values())
|
||||||
|
yield max(distance_of_routes.values())
|
17
src/holt59/aoc/2021/day1.py
Normal file
17
src/holt59/aoc/2021/day1.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
|
||||||
|
values = [int(line) for line in lines]
|
||||||
|
|
||||||
|
# part 1
|
||||||
|
yield sum(v2 > v1 for v1, v2 in zip(values[:-1], values[1:]))
|
||||||
|
|
||||||
|
# part 2
|
||||||
|
runnings = [sum(values[i : i + 3]) for i in range(len(values) - 2)]
|
||||||
|
yield sum(v2 > v1 for v1, v2 in zip(runnings[:-1], runnings[1:]))
|
47
src/holt59/aoc/2021/day10.py
Normal file
47
src/holt59/aoc/2021/day10.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from functools import reduce
|
||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
BRACKETS = {"{": "}", "[": "]", "<": ">", "(": ")"}
|
||||||
|
|
||||||
|
CORRUPT_SCORES = {")": 3, "]": 57, "}": 1197, ">": 25137}
|
||||||
|
COMPLETE_SCORES = {")": 1, "]": 2, "}": 3, ">": 4}
|
||||||
|
|
||||||
|
|
||||||
|
def corrupted_or_incomplete(line: str) -> tuple[bool, str]:
|
||||||
|
opens: list[str] = []
|
||||||
|
|
||||||
|
for c in line:
|
||||||
|
if c in BRACKETS:
|
||||||
|
opens.append(c)
|
||||||
|
elif BRACKETS[opens[-1]] != c:
|
||||||
|
return True, c
|
||||||
|
else:
|
||||||
|
opens.pop()
|
||||||
|
|
||||||
|
return (False, "".join(opens))
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
|
||||||
|
answer_1: int = 0
|
||||||
|
incomplete_scores: list[int] = []
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
c, r = corrupted_or_incomplete(line)
|
||||||
|
if c:
|
||||||
|
answer_1 += CORRUPT_SCORES[r]
|
||||||
|
else:
|
||||||
|
incomplete_scores.append(
|
||||||
|
reduce(
|
||||||
|
lambda s, c: s * 5 + COMPLETE_SCORES[BRACKETS[c]],
|
||||||
|
reversed(r),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
yield answer_1
|
||||||
|
yield sorted(incomplete_scores)[len(incomplete_scores) // 2]
|
66
src/holt59/aoc/2021/day11.py
Normal file
66
src/holt59/aoc/2021/day11.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import itertools as it
|
||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
def do_step(values: list[list[int]]) -> tuple[list[list[int]], set[tuple[int, int]]]:
|
||||||
|
values = [[c + 1 for c in r] for r in values]
|
||||||
|
flashed: set[tuple[int, int]] = set()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
found = False
|
||||||
|
|
||||||
|
for i_row, row in enumerate(values):
|
||||||
|
for i_col, col in enumerate(row):
|
||||||
|
if col <= 9 or (i_row, i_col) in flashed:
|
||||||
|
continue
|
||||||
|
|
||||||
|
found = True
|
||||||
|
flashed.add((i_row, i_col))
|
||||||
|
for dr, dc in it.product((-1, 0, 1), repeat=2):
|
||||||
|
if 0 <= i_row + dr < len(values) and 0 <= i_col + dc < len(
|
||||||
|
values[0]
|
||||||
|
):
|
||||||
|
values[i_row + dr][i_col + dc] += 1
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
break
|
||||||
|
|
||||||
|
for i, j in flashed:
|
||||||
|
values[i][j] = 0
|
||||||
|
|
||||||
|
return values, flashed
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def print_grid(self, values: list[list[int]], flashed: set[tuple[int, int]]):
|
||||||
|
for i_row, row in enumerate(values):
|
||||||
|
s_row = ""
|
||||||
|
for i_col, col in enumerate(row):
|
||||||
|
if (i_row, i_col) in flashed:
|
||||||
|
s_row += f"\033[0;31m{col}\033[0;00m"
|
||||||
|
else:
|
||||||
|
s_row += str(col)
|
||||||
|
self.logger.info(s_row)
|
||||||
|
self.logger.info("")
|
||||||
|
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
values_0 = [[int(c) for c in r] for r in input.splitlines()]
|
||||||
|
|
||||||
|
values = values_0
|
||||||
|
total_flashed: int = 0
|
||||||
|
for _ in range(100):
|
||||||
|
values, flashed = do_step(values)
|
||||||
|
total_flashed += len(flashed)
|
||||||
|
|
||||||
|
yield total_flashed
|
||||||
|
|
||||||
|
n_cells = len(values) * len(values[0])
|
||||||
|
flashed: set[tuple[int, int]] = set()
|
||||||
|
values, step = values_0, 0
|
||||||
|
while len(flashed) != n_cells:
|
||||||
|
values, flashed = do_step(values)
|
||||||
|
step += 1
|
||||||
|
|
||||||
|
yield step
|
64
src/holt59/aoc/2021/day12.py
Normal file
64
src/holt59/aoc/2021/day12.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import string
|
||||||
|
from collections import defaultdict
|
||||||
|
from functools import cache
|
||||||
|
from typing import Any, Iterator, Mapping, Sequence
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
@cache
|
||||||
|
def is_small(node: str):
|
||||||
|
return all(c in string.ascii_lowercase for c in node)
|
||||||
|
|
||||||
|
|
||||||
|
def enumerate_paths(
|
||||||
|
neighbors: Mapping[str, Sequence[str]],
|
||||||
|
duplicate_smalls: int = 0,
|
||||||
|
start: str = "start",
|
||||||
|
current: tuple[str, ...] = ("start",),
|
||||||
|
) -> Iterator[tuple[str, ...]]:
|
||||||
|
if start == "end":
|
||||||
|
yield current
|
||||||
|
|
||||||
|
for neighbor in neighbors[start]:
|
||||||
|
if not is_small(neighbor):
|
||||||
|
yield from enumerate_paths(
|
||||||
|
neighbors, duplicate_smalls, neighbor, current + (neighbor,)
|
||||||
|
)
|
||||||
|
elif neighbor not in current:
|
||||||
|
yield from enumerate_paths(
|
||||||
|
neighbors, duplicate_smalls, neighbor, current + (neighbor,)
|
||||||
|
)
|
||||||
|
elif duplicate_smalls > 0:
|
||||||
|
yield from enumerate_paths(
|
||||||
|
neighbors, duplicate_smalls - 1, neighbor, current + (neighbor,)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
neighbors: dict[str, list[str]] = defaultdict(list)
|
||||||
|
|
||||||
|
for row in input.splitlines():
|
||||||
|
a, b = row.split("-")
|
||||||
|
if a != "end" and b != "start":
|
||||||
|
neighbors[a].append(b)
|
||||||
|
if b != "end" and a != "start":
|
||||||
|
neighbors[b].append(a)
|
||||||
|
|
||||||
|
if self.files:
|
||||||
|
graph = "graph {\n"
|
||||||
|
for node, neighbors_of in neighbors.items():
|
||||||
|
graph += (
|
||||||
|
" ".join(
|
||||||
|
f"{node} -- {neighbor};"
|
||||||
|
for neighbor in neighbors_of
|
||||||
|
if node <= neighbor or node == "start" or neighbor == "end"
|
||||||
|
)
|
||||||
|
+ "\n"
|
||||||
|
)
|
||||||
|
graph += "}\n"
|
||||||
|
self.files.create("graph.dot", graph.encode(), False)
|
||||||
|
|
||||||
|
yield len(list(enumerate_paths(neighbors)))
|
||||||
|
yield len(list(enumerate_paths(neighbors, 1)))
|
7
src/holt59/aoc/2021/day13.py
Normal file
7
src/holt59/aoc/2021/day13.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]: ...
|
7
src/holt59/aoc/2021/day14.py
Normal file
7
src/holt59/aoc/2021/day14.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]: ...
|
7
src/holt59/aoc/2021/day15.py
Normal file
7
src/holt59/aoc/2021/day15.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]: ...
|
7
src/holt59/aoc/2021/day16.py
Normal file
7
src/holt59/aoc/2021/day16.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]: ...
|
7
src/holt59/aoc/2021/day17.py
Normal file
7
src/holt59/aoc/2021/day17.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]: ...
|
7
src/holt59/aoc/2021/day18.py
Normal file
7
src/holt59/aoc/2021/day18.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]: ...
|
7
src/holt59/aoc/2021/day19.py
Normal file
7
src/holt59/aoc/2021/day19.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]: ...
|
38
src/holt59/aoc/2021/day2.py
Normal file
38
src/holt59/aoc/2021/day2.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from math import prod
|
||||||
|
from typing import Any, Iterator, Literal, TypeAlias, cast
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
Command: TypeAlias = Literal["forward", "up", "down"]
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
|
||||||
|
commands: list[tuple[Command, int]] = [
|
||||||
|
(cast(Command, (p := line.split())[0]), int(p[1])) for line in lines
|
||||||
|
]
|
||||||
|
|
||||||
|
def depth_and_position(use_aim: bool):
|
||||||
|
aim, pos, depth = 0, 0, 0
|
||||||
|
for command, value in commands:
|
||||||
|
d_depth = 0
|
||||||
|
match command:
|
||||||
|
case "forward":
|
||||||
|
pos += value
|
||||||
|
depth += value * aim
|
||||||
|
case "up":
|
||||||
|
d_depth = -value
|
||||||
|
case "down":
|
||||||
|
d_depth = value
|
||||||
|
|
||||||
|
if use_aim:
|
||||||
|
aim += d_depth
|
||||||
|
else:
|
||||||
|
depth += value
|
||||||
|
|
||||||
|
return depth, pos
|
||||||
|
|
||||||
|
yield prod(depth_and_position(False))
|
||||||
|
yield prod(depth_and_position(True))
|
7
src/holt59/aoc/2021/day20.py
Normal file
7
src/holt59/aoc/2021/day20.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]: ...
|
7
src/holt59/aoc/2021/day21.py
Normal file
7
src/holt59/aoc/2021/day21.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]: ...
|
7
src/holt59/aoc/2021/day22.py
Normal file
7
src/holt59/aoc/2021/day22.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]: ...
|
7
src/holt59/aoc/2021/day23.py
Normal file
7
src/holt59/aoc/2021/day23.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]: ...
|
7
src/holt59/aoc/2021/day24.py
Normal file
7
src/holt59/aoc/2021/day24.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]: ...
|
7
src/holt59/aoc/2021/day25.py
Normal file
7
src/holt59/aoc/2021/day25.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]: ...
|
43
src/holt59/aoc/2021/day3.py
Normal file
43
src/holt59/aoc/2021/day3.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from collections import Counter
|
||||||
|
from typing import Any, Iterator, Literal
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
def generator_rating(
|
||||||
|
values: list[str], most_common: bool, default: Literal["0", "1"]
|
||||||
|
) -> str:
|
||||||
|
index = 0
|
||||||
|
most_common_idx = 0 if most_common else 1
|
||||||
|
|
||||||
|
while len(values) > 1:
|
||||||
|
cnt = Counter(value[index] for value in values)
|
||||||
|
bit = cnt.most_common(2)[most_common_idx][0]
|
||||||
|
if cnt["0"] == cnt["1"]:
|
||||||
|
bit = default
|
||||||
|
values = [value for value in values if value[index] == bit]
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
return values[0]
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
|
||||||
|
# part 1
|
||||||
|
most_and_least_common = [
|
||||||
|
tuple(
|
||||||
|
Counter(line[col] for line in lines).most_common(2)[m][0]
|
||||||
|
for m in range(2)
|
||||||
|
)
|
||||||
|
for col in range(len(lines[0]))
|
||||||
|
]
|
||||||
|
gamma_rate = int("".join(most for most, _ in most_and_least_common), base=2)
|
||||||
|
epsilon_rate = int("".join(least for _, least in most_and_least_common), base=2)
|
||||||
|
yield gamma_rate * epsilon_rate
|
||||||
|
|
||||||
|
# part 2
|
||||||
|
oxygen_generator_rating = int(generator_rating(lines, True, "1"), base=2)
|
||||||
|
co2_scrubber_rating = int(generator_rating(lines, False, "0"), base=2)
|
||||||
|
yield oxygen_generator_rating * co2_scrubber_rating
|
52
src/holt59/aoc/2021/day4.py
Normal file
52
src/holt59/aoc/2021/day4.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
|
||||||
|
numbers = [int(c) for c in lines[0].split(",")]
|
||||||
|
|
||||||
|
boards = np.asarray(
|
||||||
|
[
|
||||||
|
[[int(c) for c in line.split()] for line in lines[start : start + 5]]
|
||||||
|
for start in range(2, len(lines), 6)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# (round, score) for each board (-1 when not found)
|
||||||
|
winning_rounds: list[tuple[int, int]] = [(-1, -1) for _ in range(len(boards))]
|
||||||
|
marked = np.zeros_like(boards, dtype=bool)
|
||||||
|
|
||||||
|
for round, number in enumerate(numbers):
|
||||||
|
# mark boards
|
||||||
|
marked[boards == number] = True
|
||||||
|
|
||||||
|
# check each board for winning
|
||||||
|
for index in range(len(boards)):
|
||||||
|
if winning_rounds[index][0] > 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if np.any(
|
||||||
|
np.all(marked[index], axis=0) | np.all(marked[index], axis=1)
|
||||||
|
):
|
||||||
|
winning_rounds[index] = (
|
||||||
|
round,
|
||||||
|
number * int(np.sum(boards[index][~marked[index]])),
|
||||||
|
)
|
||||||
|
|
||||||
|
# all boards are winning - break
|
||||||
|
if np.all(marked.all(axis=1) | marked.all(axis=2)):
|
||||||
|
break
|
||||||
|
|
||||||
|
# part 1
|
||||||
|
(_, score) = min(winning_rounds, key=lambda w: w[0])
|
||||||
|
yield score
|
||||||
|
|
||||||
|
# part 2
|
||||||
|
(_, score) = max(winning_rounds, key=lambda w: w[0])
|
||||||
|
yield score
|
48
src/holt59/aoc/2021/day5.py
Normal file
48
src/holt59/aoc/2021/day5.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
|
||||||
|
sections: list[tuple[tuple[int, int], tuple[int, int]]] = [
|
||||||
|
(
|
||||||
|
(
|
||||||
|
int(line.split(" -> ")[0].split(",")[0]),
|
||||||
|
int(line.split(" -> ")[0].split(",")[1]),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
int(line.split(" -> ")[1].split(",")[0]),
|
||||||
|
int(line.split(" -> ")[1].split(",")[1]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for line in lines
|
||||||
|
]
|
||||||
|
|
||||||
|
np_sections = np.array(sections).reshape(-1, 4)
|
||||||
|
|
||||||
|
x_max, y_max = (
|
||||||
|
max(np_sections[:, 0].max(), np_sections[:, 2].max()),
|
||||||
|
max(np_sections[:, 1].max(), np_sections[:, 3].max()),
|
||||||
|
)
|
||||||
|
|
||||||
|
counts_1 = np.zeros((y_max + 1, x_max + 1), dtype=int)
|
||||||
|
counts_2 = counts_1.copy()
|
||||||
|
|
||||||
|
for (x1, y1), (x2, y2) in sections:
|
||||||
|
x_rng = range(x1, x2 + 1, 1) if x2 >= x1 else range(x1, x2 - 1, -1)
|
||||||
|
y_rng = range(y1, y2 + 1, 1) if y2 >= y1 else range(y1, y2 - 1, -1)
|
||||||
|
|
||||||
|
if x1 == x2 or y1 == y2:
|
||||||
|
counts_1[list(y_rng), list(x_rng)] += 1
|
||||||
|
counts_2[list(y_rng), list(x_rng)] += 1
|
||||||
|
elif abs(x2 - x1) == abs(y2 - y1):
|
||||||
|
for i, j in zip(y_rng, x_rng):
|
||||||
|
counts_2[i, j] += 1
|
||||||
|
|
||||||
|
yield (counts_1 >= 2).sum()
|
||||||
|
yield (counts_2 >= 2).sum()
|
21
src/holt59/aoc/2021/day6.py
Normal file
21
src/holt59/aoc/2021/day6.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
values = [int(c) for c in input.split(",")]
|
||||||
|
|
||||||
|
days = 256
|
||||||
|
lanterns = {day: 0 for day in range(days)}
|
||||||
|
for value in values:
|
||||||
|
for day in range(value, days, 7):
|
||||||
|
lanterns[day] += 1
|
||||||
|
|
||||||
|
for day in range(days):
|
||||||
|
for day2 in range(day + 9, days, 7):
|
||||||
|
lanterns[day2] += lanterns[day]
|
||||||
|
|
||||||
|
yield sum(v for k, v in lanterns.items() if k < 80) + len(values)
|
||||||
|
yield sum(lanterns.values()) + len(values)
|
22
src/holt59/aoc/2021/day7.py
Normal file
22
src/holt59/aoc/2021/day7.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
positions = [int(c) for c in input.split(",")]
|
||||||
|
|
||||||
|
min_position, max_position = min(positions), max(positions)
|
||||||
|
|
||||||
|
# part 1
|
||||||
|
yield min(
|
||||||
|
sum(abs(p - position) for p in positions)
|
||||||
|
for position in range(min_position, max_position + 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
# part 2
|
||||||
|
yield min(
|
||||||
|
sum(abs(p - position) * (abs(p - position) + 1) // 2 for p in positions)
|
||||||
|
for position in range(min_position, max_position + 1)
|
||||||
|
)
|
89
src/holt59/aoc/2021/day8.py
Normal file
89
src/holt59/aoc/2021/day8.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import itertools
|
||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
digits = {
|
||||||
|
"abcefg": 0,
|
||||||
|
"cf": 1,
|
||||||
|
"acdeg": 2,
|
||||||
|
"acdfg": 3,
|
||||||
|
"bcdf": 4,
|
||||||
|
"abdfg": 5,
|
||||||
|
"abdefg": 6,
|
||||||
|
"acf": 7,
|
||||||
|
"abcdefg": 8,
|
||||||
|
"abcdfg": 9,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
|
||||||
|
# part 1
|
||||||
|
lengths = {len(k) for k, v in digits.items() if v in (1, 4, 7, 8)}
|
||||||
|
yield sum(
|
||||||
|
len(p) in lengths
|
||||||
|
for line in lines
|
||||||
|
for p in line.split("|")[1].strip().split()
|
||||||
|
)
|
||||||
|
|
||||||
|
# part 2
|
||||||
|
values: list[int] = []
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
parts = line.split("|")
|
||||||
|
broken_digits = sorted(parts[0].strip().split(), key=len)
|
||||||
|
|
||||||
|
per_length = {
|
||||||
|
k: list(v)
|
||||||
|
for k, v in itertools.groupby(sorted(broken_digits, key=len), key=len)
|
||||||
|
}
|
||||||
|
|
||||||
|
# a can be found immediately
|
||||||
|
a = next(u for u in per_length[3][0] if u not in per_length[2][0])
|
||||||
|
|
||||||
|
# c and f have only two possible values corresponding to the single entry of
|
||||||
|
# length 2
|
||||||
|
cf = list(per_length[2][0])
|
||||||
|
|
||||||
|
# the only digit of length 4 contains bcdf, so we can deduce bd by removing cf
|
||||||
|
bd = [u for u in per_length[4][0] if u not in cf]
|
||||||
|
|
||||||
|
# the 3 digits of length 5 have a, d and g in common
|
||||||
|
adg = [
|
||||||
|
u for u in per_length[5][0] if all(u in pe for pe in per_length[5][1:])
|
||||||
|
]
|
||||||
|
|
||||||
|
# we can remove a
|
||||||
|
dg = [u for u in adg if u != a]
|
||||||
|
|
||||||
|
# we can deduce d and g
|
||||||
|
d = next(u for u in dg if u in bd)
|
||||||
|
g = next(u for u in dg if u != d)
|
||||||
|
|
||||||
|
# then b
|
||||||
|
b = next(u for u in bd if u != d)
|
||||||
|
|
||||||
|
# f is in the three 6-length digits, while c is only in 2
|
||||||
|
f = next(u for u in cf if all(u in p for p in per_length[6]))
|
||||||
|
|
||||||
|
# c is not f
|
||||||
|
c = next(u for u in cf if u != f)
|
||||||
|
|
||||||
|
# e is the last one
|
||||||
|
e = next(u for u in "abcdefg" if u not in {a, b, c, d, f, g})
|
||||||
|
|
||||||
|
mapping = dict(zip((a, b, c, d, e, f, g), "abcdefg"))
|
||||||
|
|
||||||
|
value = 0
|
||||||
|
for number in parts[1].strip().split():
|
||||||
|
digit = "".join(sorted(mapping[c] for c in number))
|
||||||
|
value = 10 * value + digits[digit]
|
||||||
|
|
||||||
|
self.logger.info(f"value for '{line}' is {value}")
|
||||||
|
|
||||||
|
values.append(value)
|
||||||
|
|
||||||
|
yield sum(values)
|
47
src/holt59/aoc/2021/day9.py
Normal file
47
src/holt59/aoc/2021/day9.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from math import prod
|
||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
def neighbors(point: tuple[int, int], n_rows: int, n_cols: int):
|
||||||
|
i, j = point
|
||||||
|
for di, dj in ((-1, 0), (+1, 0), (0, -1), (0, +1)):
|
||||||
|
if 0 <= i + di < n_rows and 0 <= j + dj < n_cols:
|
||||||
|
yield (i + di, j + dj)
|
||||||
|
|
||||||
|
|
||||||
|
def basin(values: list[list[int]], start: tuple[int, int]) -> set[tuple[int, int]]:
|
||||||
|
n_rows, n_cols = len(values), len(values[0])
|
||||||
|
visited: set[tuple[int, int]] = set()
|
||||||
|
queue = [start]
|
||||||
|
|
||||||
|
while queue:
|
||||||
|
i, j = queue.pop()
|
||||||
|
|
||||||
|
if (i, j) in visited or values[i][j] == 9:
|
||||||
|
continue
|
||||||
|
|
||||||
|
visited.add((i, j))
|
||||||
|
queue.extend(neighbors((i, j), n_rows, n_cols))
|
||||||
|
|
||||||
|
return visited
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
values = [[int(c) for c in row] for row in input.splitlines()]
|
||||||
|
n_rows, n_cols = len(values), len(values[0])
|
||||||
|
|
||||||
|
low_points = [
|
||||||
|
(i, j)
|
||||||
|
for i in range(n_rows)
|
||||||
|
for j in range(n_cols)
|
||||||
|
if all(
|
||||||
|
values[ti][tj] > values[i][j]
|
||||||
|
for ti, tj in neighbors((i, j), n_rows, n_cols)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
yield sum(values[i][j] + 1 for i, j in low_points)
|
||||||
|
yield prod(sorted(len(basin(values, point)) for point in low_points)[-3:])
|
12
src/holt59/aoc/2022/day1.py
Normal file
12
src/holt59/aoc/2022/day1.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
blocks = input.split("\n\n")
|
||||||
|
values = sorted(sum(map(int, block.split())) for block in blocks)
|
||||||
|
|
||||||
|
yield values[-1]
|
||||||
|
yield sum(values[-3:])
|
43
src/holt59/aoc/2022/day10.py
Normal file
43
src/holt59/aoc/2022/day10.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = [line.strip() for line in input.splitlines()]
|
||||||
|
|
||||||
|
cycle, x = 1, 1
|
||||||
|
values = {cycle: x}
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
cycle += 1
|
||||||
|
|
||||||
|
if line == "noop":
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
r = int(line.split()[1])
|
||||||
|
|
||||||
|
values[cycle] = x
|
||||||
|
|
||||||
|
cycle += 1
|
||||||
|
x += r
|
||||||
|
|
||||||
|
values[cycle] = x
|
||||||
|
|
||||||
|
answer_1 = sum(c * values[c] for c in range(20, max(values.keys()) + 1, 40))
|
||||||
|
yield answer_1
|
||||||
|
|
||||||
|
yield (
|
||||||
|
"\n"
|
||||||
|
+ "\n".join(
|
||||||
|
"".join(
|
||||||
|
"#"
|
||||||
|
if j >= (v := values[1 + i * 40 + j]) - 1 and j <= v + 1
|
||||||
|
else "."
|
||||||
|
for j in range(40)
|
||||||
|
)
|
||||||
|
for i in range(6)
|
||||||
|
)
|
||||||
|
+ "\n"
|
||||||
|
)
|
@@ -1,13 +1,11 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import sys
|
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from typing import Callable, Final, Mapping, Sequence
|
from typing import Any, Callable, Final, Iterator, Mapping, Sequence
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
class Monkey:
|
class Monkey:
|
||||||
|
|
||||||
id: Final[int]
|
id: Final[int]
|
||||||
items: Final[Sequence[int]]
|
items: Final[Sequence[int]]
|
||||||
worry_fn: Final[Callable[[int], int]]
|
worry_fn: Final[Callable[[int], int]]
|
||||||
@@ -97,8 +95,7 @@ def run(
|
|||||||
# number of inspects
|
# number of inspects
|
||||||
inspects = {monkey: 0 for monkey in monkeys}
|
inspects = {monkey: 0 for monkey in monkeys}
|
||||||
|
|
||||||
for round in range(n_rounds):
|
for _ in range(n_rounds):
|
||||||
|
|
||||||
for monkey in monkeys:
|
for monkey in monkeys:
|
||||||
for item in items[monkey]:
|
for item in items[monkey]:
|
||||||
inspects[monkey] += 1
|
inspects[monkey] += 1
|
||||||
@@ -123,13 +120,14 @@ def monkey_business(inspects: dict[Monkey, int]) -> int:
|
|||||||
return sorted_levels[-2] * sorted_levels[-1]
|
return sorted_levels[-2] * sorted_levels[-1]
|
||||||
|
|
||||||
|
|
||||||
monkeys = [parse_monkey(block.splitlines()) for block in sys.stdin.read().split("\n\n")]
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
monkeys = [parse_monkey(block.splitlines()) for block in input.split("\n\n")]
|
||||||
|
|
||||||
# case 1: we simply divide the worry by 3 after applying the monkey worry operation
|
# case 1: we simply divide the worry by 3 after applying the monkey worry operation
|
||||||
answer_1 = monkey_business(
|
yield monkey_business(
|
||||||
run(copy.deepcopy(monkeys), 20, me_worry_fn=lambda w: w // 3)
|
run(copy.deepcopy(monkeys), 20, me_worry_fn=lambda w: w // 3)
|
||||||
)
|
)
|
||||||
print(f"answer 1 is {answer_1}")
|
|
||||||
|
|
||||||
# case 2: to keep reasonable level values, we can use a modulo operation, we need to
|
# case 2: to keep reasonable level values, we can use a modulo operation, we need to
|
||||||
# use the product of all "divisible by" test so that the test remains valid
|
# use the product of all "divisible by" test so that the test remains valid
|
||||||
@@ -140,7 +138,10 @@ print(f"answer 1 is {answer_1}")
|
|||||||
# we use the product of all test value
|
# we use the product of all test value
|
||||||
#
|
#
|
||||||
total_test_value = reduce(lambda w, m: w * m.test_value, monkeys, 1)
|
total_test_value = reduce(lambda w, m: w * m.test_value, monkeys, 1)
|
||||||
answer_2 = monkey_business(
|
yield monkey_business(
|
||||||
run(copy.deepcopy(monkeys), 10_000, me_worry_fn=lambda w: w % total_test_value)
|
run(
|
||||||
|
copy.deepcopy(monkeys),
|
||||||
|
10_000,
|
||||||
|
me_worry_fn=lambda w: w % total_test_value,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
print(f"answer 2 is {answer_2}")
|
|
176
src/holt59/aoc/2022/day12.py
Normal file
176
src/holt59/aoc/2022/day12.py
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import heapq
|
||||||
|
from typing import Any, Callable, Iterator, TypeVar
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
Node = TypeVar("Node")
|
||||||
|
|
||||||
|
|
||||||
|
def dijkstra(
|
||||||
|
start: Node,
|
||||||
|
neighbors: Callable[[Node], Iterator[Node]],
|
||||||
|
cost: Callable[[Node, Node], float],
|
||||||
|
) -> tuple[dict[Node, float], dict[Node, Node]]:
|
||||||
|
"""
|
||||||
|
Compute shortest paths from one node to all reachable ones.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
start: Starting node.
|
||||||
|
neighbors: Function returning the neighbors of a node.
|
||||||
|
cost: Function to compute the cost of an edge.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A tuple (lengths, parents) where lengths is a mapping from Node to distance
|
||||||
|
(from the starting node) and parents a mapping from parents Node (in the
|
||||||
|
shortest path). If keyset of lengths and parents is the same. If a Node is not
|
||||||
|
in the mapping, it cannot be reached from the starting node.
|
||||||
|
"""
|
||||||
|
|
||||||
|
queue: list[tuple[float, Node]] = []
|
||||||
|
|
||||||
|
visited: set[Node] = set()
|
||||||
|
lengths: dict[Node, float] = {start: 0}
|
||||||
|
parents: dict[Node, Node] = {}
|
||||||
|
|
||||||
|
heapq.heappush(queue, (0, start))
|
||||||
|
|
||||||
|
while queue:
|
||||||
|
length, current = heapq.heappop(queue)
|
||||||
|
|
||||||
|
if current in visited:
|
||||||
|
continue
|
||||||
|
|
||||||
|
visited.add(current)
|
||||||
|
|
||||||
|
for neighbor in neighbors(current):
|
||||||
|
if neighbor in visited:
|
||||||
|
continue
|
||||||
|
|
||||||
|
neighbor_cost = length + cost(current, neighbor)
|
||||||
|
|
||||||
|
if neighbor_cost < lengths.get(neighbor, float("inf")):
|
||||||
|
lengths[neighbor] = neighbor_cost
|
||||||
|
parents[neighbor] = current
|
||||||
|
|
||||||
|
heapq.heappush(queue, (neighbor_cost, neighbor))
|
||||||
|
|
||||||
|
return lengths, parents
|
||||||
|
|
||||||
|
|
||||||
|
def make_path(parents: dict[Node, Node], start: Node, end: Node) -> list[Node] | None:
|
||||||
|
if end not in parents:
|
||||||
|
return None
|
||||||
|
|
||||||
|
path: list[Node] = [end]
|
||||||
|
|
||||||
|
while path[-1] is not start:
|
||||||
|
path.append(parents[path[-1]])
|
||||||
|
|
||||||
|
return list(reversed(path))
|
||||||
|
|
||||||
|
|
||||||
|
def neighbors(
|
||||||
|
grid: list[list[int]], node: tuple[int, int], up: bool
|
||||||
|
) -> Iterator[tuple[int, int]]:
|
||||||
|
n_rows = len(grid)
|
||||||
|
n_cols = len(grid[0])
|
||||||
|
|
||||||
|
c_row, c_col = node
|
||||||
|
for n_row, n_col in (
|
||||||
|
(c_row - 1, c_col),
|
||||||
|
(c_row + 1, c_col),
|
||||||
|
(c_row, c_col - 1),
|
||||||
|
(c_row, c_col + 1),
|
||||||
|
):
|
||||||
|
if not (n_row >= 0 and n_row < n_rows and n_col >= 0 and n_col < n_cols):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if up and grid[n_row][n_col] > grid[c_row][c_col] + 1:
|
||||||
|
continue
|
||||||
|
elif not up and grid[n_row][n_col] < grid[c_row][c_col] - 1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
yield n_row, n_col
|
||||||
|
|
||||||
|
|
||||||
|
# === main code ===
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def print_path(
|
||||||
|
self, name: str, path: list[tuple[int, int]], n_rows: int, n_cols: int
|
||||||
|
) -> None:
|
||||||
|
if not self.files:
|
||||||
|
return
|
||||||
|
|
||||||
|
end = path[-1]
|
||||||
|
|
||||||
|
graph = [["." for _c in range(n_cols)] for _r in range(n_rows)]
|
||||||
|
graph[end[0]][end[1]] = "E"
|
||||||
|
|
||||||
|
for i in range(0, len(path) - 1):
|
||||||
|
cr, cc = path[i]
|
||||||
|
nr, nc = path[i + 1]
|
||||||
|
|
||||||
|
if cr == nr and nc == cc - 1:
|
||||||
|
graph[cr][cc] = "<"
|
||||||
|
elif cr == nr and nc == cc + 1:
|
||||||
|
graph[cr][cc] = ">"
|
||||||
|
elif cr == nr - 1 and nc == cc:
|
||||||
|
graph[cr][cc] = "v"
|
||||||
|
elif cr == nr + 1 and nc == cc:
|
||||||
|
graph[cr][cc] = "^"
|
||||||
|
else:
|
||||||
|
assert False, "{} -> {} infeasible".format(path[i], path[i + 1])
|
||||||
|
|
||||||
|
self.files.create(
|
||||||
|
f"graph_{name}.txt",
|
||||||
|
"\n".join("".join(row) for row in graph).encode(),
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
|
||||||
|
grid = [[ord(cell) - ord("a") for cell in line] for line in lines]
|
||||||
|
|
||||||
|
start: tuple[int, int] | None = None
|
||||||
|
end: tuple[int, int] | None = None
|
||||||
|
|
||||||
|
# for part 2
|
||||||
|
start_s: list[tuple[int, int]] = []
|
||||||
|
|
||||||
|
for i_row, row in enumerate(grid):
|
||||||
|
for i_col, col in enumerate(row):
|
||||||
|
if chr(col + ord("a")) == "S":
|
||||||
|
start = (i_row, i_col)
|
||||||
|
start_s.append(start)
|
||||||
|
elif chr(col + ord("a")) == "E":
|
||||||
|
end = (i_row, i_col)
|
||||||
|
elif col == 0:
|
||||||
|
start_s.append((i_row, i_col))
|
||||||
|
|
||||||
|
assert start is not None
|
||||||
|
assert end is not None
|
||||||
|
|
||||||
|
# fix values
|
||||||
|
grid[start[0]][start[1]] = 0
|
||||||
|
grid[end[0]][end[1]] = ord("z") - ord("a")
|
||||||
|
|
||||||
|
lengths_1, parents_1 = dijkstra(
|
||||||
|
start=start,
|
||||||
|
neighbors=lambda n: neighbors(grid, n, True),
|
||||||
|
cost=lambda lhs, rhs: 1,
|
||||||
|
)
|
||||||
|
path_1 = make_path(parents_1, start, end)
|
||||||
|
assert path_1 is not None
|
||||||
|
|
||||||
|
self.print_path("answer1", path_1, n_rows=len(grid), n_cols=len(grid[0]))
|
||||||
|
yield lengths_1[end] - 1
|
||||||
|
|
||||||
|
lengths_2, _ = dijkstra(
|
||||||
|
start=end,
|
||||||
|
neighbors=lambda n: neighbors(grid, n, False),
|
||||||
|
cost=lambda lhs, rhs: 1,
|
||||||
|
)
|
||||||
|
yield min(lengths_2.get(start, float("inf")) for start in start_s)
|
42
src/holt59/aoc/2022/day13.py
Normal file
42
src/holt59/aoc/2022/day13.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import json
|
||||||
|
from functools import cmp_to_key
|
||||||
|
from typing import Any, Iterator, TypeAlias, cast
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
Packet: TypeAlias = list[int | list["Packet"]]
|
||||||
|
|
||||||
|
|
||||||
|
def compare(lhs: Packet, rhs: Packet) -> int:
|
||||||
|
for lhs_a, rhs_a in zip(lhs, rhs):
|
||||||
|
if isinstance(lhs_a, int) and isinstance(rhs_a, int):
|
||||||
|
if lhs_a != rhs_a:
|
||||||
|
return rhs_a - lhs_a
|
||||||
|
else:
|
||||||
|
if not isinstance(lhs_a, list):
|
||||||
|
lhs_a = [lhs_a] # type: ignore
|
||||||
|
elif not isinstance(rhs_a, list):
|
||||||
|
rhs_a = [rhs_a] # type: ignore
|
||||||
|
assert isinstance(rhs_a, list) and isinstance(lhs_a, list)
|
||||||
|
r = compare(cast(Packet, lhs_a), cast(Packet, rhs_a))
|
||||||
|
if r != 0:
|
||||||
|
return r
|
||||||
|
|
||||||
|
return len(rhs) - len(lhs)
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
blocks = input.split("\n\n")
|
||||||
|
pairs = [tuple(json.loads(p) for p in block.split("\n")) for block in blocks]
|
||||||
|
|
||||||
|
yield sum(i + 1 for i, (lhs, rhs) in enumerate(pairs) if compare(lhs, rhs) > 0)
|
||||||
|
|
||||||
|
dividers = [[[2]], [[6]]]
|
||||||
|
|
||||||
|
packets = [packet for packets in pairs for packet in packets]
|
||||||
|
packets.extend(dividers)
|
||||||
|
packets = list(reversed(sorted(packets, key=cmp_to_key(compare))))
|
||||||
|
|
||||||
|
d_index = [packets.index(d) + 1 for d in dividers]
|
||||||
|
yield d_index[0] * d_index[1]
|
146
src/holt59/aoc/2022/day14.py
Normal file
146
src/holt59/aoc/2022/day14.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
from enum import Enum, auto
|
||||||
|
from typing import Any, Callable, Iterator, cast
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Cell(Enum):
|
||||||
|
AIR = auto()
|
||||||
|
ROCK = auto()
|
||||||
|
SAND = auto()
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return {Cell.AIR: ".", Cell.ROCK: "#", Cell.SAND: "O"}[self]
|
||||||
|
|
||||||
|
|
||||||
|
def flow(
|
||||||
|
blocks: dict[tuple[int, int], Cell],
|
||||||
|
stop_fn: Callable[[int, int], bool],
|
||||||
|
fill_fn: Callable[[int, int], Cell],
|
||||||
|
) -> dict[tuple[int, int], Cell]:
|
||||||
|
"""
|
||||||
|
Flow sands onto the given set of blocks
|
||||||
|
|
||||||
|
Args:
|
||||||
|
blocks: Blocks containing ROCK position. Modified in-place.
|
||||||
|
stop_fn: Function called with the last (assumed) position of a grain of
|
||||||
|
sand BEFORE adding it to blocks. If the function returns True, the grain
|
||||||
|
is added and a new one is flowed, otherwise, the whole procedure stops
|
||||||
|
and the function returns (without adding the final grain).
|
||||||
|
fill_fn: Function called when the target position of a grain (during the
|
||||||
|
flowing process) is missing from blocks.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The input blocks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
y_max = max(y for _, y in blocks)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
x, y = 500, 0
|
||||||
|
|
||||||
|
while y <= y_max:
|
||||||
|
moved = False
|
||||||
|
for cx, cy in ((x, y + 1), (x - 1, y + 1), (x + 1, y + 1)):
|
||||||
|
if (cx, cy) not in blocks and fill_fn(cx, cy) == Cell.AIR:
|
||||||
|
x, y = cx, cy
|
||||||
|
moved = True
|
||||||
|
elif blocks[cx, cy] == Cell.AIR:
|
||||||
|
x, y = cx, cy
|
||||||
|
moved = True
|
||||||
|
|
||||||
|
if moved:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not moved:
|
||||||
|
break
|
||||||
|
|
||||||
|
if stop_fn(x, y):
|
||||||
|
break
|
||||||
|
|
||||||
|
blocks[x, y] = Cell.SAND
|
||||||
|
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
|
||||||
|
# === inputs ===
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def print_blocks(self, name: str, blocks: dict[tuple[int, int], Cell]):
|
||||||
|
"""
|
||||||
|
Print the given set of blocks on a grid.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
blocks: Set of blocks to print.
|
||||||
|
"""
|
||||||
|
if not self.files:
|
||||||
|
return
|
||||||
|
|
||||||
|
x_min, y_min, x_max, y_max = (
|
||||||
|
min(x for x, _ in blocks),
|
||||||
|
0,
|
||||||
|
max(x for x, _ in blocks),
|
||||||
|
max(y for _, y in blocks),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.files.create(
|
||||||
|
f"blocks_{name}.txt",
|
||||||
|
"\n".join(
|
||||||
|
"".join(
|
||||||
|
str(blocks.get((x, y), Cell.AIR)) for x in range(x_min, x_max + 1)
|
||||||
|
)
|
||||||
|
for y in range(y_min, y_max + 1)
|
||||||
|
).encode(),
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = [line.strip() for line in input.splitlines()]
|
||||||
|
|
||||||
|
paths: list[list[tuple[int, int]]] = []
|
||||||
|
for line in lines:
|
||||||
|
parts = line.split(" -> ")
|
||||||
|
paths.append(
|
||||||
|
[
|
||||||
|
cast(
|
||||||
|
tuple[int, int], tuple(int(c.strip()) for c in part.split(","))
|
||||||
|
)
|
||||||
|
for part in parts
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
blocks: dict[tuple[int, int], Cell] = {}
|
||||||
|
for path in paths:
|
||||||
|
for start, end in zip(path[:-1], path[1:]):
|
||||||
|
x_start = min(start[0], end[0])
|
||||||
|
x_end = max(start[0], end[0]) + 1
|
||||||
|
y_start = min(start[1], end[1])
|
||||||
|
y_end = max(start[1], end[1]) + 1
|
||||||
|
|
||||||
|
for x in range(x_start, x_end):
|
||||||
|
for y in range(y_start, y_end):
|
||||||
|
blocks[x, y] = Cell.ROCK
|
||||||
|
|
||||||
|
self.print_blocks("start", blocks)
|
||||||
|
|
||||||
|
y_max = max(y for _, y in blocks)
|
||||||
|
|
||||||
|
# === part 1 ===
|
||||||
|
|
||||||
|
blocks_1 = flow(
|
||||||
|
blocks.copy(), stop_fn=lambda x, y: y > y_max, fill_fn=lambda x, y: Cell.AIR
|
||||||
|
)
|
||||||
|
self.print_blocks("part1", blocks_1)
|
||||||
|
yield sum(v == Cell.SAND for v in blocks_1.values())
|
||||||
|
|
||||||
|
# === part 2 ===
|
||||||
|
|
||||||
|
blocks_2 = flow(
|
||||||
|
blocks.copy(),
|
||||||
|
stop_fn=lambda x, y: x == 500 and y == 0,
|
||||||
|
fill_fn=lambda x, y: Cell.AIR if y < y_max + 2 else Cell.ROCK,
|
||||||
|
)
|
||||||
|
blocks_2[500, 0] = Cell.SAND
|
||||||
|
self.print_blocks("part2", blocks_2)
|
||||||
|
yield sum(v == Cell.SAND for v in blocks_2.values())
|
95
src/holt59/aoc/2022/day15.py
Normal file
95
src/holt59/aoc/2022/day15.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import itertools as it
|
||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import parse # type: ignore
|
||||||
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def part1(
|
||||||
|
self, sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], row: int
|
||||||
|
) -> int:
|
||||||
|
no_beacons_row_l: list[NDArray[np.floating[Any]]] = []
|
||||||
|
|
||||||
|
for (sx, sy), (bx, by) in sensor_to_beacon.items():
|
||||||
|
d = abs(sx - bx) + abs(sy - by) # closest
|
||||||
|
|
||||||
|
no_beacons_row_l.append(sx - np.arange(0, d - abs(sy - row) + 1)) # type: ignore
|
||||||
|
no_beacons_row_l.append(sx + np.arange(0, d - abs(sy - row) + 1)) # type: ignore
|
||||||
|
|
||||||
|
beacons_at_row = set(bx for (bx, by) in sensor_to_beacon.values() if by == row)
|
||||||
|
no_beacons_row = set(it.chain(*no_beacons_row_l)).difference(beacons_at_row) # type: ignore
|
||||||
|
|
||||||
|
return len(no_beacons_row)
|
||||||
|
|
||||||
|
def part2_intervals(
|
||||||
|
self, sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], xy_max: int
|
||||||
|
) -> tuple[int, int, int]:
|
||||||
|
for y in self.progress.wrap(range(xy_max + 1)):
|
||||||
|
its: list[tuple[int, int]] = []
|
||||||
|
for (sx, sy), (bx, by) in sensor_to_beacon.items():
|
||||||
|
d = abs(sx - bx) + abs(sy - by)
|
||||||
|
dx = d - abs(sy - y)
|
||||||
|
|
||||||
|
if dx >= 0:
|
||||||
|
its.append((max(0, sx - dx), min(sx + dx, xy_max)))
|
||||||
|
|
||||||
|
its = sorted(its)
|
||||||
|
_, e = its[0]
|
||||||
|
|
||||||
|
for si, ei in its[1:]:
|
||||||
|
if si > e + 1:
|
||||||
|
return si - 1, y, 4_000_000 * (si - 1) + y
|
||||||
|
if ei > e:
|
||||||
|
e = ei
|
||||||
|
|
||||||
|
return (0, 0, 0)
|
||||||
|
|
||||||
|
def part2_cplex(
|
||||||
|
self, sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], xy_max: int
|
||||||
|
) -> tuple[int, int, int]:
|
||||||
|
from docplex.mp.model import Model
|
||||||
|
|
||||||
|
m = Model()
|
||||||
|
|
||||||
|
x, y = m.continuous_var_list(2, ub=xy_max, name=["x", "y"])
|
||||||
|
|
||||||
|
for (sx, sy), (bx, by) in sensor_to_beacon.items():
|
||||||
|
d = abs(sx - bx) + abs(sy - by)
|
||||||
|
m.add_constraint(
|
||||||
|
m.abs(x - sx) + m.abs(y - sy) >= d + 1, # type: ignore
|
||||||
|
ctname=f"ct_{sx}_{sy}",
|
||||||
|
)
|
||||||
|
|
||||||
|
m.set_objective("min", x + y)
|
||||||
|
|
||||||
|
s = m.solve()
|
||||||
|
assert s is not None
|
||||||
|
|
||||||
|
vx = int(s.get_value(x))
|
||||||
|
vy = int(s.get_value(y))
|
||||||
|
return vx, vy, 4_000_000 * vx + vy
|
||||||
|
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
|
||||||
|
sensor_to_beacon: dict[tuple[int, int], tuple[int, int]] = {}
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
r: dict[str, str] = parse.parse( # type: ignore
|
||||||
|
"Sensor at x={sx}, y={sy}: closest beacon is at x={bx}, y={by}", line
|
||||||
|
)
|
||||||
|
sensor_to_beacon[int(r["sx"]), int(r["sy"])] = (int(r["bx"]), int(r["by"]))
|
||||||
|
|
||||||
|
xy_max = 4_000_000 if max(sensor_to_beacon) > (1_000, 0) else 20
|
||||||
|
row = 2_000_000 if max(sensor_to_beacon) > (1_000, 0) else 10
|
||||||
|
|
||||||
|
yield self.part1(sensor_to_beacon, row)
|
||||||
|
|
||||||
|
# x, y, a2 = part2_cplex(sensor_to_beacon, xy_max)
|
||||||
|
x, y, a2 = self.part2_intervals(sensor_to_beacon, xy_max)
|
||||||
|
self.logger.info(f"answer 2 is {a2} (x={x}, y={y})")
|
||||||
|
yield a2
|
159
src/holt59/aoc/2022/day16.py
Normal file
159
src/holt59/aoc/2022/day16.py
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import heapq
|
||||||
|
import itertools
|
||||||
|
import re
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import Any, FrozenSet, Iterator, NamedTuple
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Pipe(NamedTuple):
|
||||||
|
name: str
|
||||||
|
flow: int
|
||||||
|
tunnels: list[str]
|
||||||
|
|
||||||
|
def __lt__(self, other: object) -> bool:
|
||||||
|
return isinstance(other, Pipe) and other.name < self.name
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
return isinstance(other, Pipe) and other.name == self.name
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash(self.name)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
def breadth_first_search(pipes: dict[str, Pipe], pipe: Pipe) -> dict[Pipe, int]:
|
||||||
|
"""
|
||||||
|
Runs a BFS from the given pipe and return the shortest distance (in term of hops)
|
||||||
|
to all other pipes.
|
||||||
|
"""
|
||||||
|
queue = [(0, pipe)]
|
||||||
|
visited: set[Pipe] = set()
|
||||||
|
distances: dict[Pipe, int] = {}
|
||||||
|
|
||||||
|
while len(distances) < len(pipes):
|
||||||
|
distance, current = heapq.heappop(queue)
|
||||||
|
|
||||||
|
if current in visited:
|
||||||
|
continue
|
||||||
|
|
||||||
|
visited.add(current)
|
||||||
|
distances[current] = distance
|
||||||
|
|
||||||
|
for tunnel in current.tunnels:
|
||||||
|
heapq.heappush(queue, (distance + 1, pipes[tunnel]))
|
||||||
|
|
||||||
|
return distances
|
||||||
|
|
||||||
|
|
||||||
|
def update_with_better(
|
||||||
|
node_at_times: dict[FrozenSet[Pipe], int], flow: int, flowing: FrozenSet[Pipe]
|
||||||
|
) -> None:
|
||||||
|
node_at_times[flowing] = max(node_at_times[flowing], flow)
|
||||||
|
|
||||||
|
|
||||||
|
# === MAIN ===
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def part_1(
|
||||||
|
self,
|
||||||
|
start_pipe: Pipe,
|
||||||
|
max_time: int,
|
||||||
|
distances: dict[tuple[Pipe, Pipe], int],
|
||||||
|
relevant_pipes: FrozenSet[Pipe],
|
||||||
|
):
|
||||||
|
node_at_times: dict[int, dict[Pipe, dict[FrozenSet[Pipe], int]]] = defaultdict(
|
||||||
|
lambda: defaultdict(lambda: defaultdict(lambda: 0))
|
||||||
|
)
|
||||||
|
node_at_times[0] = {start_pipe: {frozenset(): 0}}
|
||||||
|
|
||||||
|
for time in range(max_time):
|
||||||
|
for c_pipe, nodes in node_at_times[time].items():
|
||||||
|
for flowing, flow in nodes.items():
|
||||||
|
for target in relevant_pipes:
|
||||||
|
distance = distances[c_pipe, target] + 1
|
||||||
|
if time + distance >= max_time or target in flowing:
|
||||||
|
continue
|
||||||
|
|
||||||
|
update_with_better(
|
||||||
|
node_at_times[time + distance][target],
|
||||||
|
flow + sum(pipe.flow for pipe in flowing) * distance,
|
||||||
|
flowing | {target},
|
||||||
|
)
|
||||||
|
|
||||||
|
update_with_better(
|
||||||
|
node_at_times[max_time][c_pipe],
|
||||||
|
flow + sum(pipe.flow for pipe in flowing) * (max_time - time),
|
||||||
|
flowing,
|
||||||
|
)
|
||||||
|
|
||||||
|
return max(
|
||||||
|
flow
|
||||||
|
for nodes_of_pipe in node_at_times[max_time].values()
|
||||||
|
for flow in nodes_of_pipe.values()
|
||||||
|
)
|
||||||
|
|
||||||
|
def part_2(
|
||||||
|
self,
|
||||||
|
start_pipe: Pipe,
|
||||||
|
max_time: int,
|
||||||
|
distances: dict[tuple[Pipe, Pipe], int],
|
||||||
|
relevant_pipes: FrozenSet[Pipe],
|
||||||
|
):
|
||||||
|
def compute(pipes_for_me: FrozenSet[Pipe]) -> int:
|
||||||
|
return self.part_1(
|
||||||
|
start_pipe, max_time, distances, pipes_for_me
|
||||||
|
) + self.part_1(
|
||||||
|
start_pipe, max_time, distances, relevant_pipes - pipes_for_me
|
||||||
|
)
|
||||||
|
|
||||||
|
combs = [
|
||||||
|
frozenset(relevant_pipes_1)
|
||||||
|
for r in range(2, len(relevant_pipes) // 2 + 1)
|
||||||
|
for relevant_pipes_1 in itertools.combinations(relevant_pipes, r)
|
||||||
|
]
|
||||||
|
|
||||||
|
return max(compute(comb) for comb in self.progress.wrap(combs))
|
||||||
|
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = [line.strip() for line in input.splitlines()]
|
||||||
|
|
||||||
|
pipes: dict[str, Pipe] = {}
|
||||||
|
for line in lines:
|
||||||
|
r = re.match(
|
||||||
|
R"Valve ([A-Z]+) has flow rate=([0-9]+); tunnels? leads? to valves? (.+)",
|
||||||
|
line,
|
||||||
|
)
|
||||||
|
assert r
|
||||||
|
|
||||||
|
g = r.groups()
|
||||||
|
|
||||||
|
pipes[g[0]] = Pipe(g[0], int(g[1]), g[2].split(", "))
|
||||||
|
|
||||||
|
# compute distances from one valve to any other
|
||||||
|
distances: dict[tuple[Pipe, Pipe], int] = {}
|
||||||
|
for pipe_1 in pipes.values():
|
||||||
|
distances.update(
|
||||||
|
{
|
||||||
|
(pipe_1, pipe_2): distance
|
||||||
|
for pipe_2, distance in breadth_first_search(pipes, pipe_1).items()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# valves with flow
|
||||||
|
relevant_pipes = frozenset(pipe for pipe in pipes.values() if pipe.flow > 0)
|
||||||
|
|
||||||
|
# 1651, 1653
|
||||||
|
yield self.part_1(pipes["AA"], 30, distances, relevant_pipes)
|
||||||
|
|
||||||
|
# 1707, 2223
|
||||||
|
yield self.part_2(pipes["AA"], 26, distances, relevant_pipes)
|
@@ -1,25 +1,16 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
from typing import Any, Iterator, Sequence, TypeAlias, TypeVar
|
||||||
|
|
||||||
import sys
|
|
||||||
from typing import Sequence, TypeVar
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
Tower: TypeAlias = NDArray[np.bool]
|
||||||
def print_tower(tower: np.ndarray, out: str = "#"):
|
|
||||||
print("-" * (tower.shape[1] + 2))
|
|
||||||
non_empty = False
|
|
||||||
for row in reversed(range(1, tower.shape[0])):
|
|
||||||
if not non_empty and not tower[row, :].any():
|
|
||||||
continue
|
|
||||||
non_empty = True
|
|
||||||
print("|" + "".join(out if c else "." for c in tower[row, :]) + "|")
|
|
||||||
print("+" + "-" * tower.shape[1] + "+")
|
|
||||||
|
|
||||||
|
|
||||||
def tower_height(tower: np.ndarray) -> int:
|
def tower_height(tower: Tower) -> int:
|
||||||
return int(tower.shape[0] - tower[::-1, :].argmax(axis=0).min() - 1)
|
return int(tower.shape[0] - tower[::-1, :].argmax(axis=0).min() - 1)
|
||||||
|
|
||||||
|
|
||||||
@@ -47,9 +38,8 @@ def build_tower(
|
|||||||
n_rocks: int,
|
n_rocks: int,
|
||||||
jets: str,
|
jets: str,
|
||||||
early_stop: bool = False,
|
early_stop: bool = False,
|
||||||
init: np.ndarray = np.ones(WIDTH, dtype=bool),
|
init: Tower = np.ones(WIDTH, dtype=bool),
|
||||||
) -> tuple[np.ndarray, int, int, dict[int, int]]:
|
) -> tuple[Tower, int, int, dict[int, int]]:
|
||||||
|
|
||||||
tower = EMPTY_BLOCKS.copy()
|
tower = EMPTY_BLOCKS.copy()
|
||||||
tower[0, :] = init
|
tower[0, :] = init
|
||||||
|
|
||||||
@@ -59,7 +49,6 @@ def build_tower(
|
|||||||
rock_count = 0
|
rock_count = 0
|
||||||
|
|
||||||
for rock_count in range(n_rocks):
|
for rock_count in range(n_rocks):
|
||||||
|
|
||||||
if early_stop:
|
if early_stop:
|
||||||
if i_rock == 0 and (i_rock, i_jet) in done_at:
|
if i_rock == 0 and (i_rock, i_jet) in done_at:
|
||||||
break
|
break
|
||||||
@@ -75,7 +64,6 @@ def build_tower(
|
|||||||
tower = np.concatenate([tower, EMPTY_BLOCKS], axis=0)
|
tower = np.concatenate([tower, EMPTY_BLOCKS], axis=0)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
||||||
jet, i_jet = next_cycle(jets, i_jet)
|
jet, i_jet = next_cycle(jets, i_jet)
|
||||||
|
|
||||||
dx = 0
|
dx = 0
|
||||||
@@ -100,14 +88,13 @@ def build_tower(
|
|||||||
return tower, rock_count, done_at.get((i_rock, i_jet), -1), heights
|
return tower, rock_count, done_at.get((i_rock, i_jet), -1), heights
|
||||||
|
|
||||||
|
|
||||||
line = sys.stdin.read().strip()
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
tower, *_ = build_tower(2022, line)
|
tower, *_ = build_tower(2022, input)
|
||||||
answer_1 = tower_height(tower)
|
yield tower_height(tower)
|
||||||
print(f"answer 1 is {answer_1}")
|
|
||||||
|
|
||||||
TOTAL_ROCKS = 1_000_000_000_000
|
TOTAL_ROCKS = 1_000_000_000_000
|
||||||
tower_1, n_rocks_1, prev_1, heights_1 = build_tower(TOTAL_ROCKS, line, True)
|
_tower_1, n_rocks_1, prev_1, heights_1 = build_tower(TOTAL_ROCKS, input, True)
|
||||||
assert prev_1 > 0
|
assert prev_1 > 0
|
||||||
|
|
||||||
# 2767 1513
|
# 2767 1513
|
||||||
@@ -121,5 +108,4 @@ remaining_height = (
|
|||||||
heights_1[prev_1 + remaining_rocks % n_repeat_rocks] - heights_1[prev_1]
|
heights_1[prev_1 + remaining_rocks % n_repeat_rocks] - heights_1[prev_1]
|
||||||
)
|
)
|
||||||
|
|
||||||
answer_2 = base_height + (n_repeat_towers + 1) * repeat_height + remaining_height
|
yield base_height + (n_repeat_towers + 1) * repeat_height + remaining_height
|
||||||
print(f"answer 2 is {answer_2}")
|
|
58
src/holt59/aoc/2022/day18.py
Normal file
58
src/holt59/aoc/2022/day18.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
xyz = np.asarray(
|
||||||
|
[
|
||||||
|
tuple(int(x) for x in row.split(",")) # type: ignore
|
||||||
|
for row in input.splitlines()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
xyz = xyz - xyz.min(axis=0) + 1
|
||||||
|
|
||||||
|
cubes = np.zeros(xyz.max(axis=0) + 3, dtype=bool)
|
||||||
|
cubes[xyz[:, 0], xyz[:, 1], xyz[:, 2]] = True
|
||||||
|
|
||||||
|
faces = [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1)]
|
||||||
|
|
||||||
|
yield sum(
|
||||||
|
1
|
||||||
|
for x, y, z in xyz
|
||||||
|
for dx, dy, dz in faces
|
||||||
|
if not cubes[x + dx, y + dy, z + dz]
|
||||||
|
)
|
||||||
|
|
||||||
|
visited = np.zeros_like(cubes, dtype=bool)
|
||||||
|
queue = [(0, 0, 0)]
|
||||||
|
|
||||||
|
n_faces = 0
|
||||||
|
while queue:
|
||||||
|
x, y, z = queue.pop(0)
|
||||||
|
|
||||||
|
if visited[x, y, z]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
visited[x, y, z] = True
|
||||||
|
|
||||||
|
for dx, dy, dz in faces:
|
||||||
|
nx, ny, nz = x + dx, y + dy, z + dz
|
||||||
|
if not all(
|
||||||
|
n >= 0 and n < cubes.shape[i] for i, n in enumerate((nx, ny, nz))
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if visited[nx, ny, nz]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if cubes[nx, ny, nz]:
|
||||||
|
n_faces += 1
|
||||||
|
else:
|
||||||
|
queue.append((nx, ny, nz))
|
||||||
|
|
||||||
|
yield n_faces
|
@@ -1,11 +1,10 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
from typing import Any, Iterator, Literal
|
||||||
|
|
||||||
import sys
|
|
||||||
from typing import Literal
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import parse
|
import parse # pyright: ignore[reportMissingTypeStubs]
|
||||||
from tqdm import tqdm
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
Reagent = Literal["ore", "clay", "obsidian", "geode"]
|
Reagent = Literal["ore", "clay", "obsidian", "geode"]
|
||||||
REAGENTS: tuple[Reagent, ...] = (
|
REAGENTS: tuple[Reagent, ...] = (
|
||||||
@@ -37,7 +36,7 @@ class State:
|
|||||||
self.robots = robots
|
self.robots = robots
|
||||||
self.reagents = reagents
|
self.reagents = reagents
|
||||||
|
|
||||||
def __eq__(self, other) -> bool:
|
def __eq__(self, other: object) -> bool:
|
||||||
return (
|
return (
|
||||||
isinstance(other, State)
|
isinstance(other, State)
|
||||||
and self.robots == other.robots
|
and self.robots == other.robots
|
||||||
@@ -64,31 +63,7 @@ def dominates(lhs: State, rhs: State):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
lines = sys.stdin.read().splitlines()
|
|
||||||
|
|
||||||
blueprints: list[dict[Reagent, IntOfReagent]] = []
|
|
||||||
for line in lines:
|
|
||||||
r = parse.parse(
|
|
||||||
"Blueprint {}: "
|
|
||||||
"Each ore robot costs {:d} ore. "
|
|
||||||
"Each clay robot costs {:d} ore. "
|
|
||||||
"Each obsidian robot costs {:d} ore and {:d} clay. "
|
|
||||||
"Each geode robot costs {:d} ore and {:d} obsidian.",
|
|
||||||
line,
|
|
||||||
)
|
|
||||||
|
|
||||||
blueprints.append(
|
|
||||||
{
|
|
||||||
"ore": {"ore": r[1]},
|
|
||||||
"clay": {"ore": r[2]},
|
|
||||||
"obsidian": {"ore": r[3], "clay": r[4]},
|
|
||||||
"geode": {"ore": r[5], "obsidian": r[6]},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int:
|
def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int:
|
||||||
|
|
||||||
# since we can only build one robot per time, we do not need more than X robots
|
# since we can only build one robot per time, we do not need more than X robots
|
||||||
# of type K where X is the maximum number of K required among all robots, e.g.,
|
# of type K where X is the maximum number of K required among all robots, e.g.,
|
||||||
# in the first toy blueprint, we need at most 4 ore robots, 14 clay ones and 7
|
# in the first toy blueprint, we need at most 4 ore robots, 14 clay ones and 7
|
||||||
@@ -97,12 +72,12 @@ def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int:
|
|||||||
name: max(blueprint[r].get(name, 0) for r in REAGENTS) for name in REAGENTS
|
name: max(blueprint[r].get(name, 0) for r in REAGENTS) for name in REAGENTS
|
||||||
}
|
}
|
||||||
|
|
||||||
state_after_t: dict[int, set[State]] = {0: [State()]}
|
state_after_t: dict[int, set[State]] = {0: {State()}}
|
||||||
|
|
||||||
for t in range(1, max_time + 1):
|
for t in range(1, max_time + 1):
|
||||||
|
|
||||||
# list of new states at the end of step t that we are going to prune later
|
# list of new states at the end of step t that we are going to prune later
|
||||||
states_for_t: set[State] = set()
|
states_for_t: set[State] = set()
|
||||||
|
robots_that_can_be_built: list[Reagent]
|
||||||
|
|
||||||
for state in state_after_t[t - 1]:
|
for state in state_after_t[t - 1]:
|
||||||
robots_that_can_be_built = [
|
robots_that_can_be_built = [
|
||||||
@@ -136,7 +111,7 @@ def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int:
|
|||||||
for robot in robots_that_can_be_built:
|
for robot in robots_that_can_be_built:
|
||||||
robots = state.robots.copy()
|
robots = state.robots.copy()
|
||||||
robots[robot] += 1
|
robots[robot] += 1
|
||||||
reagents = {
|
reagents: IntOfReagent = {
|
||||||
reagent: state.reagents[reagent]
|
reagent: state.reagents[reagent]
|
||||||
+ state.robots[reagent]
|
+ state.robots[reagent]
|
||||||
- blueprint[robot].get(reagent, 0)
|
- blueprint[robot].get(reagent, 0)
|
||||||
@@ -155,7 +130,7 @@ def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
to_keep = []
|
to_keep: list[NDArray[np.integer[Any]]] = []
|
||||||
while len(np_states) > 0:
|
while len(np_states) > 0:
|
||||||
first_dom = (np_states[1:] >= np_states[0]).all(axis=1).any()
|
first_dom = (np_states[1:] >= np_states[0]).all(axis=1).any()
|
||||||
|
|
||||||
@@ -176,11 +151,31 @@ def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int:
|
|||||||
return max(state.reagents["geode"] for state in state_after_t[max_time])
|
return max(state.reagents["geode"] for state in state_after_t[max_time])
|
||||||
|
|
||||||
|
|
||||||
answer_1 = sum(
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
blueprints: list[dict[Reagent, IntOfReagent]] = []
|
||||||
|
for line in input.splitlines():
|
||||||
|
r: list[int] = parse.parse( # type: ignore
|
||||||
|
"Blueprint {}: "
|
||||||
|
"Each ore robot costs {:d} ore. "
|
||||||
|
"Each clay robot costs {:d} ore. "
|
||||||
|
"Each obsidian robot costs {:d} ore and {:d} clay. "
|
||||||
|
"Each geode robot costs {:d} ore and {:d} obsidian.",
|
||||||
|
line,
|
||||||
|
)
|
||||||
|
|
||||||
|
blueprints.append(
|
||||||
|
{
|
||||||
|
"ore": {"ore": r[1]},
|
||||||
|
"clay": {"ore": r[2]},
|
||||||
|
"obsidian": {"ore": r[3], "clay": r[4]},
|
||||||
|
"geode": {"ore": r[5], "obsidian": r[6]},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
yield sum(
|
||||||
(i_blueprint + 1) * run(blueprint, 24)
|
(i_blueprint + 1) * run(blueprint, 24)
|
||||||
for i_blueprint, blueprint in enumerate(blueprints)
|
for i_blueprint, blueprint in enumerate(blueprints)
|
||||||
)
|
)
|
||||||
print(f"answer 1 is {answer_1}")
|
|
||||||
|
|
||||||
answer_2 = run(blueprints[0], 32) * run(blueprints[1], 32) * run(blueprints[2], 32)
|
yield (run(blueprints[0], 32) * run(blueprints[1], 32) * run(blueprints[2], 32))
|
||||||
print(f"answer 2 is {answer_2}")
|
|
@@ -1,19 +1,6 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
from typing import Any, Iterator
|
||||||
|
|
||||||
import sys
|
from ..base import BaseSolver
|
||||||
|
|
||||||
lines = sys.stdin.readlines()
|
|
||||||
|
|
||||||
# the solution relies on replacing rock / paper / scissor by values 0 / 1 / 2 and using
|
|
||||||
# modulo-3 arithmetic
|
|
||||||
#
|
|
||||||
# in modulo-3 arithmetic, the winning move is 1 + the opponent move (e.g., winning move
|
|
||||||
# if opponent plays 0 is 1, or 0 if opponent plays 2 (0 = (2 + 1 % 3)))
|
|
||||||
#
|
|
||||||
|
|
||||||
# we read the lines in a Nx2 in array with value 0/1/2 instead of A/B/C or X/Y/Z for
|
|
||||||
# easier manipulation
|
|
||||||
values = [(ord(row[0]) - ord("A"), ord(row[2]) - ord("X")) for row in lines]
|
|
||||||
|
|
||||||
|
|
||||||
def score_1(ux: int, vx: int) -> int:
|
def score_1(ux: int, vx: int) -> int:
|
||||||
@@ -48,8 +35,23 @@ def score_2(ux: int, vx: int) -> int:
|
|||||||
return (ux + vx - 1) % 3 + 1 + vx * 3
|
return (ux + vx - 1) % 3 + 1 + vx * 3
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
|
||||||
|
# the solution relies on replacing rock / paper / scissor by values 0 / 1 / 2 and using
|
||||||
|
# modulo-3 arithmetic
|
||||||
|
#
|
||||||
|
# in modulo-3 arithmetic, the winning move is 1 + the opponent move (e.g., winning move
|
||||||
|
# if opponent plays 0 is 1, or 0 if opponent plays 2 (0 = (2 + 1 % 3)))
|
||||||
|
#
|
||||||
|
|
||||||
|
# we read the lines in a Nx2 in array with value 0/1/2 instead of A/B/C or X/Y/Z for
|
||||||
|
# easier manipulation
|
||||||
|
values = [(ord(row[0]) - ord("A"), ord(row[2]) - ord("X")) for row in lines]
|
||||||
|
|
||||||
# part 1 - 13526
|
# part 1 - 13526
|
||||||
print(f"score 1 is {sum(score_1(*v) for v in values)}")
|
yield sum(score_1(*v) for v in values)
|
||||||
|
|
||||||
# part 2 - 14204
|
# part 2 - 14204
|
||||||
print(f"score 2 is {sum(score_2(*v) for v in values)}")
|
yield sum(score_2(*v) for v in values)
|
@@ -1,8 +1,8 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
class Number:
|
class Number:
|
||||||
@@ -21,7 +21,6 @@ class Number:
|
|||||||
|
|
||||||
|
|
||||||
def decrypt(numbers: list[Number], key: int, rounds: int) -> int:
|
def decrypt(numbers: list[Number], key: int, rounds: int) -> int:
|
||||||
|
|
||||||
numbers = numbers.copy()
|
numbers = numbers.copy()
|
||||||
original = numbers.copy()
|
original = numbers.copy()
|
||||||
|
|
||||||
@@ -68,10 +67,9 @@ def decrypt(numbers: list[Number], key: int, rounds: int) -> int:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
numbers = [Number(int(x)) for i, x in enumerate(sys.stdin.readlines())]
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
numbers = [Number(int(x)) for x in input.splitlines()]
|
||||||
|
|
||||||
answer_1 = decrypt(numbers, 1, 1)
|
yield decrypt(numbers, 1, 1)
|
||||||
print(f"answer 1 is {answer_1}")
|
yield decrypt(numbers, 811589153, 10)
|
||||||
|
|
||||||
answer_2 = decrypt(numbers, 811589153, 10)
|
|
||||||
print(f"answer 2 is {answer_2}")
|
|
@@ -1,8 +1,7 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import operator
|
import operator
|
||||||
import sys
|
from typing import Any, Callable, Iterator
|
||||||
from typing import Callable
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
def compute(monkeys: dict[str, int | tuple[str, str, str]], monkey: str) -> int:
|
def compute(monkeys: dict[str, int | tuple[str, str, str]], monkey: str) -> int:
|
||||||
@@ -79,7 +78,9 @@ def invert(
|
|||||||
return monkeys
|
return monkeys
|
||||||
|
|
||||||
|
|
||||||
lines = sys.stdin.read().splitlines()
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = [line.strip() for line in input.splitlines()]
|
||||||
|
|
||||||
monkeys: dict[str, int | tuple[str, str, str]] = {}
|
monkeys: dict[str, int | tuple[str, str, str]] = {}
|
||||||
|
|
||||||
@@ -98,12 +99,10 @@ for line in lines:
|
|||||||
|
|
||||||
op_monkeys.add(name)
|
op_monkeys.add(name)
|
||||||
|
|
||||||
|
yield compute(monkeys.copy(), "root")
|
||||||
answer_1 = compute(monkeys.copy(), "root")
|
|
||||||
print(f"answer 1 is {answer_1}")
|
|
||||||
|
|
||||||
# assume the second operand of 'root' can be computed, and the first one depends on
|
# assume the second operand of 'root' can be computed, and the first one depends on
|
||||||
# humn, which is the case is my input and the test input
|
# humn, which is the case is my input and the test input
|
||||||
|
assert isinstance(monkeys["root"], tuple)
|
||||||
p1, _, p2 = monkeys["root"] # type: ignore
|
p1, _, p2 = monkeys["root"] # type: ignore
|
||||||
answer_2 = compute(invert(monkeys, "humn", compute(monkeys.copy(), p2)), "humn")
|
yield compute(invert(monkeys, "humn", compute(monkeys.copy(), p2)), "humn")
|
||||||
print(f"answer 2 is {answer_2}")
|
|
243
src/holt59/aoc/2022/day22.py
Normal file
243
src/holt59/aoc/2022/day22.py
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
import re
|
||||||
|
from typing import Any, Callable, Iterator
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
VOID, EMPTY, WALL = 0, 1, 2
|
||||||
|
TILE_FROM_CHAR = {" ": VOID, ".": EMPTY, "#": WALL}
|
||||||
|
|
||||||
|
SCORES = {"E": 0, "S": 1, "W": 2, "N": 3}
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
board_map_s, direction_s = input.split("\n\n")
|
||||||
|
|
||||||
|
# board
|
||||||
|
board_lines = board_map_s.splitlines()
|
||||||
|
max_line = max(len(line) for line in board_lines)
|
||||||
|
board = np.array(
|
||||||
|
[
|
||||||
|
[TILE_FROM_CHAR[c] for c in row] + [VOID] * (max_line - len(row))
|
||||||
|
for row in board_map_s.splitlines()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
directions = [
|
||||||
|
int(p1) if p2 else p1
|
||||||
|
for p1, p2 in re.findall(R"(([0-9])+|L|R)", direction_s)
|
||||||
|
]
|
||||||
|
|
||||||
|
# find on each row and column the first and last non-void
|
||||||
|
row_first_non_void = np.argmax(board != VOID, axis=1)
|
||||||
|
row_last_non_void = (
|
||||||
|
board.shape[1] - np.argmax(board[:, ::-1] != VOID, axis=1) - 1
|
||||||
|
)
|
||||||
|
col_first_non_void = np.argmax(board != VOID, axis=0)
|
||||||
|
col_last_non_void = (
|
||||||
|
board.shape[0] - np.argmax(board[::-1, :] != VOID, axis=0) - 1
|
||||||
|
)
|
||||||
|
|
||||||
|
faces = np.zeros_like(board)
|
||||||
|
size = np.gcd(board.shape[0], board.shape[1])
|
||||||
|
for row in range(0, board.shape[0], size):
|
||||||
|
for col in range(row_first_non_void[row], row_last_non_void[row], size):
|
||||||
|
faces[row : row + size, col : col + size] = faces.max() + 1
|
||||||
|
|
||||||
|
SIZE = np.gcd(*board.shape)
|
||||||
|
|
||||||
|
# TODO: deduce this from the actual cube...
|
||||||
|
faces_wrap: dict[int, dict[str, Callable[[int, int], tuple[int, int, str]]]]
|
||||||
|
|
||||||
|
if board.shape == (12, 16): # example
|
||||||
|
faces_wrap = {
|
||||||
|
1: {
|
||||||
|
"W": lambda y, x: (4, 4 + y, "S"), # 3N
|
||||||
|
"N": lambda y, x: (4, 11 - x, "S"), # 2N
|
||||||
|
"E": lambda y, x: (11 - y, 15, "W"), # 6E
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
"W": lambda y, x: (11, 19 - y, "N"), # 6S
|
||||||
|
"N": lambda y, x: (0, 11 - y, "S"), # 1N
|
||||||
|
"S": lambda y, x: (11, 11 - x, "N"), # 5S
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
"N": lambda y, x: (x - 4, 8, "E"), # 1W
|
||||||
|
"S": lambda y, x: (15 - x, 8, "E"), # 5W
|
||||||
|
},
|
||||||
|
4: {"E": lambda y, x: (8, 19 - y, "S")}, # 6N
|
||||||
|
5: {
|
||||||
|
"W": lambda y, x: (7, 15 - y, "N"), # 3S
|
||||||
|
"S": lambda y, x: (7, 11 - x, "N"), # 2S
|
||||||
|
},
|
||||||
|
6: {
|
||||||
|
"N": lambda y, x: (19 - x, 11, "W"), # 4E
|
||||||
|
"E": lambda y, x: (11 - y, 11, "W"), # 1E
|
||||||
|
"S": lambda y, x: (19 - x, 0, "E"), # 2W
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
else:
|
||||||
|
faces_wrap = {
|
||||||
|
1: {
|
||||||
|
"W": lambda y, x: (3 * SIZE - y - 1, 0, "E"), # 4W
|
||||||
|
"N": lambda y, x: (2 * SIZE + x, 0, "E"), # 6W
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
"N": lambda y, x: (4 * SIZE - 1, x - 2 * SIZE, "N"), # 6S
|
||||||
|
"E": lambda y, x: (3 * SIZE - y - 1, 2 * SIZE - 1, "W"), # 5E
|
||||||
|
"S": lambda y, x: (x - SIZE, 2 * SIZE - 1, "W"), # 3E
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
"W": lambda y, x: (2 * SIZE, y - SIZE, "S"), # 4N
|
||||||
|
"E": lambda y, x: (SIZE - 1, SIZE + y, "N"), # 2S
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
"W": lambda y, x: (3 * SIZE - y - 1, SIZE, "E"), # 1W
|
||||||
|
"N": lambda y, x: (SIZE + x, SIZE, "E"), # 3W
|
||||||
|
},
|
||||||
|
5: {
|
||||||
|
"E": lambda y, x: (3 * SIZE - y - 1, 3 * SIZE - 1, "W"), # 2E
|
||||||
|
"S": lambda y, x: (2 * SIZE + x, SIZE - 1, "W"), # 6E
|
||||||
|
},
|
||||||
|
6: {
|
||||||
|
"W": lambda y, x: (0, y - 2 * SIZE, "S"), # 1N
|
||||||
|
"E": lambda y, x: (3 * SIZE - 1, y - 2 * SIZE, "N"), # 5S
|
||||||
|
"S": lambda y, x: (0, x + 2 * SIZE, "S"), # 2N
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def wrap_part_1(y0: int, x0: int, r0: str) -> tuple[int, int, str]:
|
||||||
|
if r0 == "E":
|
||||||
|
return y0, row_first_non_void[y0], r0
|
||||||
|
elif r0 == "S":
|
||||||
|
return col_first_non_void[x0], x0, r0
|
||||||
|
elif r0 == "W":
|
||||||
|
return y0, row_last_non_void[y0], r0
|
||||||
|
elif r0 == "N":
|
||||||
|
return col_last_non_void[x0], x0, r0
|
||||||
|
|
||||||
|
assert False
|
||||||
|
|
||||||
|
def wrap_part_2(y0: int, x0: int, r0: str) -> tuple[int, int, str]:
|
||||||
|
cube = faces[y0, x0]
|
||||||
|
assert r0 in faces_wrap[cube]
|
||||||
|
return faces_wrap[cube][r0](y0, x0)
|
||||||
|
|
||||||
|
def run(
|
||||||
|
wrap: Callable[[int, int, str], tuple[int, int, str]],
|
||||||
|
) -> tuple[int, int, str]:
|
||||||
|
y0 = 0
|
||||||
|
x0 = np.where(board[0] == EMPTY)[0][0]
|
||||||
|
r0 = "E"
|
||||||
|
|
||||||
|
for direction in directions:
|
||||||
|
if isinstance(direction, int):
|
||||||
|
while direction > 0:
|
||||||
|
if r0 == "E":
|
||||||
|
xi = np.where(
|
||||||
|
board[y0, x0 + 1 : x0 + direction + 1] == WALL
|
||||||
|
)[0]
|
||||||
|
if len(xi):
|
||||||
|
x0 = x0 + xi[0]
|
||||||
|
direction = 0
|
||||||
|
elif (
|
||||||
|
x0 + direction < board.shape[1]
|
||||||
|
and board[y0, x0 + direction] == EMPTY
|
||||||
|
):
|
||||||
|
x0 = x0 + direction
|
||||||
|
direction = 0
|
||||||
|
else:
|
||||||
|
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
|
||||||
|
if board[y0_t, x0_t] == WALL:
|
||||||
|
x0 = row_last_non_void[y0]
|
||||||
|
direction = 0
|
||||||
|
else:
|
||||||
|
direction = (
|
||||||
|
direction - (row_last_non_void[y0] - x0) - 1
|
||||||
|
)
|
||||||
|
y0, x0, r0 = y0_t, x0_t, r0_t
|
||||||
|
elif r0 == "S":
|
||||||
|
yi = np.where(
|
||||||
|
board[y0 + 1 : y0 + direction + 1, x0] == WALL
|
||||||
|
)[0]
|
||||||
|
if len(yi):
|
||||||
|
y0 = y0 + yi[0]
|
||||||
|
direction = 0
|
||||||
|
elif (
|
||||||
|
y0 + direction < board.shape[0]
|
||||||
|
and board[y0 + direction, x0] == EMPTY
|
||||||
|
):
|
||||||
|
y0 = y0 + direction
|
||||||
|
direction = 0
|
||||||
|
else:
|
||||||
|
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
|
||||||
|
if board[y0_t, x0_t] == WALL:
|
||||||
|
y0 = col_last_non_void[x0]
|
||||||
|
direction = 0
|
||||||
|
else:
|
||||||
|
direction = (
|
||||||
|
direction - (col_last_non_void[x0] - y0) - 1
|
||||||
|
)
|
||||||
|
y0, x0, r0 = y0_t, x0_t, r0_t
|
||||||
|
elif r0 == "W":
|
||||||
|
left = max(x0 - direction - 1, 0)
|
||||||
|
xi = np.where(board[y0, left:x0] == WALL)[0]
|
||||||
|
if len(xi):
|
||||||
|
x0 = left + xi[-1] + 1
|
||||||
|
direction = 0
|
||||||
|
elif (
|
||||||
|
x0 - direction >= 0
|
||||||
|
and board[y0, x0 - direction] == EMPTY
|
||||||
|
):
|
||||||
|
x0 = x0 - direction
|
||||||
|
direction = 0
|
||||||
|
else:
|
||||||
|
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
|
||||||
|
if board[y0_t, x0_t] == WALL:
|
||||||
|
x0 = row_first_non_void[y0]
|
||||||
|
direction = 0
|
||||||
|
else:
|
||||||
|
direction = (
|
||||||
|
direction - (x0 - row_first_non_void[y0]) - 1
|
||||||
|
)
|
||||||
|
y0, x0, r0 = y0_t, x0_t, r0_t
|
||||||
|
elif r0 == "N":
|
||||||
|
top = max(y0 - direction - 1, 0)
|
||||||
|
yi = np.where(board[top:y0, x0] == WALL)[0]
|
||||||
|
if len(yi):
|
||||||
|
y0 = top + yi[-1] + 1
|
||||||
|
direction = 0
|
||||||
|
elif (
|
||||||
|
y0 - direction >= 0
|
||||||
|
and board[y0 - direction, x0] == EMPTY
|
||||||
|
):
|
||||||
|
y0 = y0 - direction
|
||||||
|
direction = 0
|
||||||
|
else:
|
||||||
|
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
|
||||||
|
if board[y0_t, x0_t] == WALL:
|
||||||
|
y0 = col_first_non_void[x0]
|
||||||
|
direction = 0
|
||||||
|
else:
|
||||||
|
direction = (
|
||||||
|
direction - (y0 - col_first_non_void[x0]) - 1
|
||||||
|
)
|
||||||
|
y0, x0, r0 = y0_t, x0_t, r0_t
|
||||||
|
else:
|
||||||
|
r0 = {
|
||||||
|
"E": {"L": "N", "R": "S"},
|
||||||
|
"N": {"L": "W", "R": "E"},
|
||||||
|
"W": {"L": "S", "R": "N"},
|
||||||
|
"S": {"L": "E", "R": "W"},
|
||||||
|
}[r0][direction]
|
||||||
|
|
||||||
|
return y0, x0, r0
|
||||||
|
|
||||||
|
y1, x1, r1 = run(wrap_part_1)
|
||||||
|
yield 1000 * (1 + y1) + 4 * (1 + x1) + SCORES[r1]
|
||||||
|
|
||||||
|
y2, x2, r2 = run(wrap_part_2)
|
||||||
|
yield 1000 * (1 + y2) + 4 * (1 + x2) + SCORES[r2]
|
96
src/holt59/aoc/2022/day23.py
Normal file
96
src/holt59/aoc/2022/day23.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import itertools
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
Directions = list[
|
||||||
|
tuple[
|
||||||
|
str, tuple[int, int], tuple[tuple[int, int], tuple[int, int], tuple[int, int]]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
# (Y, X)
|
||||||
|
DIRECTIONS: Directions = [
|
||||||
|
("N", (-1, 0), ((-1, -1), (-1, 0), (-1, 1))),
|
||||||
|
("S", (1, 0), ((1, -1), (1, 0), (1, 1))),
|
||||||
|
("W", (0, -1), ((-1, -1), (0, -1), (1, -1))),
|
||||||
|
("E", (0, 1), ((-1, 1), (0, 1), (1, 1))),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def min_max_yx(positions: set[tuple[int, int]]) -> tuple[int, int, int, int]:
|
||||||
|
ys, xs = {y for y, _x in positions}, {x for _y, x in positions}
|
||||||
|
return min(ys), min(xs), max(ys), max(xs)
|
||||||
|
|
||||||
|
|
||||||
|
def round(
|
||||||
|
positions: set[tuple[int, int]],
|
||||||
|
directions: Directions,
|
||||||
|
):
|
||||||
|
to_move: dict[tuple[int, int], list[tuple[int, int]]] = defaultdict(lambda: [])
|
||||||
|
for y, x in positions:
|
||||||
|
elves = {
|
||||||
|
(dy, dx): (y + dy, x + dx) in positions
|
||||||
|
for dy, dx in itertools.product((-1, 0, 1), (-1, 0, 1))
|
||||||
|
if (dy, dx) != (0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if not any(elves.values()):
|
||||||
|
to_move[y, x].append((y, x))
|
||||||
|
continue
|
||||||
|
|
||||||
|
found: str | None = None
|
||||||
|
for d, (dy, dx), d_yx_check in directions:
|
||||||
|
if not any(elves[dy, dx] for dy, dx in d_yx_check):
|
||||||
|
found = d
|
||||||
|
to_move[y + dy, x + dx].append((y, x))
|
||||||
|
break
|
||||||
|
if found is None:
|
||||||
|
to_move[y, x].append((y, x))
|
||||||
|
|
||||||
|
positions.clear()
|
||||||
|
for ty, tx in to_move:
|
||||||
|
if len(to_move[ty, tx]) > 1:
|
||||||
|
positions.update(to_move[ty, tx])
|
||||||
|
else:
|
||||||
|
positions.add((ty, tx))
|
||||||
|
|
||||||
|
directions.append(directions.pop(0))
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
POSITIONS = {
|
||||||
|
(i, j)
|
||||||
|
for i, row in enumerate(input.splitlines())
|
||||||
|
for j, col in enumerate(row)
|
||||||
|
if col == "#"
|
||||||
|
}
|
||||||
|
|
||||||
|
# === part 1 ===
|
||||||
|
|
||||||
|
p1, d1 = POSITIONS.copy(), DIRECTIONS.copy()
|
||||||
|
for _ in range(10):
|
||||||
|
round(p1, d1)
|
||||||
|
|
||||||
|
min_y, min_x, max_y, max_x = min_max_yx(p1)
|
||||||
|
yield sum(
|
||||||
|
(y, x) not in p1
|
||||||
|
for y in range(min_y, max_y + 1)
|
||||||
|
for x in range(min_x, max_x + 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
# === part 2 ===
|
||||||
|
|
||||||
|
p2, d2 = POSITIONS.copy(), DIRECTIONS.copy()
|
||||||
|
answer_2 = 0
|
||||||
|
while True:
|
||||||
|
answer_2 += 1
|
||||||
|
backup = p2.copy()
|
||||||
|
round(p2, d2)
|
||||||
|
|
||||||
|
if backup == p2:
|
||||||
|
break
|
||||||
|
|
||||||
|
yield answer_2
|
117
src/holt59/aoc/2022/day24.py
Normal file
117
src/holt59/aoc/2022/day24.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import heapq
|
||||||
|
import math
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = [line.strip() for line in input.splitlines()]
|
||||||
|
|
||||||
|
winds = {
|
||||||
|
(i - 1, j - 1, lines[i][j])
|
||||||
|
for i in range(1, len(lines) - 1)
|
||||||
|
for j in range(1, len(lines[i]) - 1)
|
||||||
|
if lines[i][j] != "."
|
||||||
|
}
|
||||||
|
|
||||||
|
n_rows, n_cols = len(lines) - 2, len(lines[0]) - 2
|
||||||
|
CYCLE = math.lcm(n_rows, n_cols)
|
||||||
|
|
||||||
|
east_winds = [
|
||||||
|
{j for j in range(n_cols) if (i, j, ">") in winds} for i in range(n_rows)
|
||||||
|
]
|
||||||
|
west_winds = [
|
||||||
|
{j for j in range(n_cols) if (i, j, "<") in winds} for i in range(n_rows)
|
||||||
|
]
|
||||||
|
north_winds = [
|
||||||
|
{i for i in range(n_rows) if (i, j, "^") in winds} for j in range(n_cols)
|
||||||
|
]
|
||||||
|
south_winds = [
|
||||||
|
{i for i in range(n_rows) if (i, j, "v") in winds} for j in range(n_cols)
|
||||||
|
]
|
||||||
|
|
||||||
|
def run(start: tuple[int, int], start_cycle: int, end: tuple[int, int]):
|
||||||
|
def heuristic(y: int, x: int) -> int:
|
||||||
|
return abs(end[0] - y) + abs(end[1] - x)
|
||||||
|
|
||||||
|
# (distance + heuristic, distance, (start_pos, cycle))
|
||||||
|
queue = [
|
||||||
|
(heuristic(start[0], start[1]), 0, ((start[0], start[1]), start_cycle))
|
||||||
|
]
|
||||||
|
visited: set[tuple[tuple[int, int], int]] = set()
|
||||||
|
distances: dict[tuple[int, int], dict[int, int]] = defaultdict(lambda: {})
|
||||||
|
|
||||||
|
while queue:
|
||||||
|
_, distance, ((y, x), cycle) = heapq.heappop(queue)
|
||||||
|
|
||||||
|
if ((y, x), cycle) in visited:
|
||||||
|
continue
|
||||||
|
|
||||||
|
distances[y, x][cycle] = distance
|
||||||
|
|
||||||
|
visited.add(((y, x), cycle))
|
||||||
|
|
||||||
|
if (y, x) == (end[0], end[1]):
|
||||||
|
break
|
||||||
|
|
||||||
|
for dy, dx in (0, 0), (-1, 0), (1, 0), (0, -1), (0, 1):
|
||||||
|
ty = y + dy
|
||||||
|
tx = x + dx
|
||||||
|
|
||||||
|
n_cycle = (cycle + 1) % CYCLE
|
||||||
|
|
||||||
|
if (ty, tx) == end:
|
||||||
|
heapq.heappush(
|
||||||
|
queue, (distance + 1, distance + 1, ((ty, tx), n_cycle))
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
if ((ty, tx), n_cycle) in visited:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (ty, tx) != start and (
|
||||||
|
ty < 0 or tx < 0 or ty >= n_rows or tx >= n_cols
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (ty, tx) != start:
|
||||||
|
if (ty - n_cycle) % n_rows in south_winds[tx]:
|
||||||
|
continue
|
||||||
|
if (ty + n_cycle) % n_rows in north_winds[tx]:
|
||||||
|
continue
|
||||||
|
if (tx + n_cycle) % n_cols in west_winds[ty]:
|
||||||
|
continue
|
||||||
|
if (tx - n_cycle) % n_cols in east_winds[ty]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
heapq.heappush(
|
||||||
|
queue,
|
||||||
|
(
|
||||||
|
(
|
||||||
|
heuristic(ty, tx) + distance + 1,
|
||||||
|
distance + 1,
|
||||||
|
((ty, tx), n_cycle),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return distances, next(iter(distances[end].values()))
|
||||||
|
|
||||||
|
start = (
|
||||||
|
-1,
|
||||||
|
next(j for j in range(1, len(lines[0]) - 1) if lines[0][j] == ".") - 1,
|
||||||
|
)
|
||||||
|
end = (
|
||||||
|
n_rows,
|
||||||
|
next(j for j in range(1, len(lines[-1]) - 1) if lines[-1][j] == ".") - 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
distances_1, forward_1 = run(start, 0, end)
|
||||||
|
yield forward_1
|
||||||
|
|
||||||
|
distances_2, return_1 = run(end, next(iter(distances_1[end].keys())), start)
|
||||||
|
_distances_3, forward_2 = run(start, next(iter(distances_2[start].keys())), end)
|
||||||
|
yield forward_1 + return_1 + forward_2
|
28
src/holt59/aoc/2022/day25.py
Normal file
28
src/holt59/aoc/2022/day25.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = [line.strip() for line in input.splitlines()]
|
||||||
|
|
||||||
|
coeffs = {"2": 2, "1": 1, "0": 0, "-": -1, "=": -2}
|
||||||
|
|
||||||
|
def snafu2number(number: str) -> int:
|
||||||
|
value = 0
|
||||||
|
for c in number:
|
||||||
|
value *= 5
|
||||||
|
value += coeffs[c]
|
||||||
|
return value
|
||||||
|
|
||||||
|
def number2snafu(number: int) -> str:
|
||||||
|
values = ["0", "1", "2", "=", "-"]
|
||||||
|
res = ""
|
||||||
|
while number > 0:
|
||||||
|
mod = number % 5
|
||||||
|
res = res + values[mod]
|
||||||
|
number = number // 5 + int(mod >= 3)
|
||||||
|
return "".join(reversed(res))
|
||||||
|
|
||||||
|
yield number2snafu(sum(map(snafu2number, lines)))
|
28
src/holt59/aoc/2022/day3.py
Normal file
28
src/holt59/aoc/2022/day3.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import string
|
||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = [line.strip() for line in input.splitlines()]
|
||||||
|
|
||||||
|
# extract content of each part
|
||||||
|
parts = [
|
||||||
|
(set(line[: len(line) // 2]), set(line[len(line) // 2 :])) for line in lines
|
||||||
|
]
|
||||||
|
|
||||||
|
# priorities
|
||||||
|
priorities = {c: i + 1 for i, c in enumerate(string.ascii_letters)}
|
||||||
|
|
||||||
|
# part 1
|
||||||
|
yield sum(priorities[c] for p1, p2 in parts for c in p1.intersection(p2))
|
||||||
|
|
||||||
|
# part 2
|
||||||
|
n_per_group = 3
|
||||||
|
yield sum(
|
||||||
|
priorities[c]
|
||||||
|
for i in range(0, len(lines), n_per_group)
|
||||||
|
for c in set(lines[i]).intersection(*lines[i + 1 : i + n_per_group])
|
||||||
|
)
|
20
src/holt59/aoc/2022/day4.py
Normal file
20
src/holt59/aoc/2022/day4.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
def make_range(value: str) -> set[int]:
|
||||||
|
parts = value.split("-")
|
||||||
|
return set(range(int(parts[0]), int(parts[1]) + 1))
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = [line.strip() for line in input.splitlines()]
|
||||||
|
|
||||||
|
sections = [
|
||||||
|
tuple(make_range(part) for part in line.split(",")) for line in lines
|
||||||
|
]
|
||||||
|
|
||||||
|
yield sum(s1.issubset(s2) or s2.issubset(s1) for s1, s2 in sections)
|
||||||
|
yield sum(bool(s1.intersection(s2)) for s1, s2 in sections)
|
43
src/holt59/aoc/2022/day5.py
Normal file
43
src/holt59/aoc/2022/day5.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import copy
|
||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
blocks_s, moves_s = (part.splitlines() for part in input.split("\n\n"))
|
||||||
|
|
||||||
|
blocks: dict[str, list[str]] = {stack: [] for stack in blocks_s[-1].split()}
|
||||||
|
|
||||||
|
# this codes assumes that the lines are regular, i.e., 4 characters per "crate" in the
|
||||||
|
# form of '[X] ' (including the trailing space)
|
||||||
|
#
|
||||||
|
for block in blocks_s[-2::-1]:
|
||||||
|
for stack, index in zip(blocks, range(0, len(block), 4)):
|
||||||
|
crate = block[index + 1 : index + 2].strip()
|
||||||
|
|
||||||
|
if crate:
|
||||||
|
blocks[stack].append(crate)
|
||||||
|
|
||||||
|
# part 1 - deep copy for part 2
|
||||||
|
blocks_1 = copy.deepcopy(blocks)
|
||||||
|
|
||||||
|
for move in moves_s:
|
||||||
|
_, count_s, _, from_, _, to_ = move.strip().split()
|
||||||
|
|
||||||
|
for _i in range(int(count_s)):
|
||||||
|
blocks_1[to_].append(blocks_1[from_].pop())
|
||||||
|
|
||||||
|
# part 2
|
||||||
|
blocks_2 = copy.deepcopy(blocks)
|
||||||
|
|
||||||
|
for move in moves_s:
|
||||||
|
_, count_s, _, from_, _, to_ = move.strip().split()
|
||||||
|
count = int(count_s)
|
||||||
|
|
||||||
|
blocks_2[to_].extend(blocks_2[from_][-count:])
|
||||||
|
del blocks_2[from_][-count:]
|
||||||
|
|
||||||
|
yield "".join(s[-1] for s in blocks_1.values())
|
||||||
|
yield "".join(s[-1] for s in blocks_2.values())
|
16
src/holt59/aoc/2022/day6.py
Normal file
16
src/holt59/aoc/2022/day6.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
|
def index_of_first_n_differents(data: str, n: int) -> int:
|
||||||
|
for i in range(len(data)):
|
||||||
|
if len(set(data[i : i + n])) == n:
|
||||||
|
return i + n
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
yield index_of_first_n_differents(input, 4)
|
||||||
|
yield index_of_first_n_differents(input, 14)
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user