Compare commits

..

29 Commits

Author SHA1 Message Date
Mikaël Capelle
100df02a09 Better day 16. 2022-12-16 22:52:03 +01:00
Mikael CAPELLE
15b987a590 Update ugly day 16. 2022-12-16 18:40:21 +01:00
Mikael CAPELLE
b1578f5709 Add ugly day 16, to be improved... 2022-12-16 09:21:54 +01:00
Mikael CAPELLE
d80dbb6c7c Update day 15. 2022-12-15 14:17:04 +01:00
Mikael CAPELLE
b679c1f895 Add day 15. 2022-12-15 09:36:19 +01:00
Mikael CAPELLE
e9d5f9747b Add day 14. 2022-12-14 09:29:56 +01:00
Mikael CAPELLE
fe3aad7ddd Clean day 9. 2022-12-13 11:18:31 +01:00
Mikael CAPELLE
7ac9981ae5 Clean day 13. 2022-12-13 08:59:53 +01:00
Mikael CAPELLE
652756a341 Add day 13. 2022-12-13 08:54:15 +01:00
Mikael CAPELLE
c7ef505f1b Clean day 12. 2022-12-12 17:55:24 +01:00
Mikael CAPELLE
c55f6ac8e1 One to many Dijkstra. 2022-12-12 17:50:48 +01:00
Mikael CAPELLE
726a6aecac Generic Dijkstra for day 12. 2022-12-12 15:59:19 +01:00
Mikael CAPELLE
291b188238 Clean day 12. 2022-12-12 10:48:37 +01:00
Mikael CAPELLE
289e3b7d02 Add day 12. 2022-12-12 09:35:12 +01:00
Mikaël Capelle
9820765e9c Clean monkey code. 2022-12-11 11:50:23 +01:00
Mikaël Capelle
c6522de8a2 Add day 11. 2022-12-11 11:42:47 +01:00
Mikaël Capelle
80465e5e53 Add day 10. 2022-12-10 10:24:23 +01:00
Mikaël Capelle
af1428b5e1 Add day 9. 2022-12-09 10:45:00 +01:00
Mikael CAPELLE
fca283527d Add 2022 day 8. 2022-12-08 08:59:25 +01:00
Mikael CAPELLE
0d37458ec5 Add 2021 day 5. 2022-12-07 18:41:39 +01:00
Mikael CAPELLE
198927e4a3 Add day 7. 2022-12-07 09:28:06 +01:00
Mikael CAPELLE
4192c98bba Cleaning. 2022-12-06 15:28:46 +01:00
Mikael CAPELLE
7cb8317659 Add day 6. 2022-12-06 09:03:22 +01:00
Mikaël Capelle
f46cb51c60 Add day 4. 2022-12-05 19:09:55 +01:00
Mikael CAPELLE
261a396ae7 Add day 5. 2022-12-05 08:58:25 +01:00
Mikaël Capelle
4b3af377ab Day 3. 2022-12-03 11:42:09 +01:00
Mikael CAPELLE
f697415ef2 More comments. 2022-12-02 15:06:37 +01:00
Mikael CAPELLE
ac2806b0fb Comments. 2022-12-02 09:21:38 +01:00
Mikael CAPELLE
c62b8abfd0 Day 2. 2022-12-02 09:12:49 +01:00
436 changed files with 1017 additions and 62796 deletions

View File

@ -1,12 +0,0 @@
---
kind: pipeline
type: docker
name: default
steps:
- name: tests
image: python:3.10-slim
commands:
- pip install poetry
- poetry install
- poetry run poe lint

6
.gitignore vendored
View File

@ -1,7 +1 @@
# python / VS Code
venv venv
__pycache__
.ruff_cache
.vscode
build
files

52
2021/day5.py Normal file
View File

@ -0,0 +1,52 @@
# -*- 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 Normal file
View File

@ -0,0 +1,20 @@
# -*- 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:])}")

40
2022/day10.py Normal file
View File

@ -0,0 +1,40 @@
# -*- 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()

View File

@ -1,11 +1,13 @@
import copy # -*- encoding: utf-8 -*-
from functools import reduce
from typing import Any, Callable, Final, Iterator, Mapping, Sequence
from ..base import BaseSolver import copy
import sys
from functools import reduce
from typing import Callable, Final, Mapping, Sequence
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]]
@ -95,7 +97,8 @@ def run(
# number of inspects # number of inspects
inspects = {monkey: 0 for monkey in monkeys} inspects = {monkey: 0 for monkey in monkeys}
for _ in range(n_rounds): for round 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
@ -120,28 +123,24 @@ def monkey_business(inspects: dict[Monkey, int]) -> int:
return sorted_levels[-2] * sorted_levels[-1] return sorted_levels[-2] * sorted_levels[-1]
class Solver(BaseSolver): monkeys = [parse_monkey(block.splitlines()) for block in sys.stdin.read().split("\n\n")]
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
yield monkey_business( answer_1 = 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
# #
# (a + b) % c == ((a % c) + (b % c)) % c --- this would work for a single test value # (a + b) % c == ((a % c) + (b % c)) % c --- this would work for a single test value
# #
# (a + b) % c == ((a % d) + (b % d)) % c --- if d is a multiple of c, which is why here # (a + b) % c == ((a % d) + (b % d)) % c --- if d is a multiple of c, which is why here
# 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)
yield monkey_business( answer_2 = monkey_business(
run( run(copy.deepcopy(monkeys), 10_000, me_worry_fn=lambda w: w % total_test_value)
copy.deepcopy(monkeys), )
10_000, print(f"answer 2 is {answer_2}")
me_worry_fn=lambda w: w % total_test_value,
)
)

165
2022/day12.py Normal file
View File

@ -0,0 +1,165 @@
# -*- 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}")

41
2022/day13.py Normal file
View File

@ -0,0 +1,41 @@
# -*- 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 Normal file
View File

@ -0,0 +1,144 @@
# -*- 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())}")

90
2022/day15.py Normal file
View File

@ -0,0 +1,90 @@
# -*- 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})")

155
2022/day16.py Normal file
View File

@ -0,0 +1,155 @@
# -*- 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_1: Pipe, pipe_2: Pipe) -> int:
queue = [(0, pipe_1)]
visited = set()
while queue:
distance, current = heapq.heappop(queue)
if current in visited:
continue
visited.add(current)
if current == pipe_2:
return distance
for tunnel in current.tunnels:
heapq.heappush(queue, (distance + 1, pipes[tunnel]))
return -1
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():
for pipe_2 in pipes.values():
distances[pipe_1, pipe_2] = breadth_first_search(pipes, pipe_1, pipe_2)
# 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))

View File

@ -1,6 +1,19 @@
from typing import Any, Iterator # -*- encoding: utf-8 -*-
from ..base import BaseSolver import sys
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:
@ -35,23 +48,8 @@ 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): # part 1 - 13526
def solve(self, input: str) -> Iterator[Any]: print(f"score 1 is {sum(score_1(*v) for v in values)}")
lines = input.splitlines()
# the solution relies on replacing rock / paper / scissor by values 0 / 1 / 2 and using # part 2 - 14204
# modulo-3 arithmetic print(f"score 2 is {sum(score_2(*v) for v in values)}")
#
# 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
yield sum(score_1(*v) for v in values)
# part 2 - 14204
yield sum(score_2(*v) for v in values)

25
2022/day3.py Normal file
View File

@ -0,0 +1,25 @@
# -*- 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 Normal file
View File

@ -0,0 +1,19 @@
# -*- 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 Normal file
View File

@ -0,0 +1,43 @@
# -*- 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 Normal file
View File

@ -0,0 +1,16 @@
# -*- 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 Normal file
View File

@ -0,0 +1,82 @@
# -*- 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 Normal file
View File

@ -0,0 +1,54 @@
# -*- 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}")

View File

@ -1,12 +1,12 @@
import itertools as it # -*- encoding: utf-8 -*-
from typing import Any, Iterator
import sys
import numpy as np import numpy as np
from ..base import BaseSolver
def move(head: tuple[int, int], command: str) -> tuple[int, int]: def move(head: tuple[int, int], command: str) -> tuple[int, int]:
h_col, h_row = head h_col, h_row = head
if command == "L": if command == "L":
@ -22,6 +22,7 @@ def move(head: tuple[int, int], command: str) -> tuple[int, int]:
def follow(head: tuple[int, int], tail: tuple[int, int]) -> tuple[int, int]: def follow(head: tuple[int, int], tail: tuple[int, int]) -> tuple[int, int]:
h_col, h_row = head h_col, h_row = head
t_col, t_row = tail t_col, t_row = tail
@ -32,7 +33,8 @@ def follow(head: tuple[int, int], tail: tuple[int, int]) -> tuple[int, int]:
def run(commands: list[str], n_blocks: int) -> list[tuple[int, int]]: def run(commands: list[str], n_blocks: int) -> list[tuple[int, int]]:
blocks: list[tuple[int, int]] = [(0, 0) for _ in range(n_blocks)]
blocks = [(0, 0) for _ in range(n_blocks)]
visited = [blocks[-1]] visited = [blocks[-1]]
for command in commands: for command in commands:
@ -46,14 +48,17 @@ def run(commands: list[str], n_blocks: int) -> list[tuple[int, int]]:
return visited return visited
class Solver(BaseSolver): lines = sys.stdin.read().splitlines()
def solve(self, input: str) -> Iterator[Any]:
lines = [line.strip() for line in input.splitlines()]
# flatten the commands # flatten the commands
commands = list( commands: list[str] = []
it.chain(*(p[0] * int(p[1]) for line in lines if (p := line.split()))) for line in lines:
) d, c = line.split()
commands.extend(d * int(c))
yield len(set(run(commands, n_blocks=2)))
yield len(set(run(commands, n_blocks=10))) visited_1 = run(commands, n_blocks=2)
print(f"answer 1 is {len(set(visited_1))}")
visited_2 = run(commands, n_blocks=10)
print(f"answer 2 is {len(set(visited_2))}")

View File

@ -1,36 +1,7 @@
# Holt59 - Advent Of Code # Advent Of Code
Installation (with [`poetry`](https://python-poetry.org/)): To run any script, you need to pipe the input:
```bash ```bash
poetry install cat 2022/inputs/day2.txt | python 2022/day2.py
```
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

File diff suppressed because it is too large Load Diff

View File

@ -1,52 +0,0 @@
[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"

3
run.ps1 Normal file
View File

@ -0,0 +1,3 @@
param ($day)
Get-Content ".\2022\inputs\day$day.txt" | python ".\2022\day$day.py"

View File

@ -1,12 +0,0 @@
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)

View File

@ -1,147 +0,0 @@
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)

View File

@ -1,49 +0,0 @@
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))

View File

@ -1,27 +0,0 @@
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")

View File

@ -1,40 +0,0 @@
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)

View File

@ -1,63 +0,0 @@
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

View File

@ -1,56 +0,0 @@
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)

View File

@ -1,57 +0,0 @@
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,
),
)

View File

@ -1,34 +0,0 @@
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
)

View File

@ -1,66 +0,0 @@
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)

View File

@ -1,57 +0,0 @@
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

View File

@ -1,24 +0,0 @@
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
)
)

View File

@ -1,29 +0,0 @@
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)

View File

@ -1,64 +0,0 @@
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

View File

@ -1,181 +0,0 @@
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
)
)

View File

@ -1,107 +0,0 @@
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"]

View File

@ -1,88 +0,0 @@
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)

View File

@ -1,16 +0,0 @@
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

View File

@ -1,33 +0,0 @@
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]))

View File

@ -1,20 +0,0 @@
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")
)

View File

@ -1,36 +0,0 @@
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))

View File

@ -1,32 +0,0 @@
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()

View File

@ -1,96 +0,0 @@
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"]

View File

@ -1,32 +0,0 @@
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
)

View File

@ -1,28 +0,0 @@
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())

View File

@ -1,17 +0,0 @@
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:]))

View File

@ -1,47 +0,0 @@
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]

View File

@ -1,66 +0,0 @@
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

View File

@ -1,64 +0,0 @@
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)))

View File

@ -1,7 +0,0 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@ -1,7 +0,0 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@ -1,7 +0,0 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@ -1,7 +0,0 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@ -1,7 +0,0 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@ -1,7 +0,0 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@ -1,7 +0,0 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@ -1,38 +0,0 @@
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))

View File

@ -1,7 +0,0 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@ -1,7 +0,0 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@ -1,7 +0,0 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@ -1,7 +0,0 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@ -1,7 +0,0 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@ -1,7 +0,0 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@ -1,43 +0,0 @@
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

View File

@ -1,52 +0,0 @@
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

View File

@ -1,48 +0,0 @@
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()

View File

@ -1,21 +0,0 @@
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)

View File

@ -1,22 +0,0 @@
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)
)

View File

@ -1,89 +0,0 @@
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)

View File

@ -1,47 +0,0 @@
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:])

View File

@ -1,12 +0,0 @@
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:])

View File

@ -1,43 +0,0 @@
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"
)

View File

@ -1,176 +0,0 @@
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)

View File

@ -1,42 +0,0 @@
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]

View File

@ -1,146 +0,0 @@
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())

View File

@ -1,95 +0,0 @@
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

View File

@ -1,159 +0,0 @@
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)

Some files were not shown because too many files have changed in this diff Show More