1 Commits

Author SHA1 Message Date
Mikaël Capelle
2fb65387f7 Backup local 2022 day 16. 2023-12-05 20:16:27 +01:00
435 changed files with 2142 additions and 31511 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

5
.gitignore vendored
View File

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

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}")

9
2022/day1.py Normal file
View File

@@ -0,0 +1,9 @@
# -*- encoding: utf-8 -*-
import sys
blocks = sys.stdin.read().split("\n\n")
values = sorted(sum(map(int, block.split())) for block in blocks)
print(f"answer 1 is {values[-1]}")
print(f"answer 2 is {sum(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,14 +123,13 @@ 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
@@ -138,10 +140,7 @@ class Solver(BaseSolver):
# 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,
me_worry_fn=lambda w: w % total_test_value,
)
) )
print(f"answer 2 is {answer_2}")

View File

@@ -1,7 +1,8 @@
import heapq # -*- encoding: utf-8 -*-
from typing import Any, Callable, Iterator, TypeVar
from ..base import BaseSolver import heapq
import sys
from typing import Callable, Iterator, TypeVar
Node = TypeVar("Node") Node = TypeVar("Node")
@@ -43,6 +44,7 @@ def dijkstra(
visited.add(current) visited.add(current)
for neighbor in neighbors(current): for neighbor in neighbors(current):
if neighbor in visited: if neighbor in visited:
continue continue
@@ -58,6 +60,7 @@ def dijkstra(
def make_path(parents: dict[Node, Node], start: Node, end: Node) -> list[Node] | None: def make_path(parents: dict[Node, Node], start: Node, end: Node) -> list[Node] | None:
if end not in parents: if end not in parents:
return None return None
@@ -69,35 +72,7 @@ def make_path(parents: dict[Node, Node], start: Node, end: Node) -> list[Node] |
return list(reversed(path)) return list(reversed(path))
def neighbors( def print_path(path: list[tuple[int, int]], n_rows: int, n_cols: int) -> None:
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, path: list[tuple[int, int]], n_rows: int, n_cols: int) -> None:
end = path[-1] end = path[-1]
graph = [["." for _c in range(n_cols)] for _r in range(n_rows)] graph = [["." for _c in range(n_cols)] for _r in range(n_rows)]
@@ -118,16 +93,42 @@ class Solver(BaseSolver):
else: else:
assert False, "{} -> {} infeasible".format(path[i], path[i + 1]) assert False, "{} -> {} infeasible".format(path[i], path[i + 1])
for row in graph: print("\n".join("".join(row) for row in graph))
self.logger.info("".join(row))
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines() 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] grid = [[ord(cell) - ord("a") for cell in line] for line in lines]
start: tuple[int, int] | None = None start: tuple[int, int]
end: tuple[int, int] | None = None end: tuple[int, int]
# for part 2 # for part 2
start_s: list[tuple[int, int]] = [] start_s: list[tuple[int, int]] = []
@@ -142,27 +143,23 @@ class Solver(BaseSolver):
elif col == 0: elif col == 0:
start_s.append((i_row, i_col)) start_s.append((i_row, i_col))
assert start is not None
assert end is not None
# fix values # fix values
grid[start[0]][start[1]] = 0 grid[start[0]][start[1]] = 0
grid[end[0]][end[1]] = ord("z") - ord("a") grid[end[0]][end[1]] = ord("z") - ord("a")
lengths_1, parents_1 = dijkstra( lengths_1, parents_1 = dijkstra(
start=start, start=start, neighbors=lambda n: neighbors(grid, n, True), cost=lambda lhs, rhs: 1
neighbors=lambda n: neighbors(grid, n, True),
cost=lambda lhs, rhs: 1,
) )
path_1 = make_path(parents_1, start, end) path_1 = make_path(parents_1, start, end)
assert path_1 is not None assert path_1 is not None
self.print_path(path_1, n_rows=len(grid), n_cols=len(grid[0])) print_path(path_1, n_rows=len(grid), n_cols=len(grid[0]))
yield lengths_1[end] - 1
lengths_2, _ = dijkstra( print(f"answer 1 is {lengths_1[end] - 1}")
start=end,
neighbors=lambda n: neighbors(grid, n, False), lengths_2, parents_2 = dijkstra(
cost=lambda lhs, rhs: 1, 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) 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})")

270
2022/day16.py Normal file
View File

@@ -0,0 +1,270 @@
# -*- encoding: utf-8 -*-
from __future__ import annotations
import heapq
import itertools
import re
import sys
import time as time_p
from collections import defaultdict
from typing import FrozenSet, NamedTuple
from tqdm import tqdm, trange
class Pipe(NamedTuple):
name: str
flow: int
tunnels: list[str]
def __lt__(self, other: object) -> bool:
return isinstance(other, Pipe) and other.name < self.name
def __eq__(self, other: object) -> bool:
return isinstance(other, Pipe) and other.name == self.name
def __hash__(self) -> int:
return hash(self.name)
def __str__(self) -> str:
return self.name
def __repr__(self) -> str:
return self.name
def breadth_first_search(pipes: dict[str, Pipe], pipe: Pipe) -> dict[Pipe, int]:
"""
Runs a BFS from the given pipe and return the shortest distance (in term of hops)
to all other pipes.
"""
queue = [(0, pipe_1)]
visited = set()
distances: dict[Pipe, int] = {}
while len(distances) < len(pipes):
distance, current = heapq.heappop(queue)
if current in visited:
continue
visited.add(current)
distances[current] = distance
for tunnel in current.tunnels:
heapq.heappush(queue, (distance + 1, pipes[tunnel]))
return distances
def update_with_better(
node_at_times: dict[FrozenSet[Pipe], int], flow: int, flowing: FrozenSet[Pipe]
) -> None:
node_at_times[flowing] = max(node_at_times[flowing], flow)
def part_1(
start_pipe: Pipe,
max_time: int,
distances: dict[tuple[Pipe, Pipe], int],
relevant_pipes: FrozenSet[Pipe],
):
node_at_times: dict[int, dict[Pipe, dict[FrozenSet[Pipe], int]]] = defaultdict(
lambda: defaultdict(lambda: defaultdict(lambda: 0))
)
node_at_times[0] = {start_pipe: {frozenset(): 0}}
for time in range(max_time):
for c_pipe, nodes in node_at_times[time].items():
for flowing, flow in nodes.items():
for target in relevant_pipes:
distance = distances[c_pipe, target] + 1
if time + distance >= max_time or target in flowing:
continue
update_with_better(
node_at_times[time + distance][target],
flow + sum(pipe.flow for pipe in flowing) * distance,
flowing | {target},
)
update_with_better(
node_at_times[max_time][c_pipe],
flow + sum(pipe.flow for pipe in flowing) * (max_time - time),
flowing,
)
return max(
flow
for nodes_of_pipe in node_at_times[max_time].values()
for flow in nodes_of_pipe.values()
)
def part_2(
start_pipe: Pipe,
max_time: int,
pipes: dict[str, Pipe],
relevant_pipes: FrozenSet[Pipe],
distances: dict[tuple[Pipe, Pipe], int],
):
node_at_times: dict[
int, dict[tuple[Pipe, Pipe], dict[FrozenSet[Pipe], int]]
] = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: 0)))
node_at_times[0] = {(start_pipe, start_pipe): {frozenset(): 0}}
# map node + distance to
d1, d2, d3, d4 = 0, 0, 0, 0
best_flow = 0
for time in range(max_time):
print(
f"{time + 1:2d}/{max_time} - {best_flow:4d} - "
f"{sum(map(len, node_at_times[time].values())):7d} - "
f"{d1:.3f} {d2:.3f} {d3:.3f} {d4:.3f}"
)
d1, d2, d3, d4 = 0, 0, 0, 0
for (c_pipe, e_pipe), nodes in node_at_times[time].items():
for flowing, flow in nodes.items():
t1 = time_p.time()
c_best_flow = (
flow
+ sum(pipe.flow for pipe in flowing) * (max_time - time)
+ sum(
(
pipe.flow
* (
max_time
- time
- 1
- min(distances[c_pipe, pipe], distances[e_pipe, pipe])
)
for pipe in relevant_pipes
if pipe not in flowing
),
start=0,
)
)
d1 += time_p.time() - t1
if c_best_flow < best_flow:
continue
best_flow = max(
best_flow,
flow + sum(pipe.flow for pipe in flowing) * (max_time - time),
)
t1 = time_p.time()
if flowing != relevant_pipes:
for c_next_s, e_next_s in itertools.product(
c_pipe.tunnels, e_pipe.tunnels
):
c_next = pipes[c_next_s]
e_next = pipes[e_next_s]
update_with_better(
node_at_times[time + 1][c_next, e_next],
flow + sum(pipe.flow for pipe in flowing),
flowing,
)
d2 += time_p.time() - t1
t1 = time_p.time()
if c_pipe in relevant_pipes and c_pipe not in flowing:
for e_next_s in e_pipe.tunnels:
e_next = pipes[e_next_s]
update_with_better(
node_at_times[time + 1][c_pipe, e_next],
flow + sum(pipe.flow for pipe in flowing),
flowing | {c_pipe},
)
if e_pipe in relevant_pipes and e_pipe not in flowing:
for c_next_s in c_pipe.tunnels:
c_next = pipes[c_next_s]
update_with_better(
node_at_times[time + 1][c_next, e_pipe],
flow + sum(pipe.flow for pipe in flowing),
flowing | {e_pipe},
)
if (
e_pipe in relevant_pipes
and c_pipe in relevant_pipes
and e_pipe not in flowing
and c_pipe not in flowing
):
update_with_better(
node_at_times[time + 1][c_pipe, e_pipe],
flow + sum(pipe.flow for pipe in flowing),
flowing | {c_pipe, e_pipe},
)
update_with_better(
node_at_times[max_time][c_pipe, e_pipe],
flow + sum(pipe.flow for pipe in flowing) * (max_time - time),
flowing,
)
d3 += time_p.time() - t1
return max(
flow
for nodes_of_pipe in node_at_times[max_time].values()
for flow in nodes_of_pipe.values()
)
# === MAIN ===
lines = sys.stdin.read().splitlines()
pipes: dict[str, Pipe] = {}
for line in lines:
r = re.match(
R"Valve ([A-Z]+) has flow rate=([0-9]+); tunnels? leads? to valves? (.+)",
line,
)
assert r
g = r.groups()
pipes[g[0]] = Pipe(g[0], int(g[1]), g[2].split(", "))
# compute distances from one valve to any other
distances: dict[tuple[Pipe, Pipe], int] = {}
for pipe_1 in pipes.values():
distances.update(
{
(pipe_1, pipe_2): distance
for pipe_2, distance in breadth_first_search(pipes, pipe_1).items()
}
)
# valves with flow
relevant_pipes = frozenset(pipe for pipe in pipes.values() if pipe.flow > 0)
# 1651, 1653
print(part_1(pipes["AA"], 30, distances, relevant_pipes))
# 1707, 2223
print(part_2(pipes["AA"], 26, pipes, relevant_pipes, distances))

View File

@@ -1,16 +1,14 @@
from typing import Any, Iterator, Sequence, TypeAlias, TypeVar # -*- encoding: utf-8 -*-
import sys
from typing import Sequence, TypeVar
import numpy as np import numpy as np
from numpy.typing import NDArray
from ..base import BaseSolver
T = TypeVar("T") T = TypeVar("T")
Tower: TypeAlias = NDArray[np.bool]
def print_tower(tower: np.ndarray, out: str = "#"):
def print_tower(tower: Tower, out: str = "#"):
print("-" * (tower.shape[1] + 2)) print("-" * (tower.shape[1] + 2))
non_empty = False non_empty = False
for row in reversed(range(1, tower.shape[0])): for row in reversed(range(1, tower.shape[0])):
@@ -21,7 +19,7 @@ def print_tower(tower: Tower, out: str = "#"):
print("+" + "-" * tower.shape[1] + "+") print("+" + "-" * tower.shape[1] + "+")
def tower_height(tower: Tower) -> int: def tower_height(tower: np.ndarray) -> int:
return int(tower.shape[0] - tower[::-1, :].argmax(axis=0).min() - 1) return int(tower.shape[0] - tower[::-1, :].argmax(axis=0).min() - 1)
@@ -49,8 +47,9 @@ def build_tower(
n_rocks: int, n_rocks: int,
jets: str, jets: str,
early_stop: bool = False, early_stop: bool = False,
init: Tower = np.ones(WIDTH, dtype=bool), init: np.ndarray = np.ones(WIDTH, dtype=bool),
) -> tuple[Tower, int, int, dict[int, int]]: ) -> tuple[np.ndarray, int, int, dict[int, int]]:
tower = EMPTY_BLOCKS.copy() tower = EMPTY_BLOCKS.copy()
tower[0, :] = init tower[0, :] = init
@@ -60,6 +59,7 @@ def build_tower(
rock_count = 0 rock_count = 0
for rock_count in range(n_rocks): for rock_count in range(n_rocks):
if early_stop: if early_stop:
if i_rock == 0 and (i_rock, i_jet) in done_at: if i_rock == 0 and (i_rock, i_jet) in done_at:
break break
@@ -75,6 +75,7 @@ def build_tower(
tower = np.concatenate([tower, EMPTY_BLOCKS], axis=0) tower = np.concatenate([tower, EMPTY_BLOCKS], axis=0)
while True: while True:
jet, i_jet = next_cycle(jets, i_jet) jet, i_jet = next_cycle(jets, i_jet)
dx = 0 dx = 0
@@ -99,13 +100,14 @@ def build_tower(
return tower, rock_count, done_at.get((i_rock, i_jet), -1), heights return tower, rock_count, done_at.get((i_rock, i_jet), -1), heights
class Solver(BaseSolver): line = sys.stdin.read().strip()
def solve(self, input: str) -> Iterator[Any]:
tower, *_ = build_tower(2022, input) tower, *_ = build_tower(2022, line)
yield tower_height(tower) answer_1 = tower_height(tower)
print(f"answer 1 is {answer_1}")
TOTAL_ROCKS = 1_000_000_000_000 TOTAL_ROCKS = 1_000_000_000_000
_tower_1, n_rocks_1, prev_1, heights_1 = build_tower(TOTAL_ROCKS, input, True) tower_1, n_rocks_1, prev_1, heights_1 = build_tower(TOTAL_ROCKS, line, True)
assert prev_1 > 0 assert prev_1 > 0
# 2767 1513 # 2767 1513
@@ -119,4 +121,5 @@ class Solver(BaseSolver):
heights_1[prev_1 + remaining_rocks % n_repeat_rocks] - heights_1[prev_1] heights_1[prev_1 + remaining_rocks % n_repeat_rocks] - heights_1[prev_1]
) )
yield base_height + (n_repeat_towers + 1) * repeat_height + remaining_height answer_2 = base_height + (n_repeat_towers + 1) * repeat_height + remaining_height
print(f"answer 2 is {answer_2}")

53
2022/day18.py Normal file
View File

@@ -0,0 +1,53 @@
# -*- encoding: utf-8 -*-
import sys
from typing import FrozenSet
import numpy as np
xyz = np.asarray(
[
tuple(int(x) for x in row.split(",")) # type: ignore
for row in sys.stdin.read().splitlines()
]
)
xyz = xyz - xyz.min(axis=0) + 1
cubes = np.zeros(xyz.max(axis=0) + 3, dtype=bool)
cubes[xyz[:, 0], xyz[:, 1], xyz[:, 2]] = True
n_dims = len(cubes.shape)
faces = [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1)]
answer_1 = sum(
1 for x, y, z in xyz for dx, dy, dz in faces if not cubes[x + dx, y + dy, z + dz]
)
print(f"answer 1 is {answer_1}")
visited = np.zeros_like(cubes, dtype=bool)
queue = [(0, 0, 0)]
n_faces = 0
while queue:
x, y, z = queue.pop(0)
if visited[x, y, z]:
continue
visited[x, y, z] = True
for dx, dy, dz in faces:
nx, ny, nz = x + dx, y + dy, z + dz
if not all(n >= 0 and n < cubes.shape[i] for i, n in enumerate((nx, ny, nz))):
continue
if visited[nx, ny, nz]:
continue
if cubes[nx, ny, nz]:
n_faces += 1
else:
queue.append((nx, ny, nz))
print(f"answer 2 is {n_faces}")

View File

@@ -1,10 +1,11 @@
from typing import Any, Iterator, Literal # -*- encoding: utf-8 -*-
import sys
from typing import Literal
import numpy as np import numpy as np
import parse # pyright: ignore[reportMissingTypeStubs] import parse
from numpy.typing import NDArray from tqdm import tqdm
from ..base import BaseSolver
Reagent = Literal["ore", "clay", "obsidian", "geode"] Reagent = Literal["ore", "clay", "obsidian", "geode"]
REAGENTS: tuple[Reagent, ...] = ( REAGENTS: tuple[Reagent, ...] = (
@@ -36,7 +37,7 @@ class State:
self.robots = robots self.robots = robots
self.reagents = reagents self.reagents = reagents
def __eq__(self, other: object) -> bool: def __eq__(self, other) -> bool:
return ( return (
isinstance(other, State) isinstance(other, State)
and self.robots == other.robots and self.robots == other.robots
@@ -63,7 +64,31 @@ def dominates(lhs: State, rhs: State):
) )
lines = sys.stdin.read().splitlines()
blueprints: list[dict[Reagent, IntOfReagent]] = []
for line in lines:
r = parse.parse(
"Blueprint {}: "
"Each ore robot costs {:d} ore. "
"Each clay robot costs {:d} ore. "
"Each obsidian robot costs {:d} ore and {:d} clay. "
"Each geode robot costs {:d} ore and {:d} obsidian.",
line,
)
blueprints.append(
{
"ore": {"ore": r[1]},
"clay": {"ore": r[2]},
"obsidian": {"ore": r[3], "clay": r[4]},
"geode": {"ore": r[5], "obsidian": r[6]},
}
)
def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int: def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int:
# since we can only build one robot per time, we do not need more than X robots # since we can only build one robot per time, we do not need more than X robots
# of type K where X is the maximum number of K required among all robots, e.g., # of type K where X is the maximum number of K required among all robots, e.g.,
# in the first toy blueprint, we need at most 4 ore robots, 14 clay ones and 7 # in the first toy blueprint, we need at most 4 ore robots, 14 clay ones and 7
@@ -72,12 +97,12 @@ def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int:
name: max(blueprint[r].get(name, 0) for r in REAGENTS) for name in REAGENTS name: max(blueprint[r].get(name, 0) for r in REAGENTS) for name in REAGENTS
} }
state_after_t: dict[int, set[State]] = {0: {State()}} state_after_t: dict[int, set[State]] = {0: [State()]}
for t in range(1, max_time + 1): for t in range(1, max_time + 1):
# list of new states at the end of step t that we are going to prune later # list of new states at the end of step t that we are going to prune later
states_for_t: set[State] = set() states_for_t: set[State] = set()
robots_that_can_be_built: list[Reagent]
for state in state_after_t[t - 1]: for state in state_after_t[t - 1]:
robots_that_can_be_built = [ robots_that_can_be_built = [
@@ -111,7 +136,7 @@ def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int:
for robot in robots_that_can_be_built: for robot in robots_that_can_be_built:
robots = state.robots.copy() robots = state.robots.copy()
robots[robot] += 1 robots[robot] += 1
reagents: IntOfReagent = { reagents = {
reagent: state.reagents[reagent] reagent: state.reagents[reagent]
+ state.robots[reagent] + state.robots[reagent]
- blueprint[robot].get(reagent, 0) - blueprint[robot].get(reagent, 0)
@@ -130,7 +155,7 @@ def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int:
] ]
) )
to_keep: list[NDArray[np.integer[Any]]] = [] to_keep = []
while len(np_states) > 0: while len(np_states) > 0:
first_dom = (np_states[1:] >= np_states[0]).all(axis=1).any() first_dom = (np_states[1:] >= np_states[0]).all(axis=1).any()
@@ -151,31 +176,11 @@ def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int:
return max(state.reagents["geode"] for state in state_after_t[max_time]) return max(state.reagents["geode"] for state in state_after_t[max_time])
class Solver(BaseSolver): answer_1 = sum(
def solve(self, input: str) -> Iterator[Any]:
blueprints: list[dict[Reagent, IntOfReagent]] = []
for line in input.splitlines():
r: list[int] = parse.parse( # type: ignore
"Blueprint {}: "
"Each ore robot costs {:d} ore. "
"Each clay robot costs {:d} ore. "
"Each obsidian robot costs {:d} ore and {:d} clay. "
"Each geode robot costs {:d} ore and {:d} obsidian.",
line,
)
blueprints.append(
{
"ore": {"ore": r[1]},
"clay": {"ore": r[2]},
"obsidian": {"ore": r[3], "clay": r[4]},
"geode": {"ore": r[5], "obsidian": r[6]},
}
)
yield sum(
(i_blueprint + 1) * run(blueprint, 24) (i_blueprint + 1) * run(blueprint, 24)
for i_blueprint, blueprint in enumerate(blueprints) for i_blueprint, blueprint in enumerate(blueprints)
) )
print(f"answer 1 is {answer_1}")
yield (run(blueprints[0], 32) * run(blueprints[1], 32) * run(blueprints[2], 32)) answer_2 = run(blueprints[0], 32) * run(blueprints[1], 32) * run(blueprints[2], 32)
print(f"answer 2 is {answer_2}")

View File

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

View File

@@ -1,8 +1,8 @@
# -*- encoding: utf-8 -*-
from __future__ import annotations from __future__ import annotations
from typing import Any, Iterator import sys
from ..base import BaseSolver
class Number: class Number:
@@ -21,6 +21,7 @@ class Number:
def decrypt(numbers: list[Number], key: int, rounds: int) -> int: def decrypt(numbers: list[Number], key: int, rounds: int) -> int:
numbers = numbers.copy() numbers = numbers.copy()
original = numbers.copy() original = numbers.copy()
@@ -67,9 +68,10 @@ def decrypt(numbers: list[Number], key: int, rounds: int) -> int:
) )
class Solver(BaseSolver): numbers = [Number(int(x)) for i, x in enumerate(sys.stdin.readlines())]
def solve(self, input: str) -> Iterator[Any]:
numbers = [Number(int(x)) for x in input.splitlines()]
yield decrypt(numbers, 1, 1) answer_1 = decrypt(numbers, 1, 1)
yield decrypt(numbers, 811589153, 10) print(f"answer 1 is {answer_1}")
answer_2 = decrypt(numbers, 811589153, 10)
print(f"answer 2 is {answer_2}")

View File

@@ -1,7 +1,8 @@
import operator # -*- encoding: utf-8 -*-
from typing import Any, Callable, Iterator
from ..base import BaseSolver import operator
import sys
from typing import Callable
def compute(monkeys: dict[str, int | tuple[str, str, str]], monkey: str) -> int: def compute(monkeys: dict[str, int | tuple[str, str, str]], monkey: str) -> int:
@@ -78,9 +79,7 @@ def invert(
return monkeys return monkeys
class Solver(BaseSolver): lines = sys.stdin.read().splitlines()
def solve(self, input: str) -> Iterator[Any]:
lines = [line.strip() for line in input.splitlines()]
monkeys: dict[str, int | tuple[str, str, str]] = {} monkeys: dict[str, int | tuple[str, str, str]] = {}
@@ -99,10 +98,12 @@ class Solver(BaseSolver):
op_monkeys.add(name) op_monkeys.add(name)
yield compute(monkeys.copy(), "root")
answer_1 = compute(monkeys.copy(), "root")
print(f"answer 1 is {answer_1}")
# assume the second operand of 'root' can be computed, and the first one depends on # assume the second operand of 'root' can be computed, and the first one depends on
# humn, which is the case is my input and the test input # humn, which is the case is my input and the test input
assert isinstance(monkeys["root"], tuple)
p1, _, p2 = monkeys["root"] # type: ignore p1, _, p2 = monkeys["root"] # type: ignore
yield compute(invert(monkeys, "humn", compute(monkeys.copy(), p2)), "humn") answer_2 = compute(invert(monkeys, "humn", compute(monkeys.copy(), p2)), "humn")
print(f"answer 2 is {answer_2}")

226
2022/day22.py Normal file
View File

@@ -0,0 +1,226 @@
# -*- encoding: utf-8 -*-
import re
import sys
from typing import Callable
import numpy as np
VOID, EMPTY, WALL = 0, 1, 2
TILE_FROM_CHAR = {" ": VOID, ".": EMPTY, "#": WALL}
SCORES = {"E": 0, "S": 1, "W": 2, "N": 3}
board_map_s, direction_s = sys.stdin.read().split("\n\n")
# board
board_lines = board_map_s.splitlines()
max_line = max(len(line) for line in board_lines)
board = np.array(
[
[TILE_FROM_CHAR[c] for c in row] + [VOID] * (max_line - len(row))
for row in board_map_s.splitlines()
]
)
directions = [
int(p1) if p2 else p1 for p1, p2 in re.findall(R"(([0-9])+|L|R)", direction_s)
]
# find on each row and column the first and last non-void
row_first_non_void = np.argmax(board != VOID, axis=1)
row_last_non_void = board.shape[1] - np.argmax(board[:, ::-1] != VOID, axis=1) - 1
col_first_non_void = np.argmax(board != VOID, axis=0)
col_last_non_void = board.shape[0] - np.argmax(board[::-1, :] != VOID, axis=0) - 1
faces = np.zeros_like(board)
size = np.gcd(board.shape[0], board.shape[1])
for row in range(0, board.shape[0], size):
for col in range(row_first_non_void[row], row_last_non_void[row], size):
faces[row : row + size, col : col + size] = faces.max() + 1
SIZE = np.gcd(*board.shape)
# TODO: deduce this from the actual cube...
faces_wrap: dict[int, dict[str, Callable[[int, int], tuple[int, int, str]]]]
if board.shape == (12, 16): # example
faces_wrap = {
1: {
"W": lambda y, x: (4, 4 + y, "S"), # 3N
"N": lambda y, x: (4, 11 - x, "S"), # 2N
"E": lambda y, x: (11 - y, 15, "W"), # 6E
},
2: {
"W": lambda y, x: (11, 19 - y, "N"), # 6S
"N": lambda y, x: (0, 11 - y, "S"), # 1N
"S": lambda y, x: (11, 11 - x, "N"), # 5S
},
3: {
"N": lambda y, x: (x - 4, 8, "E"), # 1W
"S": lambda y, x: (15 - x, 8, "E"), # 5W
},
4: {"E": lambda y, x: (8, 19 - y, "S")}, # 6N
5: {
"W": lambda y, x: (7, 15 - y, "N"), # 3S
"S": lambda y, x: (7, 11 - x, "N"), # 2S
},
6: {
"N": lambda y, x: (19 - x, 11, "W"), # 4E
"E": lambda y, x: (11 - y, 11, "W"), # 1E
"S": lambda y, x: (19 - x, 0, "E"), # 2W
},
}
else:
faces_wrap = {
1: {
"W": lambda y, x: (3 * SIZE - y - 1, 0, "E"), # 4W
"N": lambda y, x: (2 * SIZE + x, 0, "E"), # 6W
},
2: {
"N": lambda y, x: (4 * SIZE - 1, x - 2 * SIZE, "N"), # 6S
"E": lambda y, x: (3 * SIZE - y - 1, 2 * SIZE - 1, "W"), # 5E
"S": lambda y, x: (x - SIZE, 2 * SIZE - 1, "W"), # 3E
},
3: {
"W": lambda y, x: (2 * SIZE, y - SIZE, "S"), # 4N
"E": lambda y, x: (SIZE - 1, SIZE + y, "N"), # 2S
},
4: {
"W": lambda y, x: (3 * SIZE - y - 1, SIZE, "E"), # 1W
"N": lambda y, x: (SIZE + x, SIZE, "E"), # 3W
},
5: {
"E": lambda y, x: (3 * SIZE - y - 1, 3 * SIZE - 1, "W"), # 2E
"S": lambda y, x: (2 * SIZE + x, SIZE - 1, "W"), # 6E
},
6: {
"W": lambda y, x: (0, y - 2 * SIZE, "S"), # 1N
"E": lambda y, x: (3 * SIZE - 1, y - 2 * SIZE, "N"), # 5S
"S": lambda y, x: (0, x + 2 * SIZE, "S"), # 2N
},
}
def wrap_part_1(y0: int, x0: int, r0: str) -> tuple[int, int, str]:
if r0 == "E":
return y0, row_first_non_void[y0], r0
elif r0 == "S":
return col_first_non_void[x0], x0, r0
elif r0 == "W":
return y0, row_last_non_void[y0], r0
elif r0 == "N":
return col_last_non_void[x0], x0, r0
assert False
def wrap_part_2(y0: int, x0: int, r0: str) -> tuple[int, int, str]:
cube = faces[y0, x0]
assert r0 in faces_wrap[cube]
return faces_wrap[cube][r0](y0, x0)
def run(wrap: Callable[[int, int, str], tuple[int, int, str]]) -> tuple[int, int, str]:
y0 = 0
x0 = np.where(board[0] == EMPTY)[0][0]
r0 = "E"
for direction in directions:
if isinstance(direction, int):
while direction > 0:
if r0 == "E":
xi = np.where(board[y0, x0 + 1 : x0 + direction + 1] == WALL)[0]
if len(xi):
x0 = x0 + xi[0]
direction = 0
elif (
x0 + direction < board.shape[1]
and board[y0, x0 + direction] == EMPTY
):
x0 = x0 + direction
direction = 0
else:
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
if board[y0_t, x0_t] == WALL:
x0 = row_last_non_void[y0]
direction = 0
else:
direction = direction - (row_last_non_void[y0] - x0) - 1
y0, x0, r0 = y0_t, x0_t, r0_t
elif r0 == "S":
yi = np.where(board[y0 + 1 : y0 + direction + 1, x0] == WALL)[0]
if len(yi):
y0 = y0 + yi[0]
direction = 0
elif (
y0 + direction < board.shape[0]
and board[y0 + direction, x0] == EMPTY
):
y0 = y0 + direction
direction = 0
else:
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
if board[y0_t, x0_t] == WALL:
y0 = col_last_non_void[x0]
direction = 0
else:
direction = direction - (col_last_non_void[x0] - y0) - 1
y0, x0, r0 = y0_t, x0_t, r0_t
elif r0 == "W":
left = max(x0 - direction - 1, 0)
xi = np.where(board[y0, left:x0] == WALL)[0]
if len(xi):
x0 = left + xi[-1] + 1
direction = 0
elif x0 - direction >= 0 and board[y0, x0 - direction] == EMPTY:
x0 = x0 - direction
direction = 0
else:
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
if board[y0_t, x0_t] == WALL:
x0 = row_first_non_void[y0]
direction = 0
else:
direction = direction - (x0 - row_first_non_void[y0]) - 1
y0, x0, r0 = y0_t, x0_t, r0_t
elif r0 == "N":
top = max(y0 - direction - 1, 0)
yi = np.where(board[top:y0, x0] == WALL)[0]
if len(yi):
y0 = top + yi[-1] + 1
direction = 0
elif y0 - direction >= 0 and board[y0 - direction, x0] == EMPTY:
y0 = y0 - direction
direction = 0
else:
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
if board[y0_t, x0_t] == WALL:
y0 = col_first_non_void[x0]
direction = 0
else:
direction = direction - (y0 - col_first_non_void[x0]) - 1
y0, x0, r0 = y0_t, x0_t, r0_t
else:
r0 = {
"E": {"L": "N", "R": "S"},
"N": {"L": "W", "R": "E"},
"W": {"L": "S", "R": "N"},
"S": {"L": "E", "R": "W"},
}[r0][direction]
return y0, x0, r0
y1, x1, r1 = run(wrap_part_1)
answer_1 = 1000 * (1 + y1) + 4 * (1 + x1) + SCORES[r1]
print(f"answer 1 is {answer_1}")
y2, x2, r2 = run(wrap_part_2)
answer_2 = 1000 * (1 + y2) + 4 * (1 + x2) + SCORES[r2]
print(f"answer 2 is {answer_2}")

View File

@@ -1,8 +1,8 @@
import itertools # -*- encoding: utf-8 -*-
from collections import defaultdict
from typing import Any, Iterator
from ..base import BaseSolver import itertools
import sys
from collections import defaultdict
Directions = list[ Directions = list[
tuple[ tuple[
@@ -20,7 +20,7 @@ DIRECTIONS: Directions = [
def min_max_yx(positions: set[tuple[int, int]]) -> tuple[int, int, int, int]: def min_max_yx(positions: set[tuple[int, int]]) -> tuple[int, int, int, int]:
ys, xs = {y for y, _x in positions}, {x for _y, x in positions} ys, xs = {y for y, x in positions}, {x for y, x in positions}
return min(ys), min(xs), max(ys), max(xs) return min(ys), min(xs), max(ys), max(xs)
@@ -41,7 +41,7 @@ def round(
directions: Directions, directions: Directions,
): ):
to_move: dict[tuple[int, int], list[tuple[int, int]]] = defaultdict(lambda: []) to_move: dict[tuple[int, int], list[tuple[int, int]]] = defaultdict(lambda: [])
for y, x in positions: for (y, x) in positions:
elves = { elves = {
(dy, dx): (y + dy, x + dx) in positions (dy, dx): (y + dy, x + dx) in positions
for dy, dx in itertools.product((-1, 0, 1), (-1, 0, 1)) for dy, dx in itertools.product((-1, 0, 1), (-1, 0, 1))
@@ -71,11 +71,9 @@ def round(
directions.append(directions.pop(0)) directions.append(directions.pop(0))
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
POSITIONS = { POSITIONS = {
(i, j) (i, j)
for i, row in enumerate(input.splitlines()) for i, row in enumerate(sys.stdin.read().splitlines())
for j, col in enumerate(row) for j, col in enumerate(row)
if col == "#" if col == "#"
} }
@@ -83,15 +81,14 @@ class Solver(BaseSolver):
# === part 1 === # === part 1 ===
p1, d1 = POSITIONS.copy(), DIRECTIONS.copy() p1, d1 = POSITIONS.copy(), DIRECTIONS.copy()
for _ in range(10): for r in range(10):
round(p1, d1) round(p1, d1)
min_y, min_x, max_y, max_x = min_max_yx(p1) min_y, min_x, max_y, max_x = min_max_yx(p1)
yield sum( answer_1 = sum(
(y, x) not in p1 (y, x) not in p1 for y in range(min_y, max_y + 1) for x in range(min_x, max_x + 1)
for y in range(min_y, max_y + 1)
for x in range(min_x, max_x + 1)
) )
print(f"answer 1 is {answer_1}")
# === part 2 === # === part 2 ===
@@ -105,4 +102,4 @@ class Solver(BaseSolver):
if backup == p2: if backup == p2:
break break
yield answer_2 print(f"answer 2 is {answer_2}")

100
2022/day24.py Normal file
View File

@@ -0,0 +1,100 @@
# -*- encoding: utf-8 -*-
import heapq
import math
import sys
from collections import defaultdict
lines = sys.stdin.read().splitlines()
winds = {
(i - 1, j - 1, lines[i][j])
for i in range(1, len(lines) - 1)
for j in range(1, len(lines[i]) - 1)
if lines[i][j] != "."
}
n_rows, n_cols = len(lines) - 2, len(lines[0]) - 2
CYCLE = math.lcm(n_rows, n_cols)
east_winds = [{j for j in range(n_cols) if (i, j, ">") in winds} for i in range(n_rows)]
west_winds = [{j for j in range(n_cols) if (i, j, "<") in winds} for i in range(n_rows)]
north_winds = [
{i for i in range(n_rows) if (i, j, "^") in winds} for j in range(n_cols)
]
south_winds = [
{i for i in range(n_rows) if (i, j, "v") in winds} for j in range(n_cols)
]
def run(start: tuple[int, int], start_cycle: int, end: tuple[int, int]):
def heuristic(y: int, x: int) -> int:
return abs(end[0] - y) + abs(end[1] - x)
# (distance + heuristic, distance, (start_pos, cycle))
queue = [(heuristic(start[0], start[1]), 0, ((start[0], start[1]), start_cycle))]
visited: set[tuple[tuple[int, int], int]] = set()
distances: dict[tuple[int, int], dict[int, int]] = defaultdict(lambda: {})
while queue:
_, distance, ((y, x), cycle) = heapq.heappop(queue)
if ((y, x), cycle) in visited:
continue
distances[y, x][cycle] = distance
visited.add(((y, x), cycle))
if (y, x) == (end[0], end[1]):
break
for dy, dx in (0, 0), (-1, 0), (1, 0), (0, -1), (0, 1):
ty = y + dy
tx = x + dx
n_cycle = (cycle + 1) % CYCLE
if (ty, tx) == end:
heapq.heappush(queue, (distance + 1, distance + 1, ((ty, tx), n_cycle)))
break
if ((ty, tx), n_cycle) in visited:
continue
if (ty, tx) != start and (ty < 0 or tx < 0 or ty >= n_rows or tx >= n_cols):
continue
if (ty, tx) != start:
if (ty - n_cycle) % n_rows in south_winds[tx]:
continue
if (ty + n_cycle) % n_rows in north_winds[tx]:
continue
if (tx + n_cycle) % n_cols in west_winds[ty]:
continue
if (tx - n_cycle) % n_cols in east_winds[ty]:
continue
heapq.heappush(
queue,
((heuristic(ty, tx) + distance + 1, distance + 1, ((ty, tx), n_cycle))),
)
return distances, next(iter(distances[end].values()))
start = (
-1,
next(j for j in range(1, len(lines[0]) - 1) if lines[0][j] == ".") - 1,
)
end = (
n_rows,
next(j for j in range(1, len(lines[-1]) - 1) if lines[-1][j] == ".") - 1,
)
distances_1, forward_1 = run(start, 0, end)
print(f"answer 1 is {forward_1}")
distances_2, return_1 = run(end, next(iter(distances_1[end].keys())), start)
distances_3, forward_2 = run(start, next(iter(distances_2[start].keys())), end)
print(f"answer 2 is {forward_1 + return_1 + forward_2}")

29
2022/day25.py Normal file
View File

@@ -0,0 +1,29 @@
# -*- encoding: utf-8 -*-
import sys
lines = sys.stdin.read().splitlines()
coeffs = {"2": 2, "1": 1, "0": 0, "-": -1, "=": -2}
def snafu2number(number: str) -> int:
value = 0
for c in number:
value *= 5
value += coeffs[c]
return value
def number2snafu(number: int) -> str:
values = ["0", "1", "2", "=", "-"]
res = ""
while number > 0:
mod = number % 5
res = res + values[mod]
number = number // 5 + int(mod >= 3)
return "".join(reversed(res))
answer_1 = number2snafu(sum(map(snafu2number, lines)))
print(f"answer 1 is {answer_1}")

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(lines[i]).intersection(*lines[i + 1 : 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}")

17
2022/day6.py Normal file
View File

@@ -0,0 +1,17 @@
# -*- encoding: utf-8 -*-
import sys
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
data = sys.stdin.read().strip()
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))}")

45
2023/day1.py Normal file
View File

@@ -0,0 +1,45 @@
import sys
lines = sys.stdin.read().splitlines()
lookups_1 = {str(d): d for d in range(1, 10)}
lookups_2 = lookups_1 | {
d: i + 1
for i, d in enumerate(
(
"one",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
)
)
}
def find_values(lookups: dict[str, int]) -> list[int]:
values: list[int] = []
for line in filter(bool, lines):
first_digit = min(
lookups,
key=lambda lookup: index
if (index := line.find(lookup)) >= 0
else len(line),
)
last_digit = max(
lookups,
key=lambda lookup: index if (index := line.rfind(lookup)) >= 0 else -1,
)
values.append(10 * lookups[first_digit] + lookups[last_digit])
return values
print(f"answer 1 is {sum(find_values(lookups_1))}")
print(f"answer 2 is {sum(find_values(lookups_2))}")

13
2023/day10.py Normal file
View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

13
2023/day11.py Normal file
View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

13
2023/day12.py Normal file
View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

13
2023/day13.py Normal file
View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

13
2023/day14.py Normal file
View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

13
2023/day15.py Normal file
View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

13
2023/day16.py Normal file
View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

13
2023/day17.py Normal file
View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

13
2023/day18.py Normal file
View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

13
2023/day19.py Normal file
View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

45
2023/day2.py Normal file
View File

@@ -0,0 +1,45 @@
import operator
import sys
from functools import reduce
from typing import Literal, TypeAlias, cast
CubeType: TypeAlias = Literal["red", "blue", "green"]
MAX_CUBES: dict[CubeType, int] = {"red": 12, "green": 13, "blue": 14}
# parse games
lines = sys.stdin.read().splitlines()
games: dict[int, list[dict[CubeType, int]]] = {}
for line in filter(bool, lines):
id_part, sets_part = line.split(":")
games[int(id_part.split(" ")[-1])] = [
{
cast(CubeType, s[1]): int(s[0])
for cube_draw in cube_set_s.strip().split(", ")
if (s := cube_draw.split(" "))
}
for cube_set_s in sets_part.strip().split(";")
]
# part 1
answer_1 = sum(
id
for id, set_of_cubes in games.items()
if all(
n_cubes <= MAX_CUBES[cube]
for cube_set in set_of_cubes
for cube, n_cubes in cube_set.items()
)
)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = sum(
reduce(
operator.mul,
(max(cube_set.get(cube, 0) for cube_set in set_of_cubes) for cube in MAX_CUBES),
)
for set_of_cubes in games.values()
)
print(f"answer 2 is {answer_2}")

13
2023/day20.py Normal file
View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

13
2023/day21.py Normal file
View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

13
2023/day22.py Normal file
View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

13
2023/day23.py Normal file
View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

13
2023/day24.py Normal file
View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

13
2023/day25.py Normal file
View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

53
2023/day3.py Normal file
View File

@@ -0,0 +1,53 @@
import string
import sys
from collections import defaultdict
NOT_A_SYMBOL = "." + string.digits
lines = sys.stdin.read().splitlines()
values: list[int] = []
gears: dict[tuple[int, int], list[int]] = defaultdict(list)
for i, line in enumerate(lines):
j = 0
while j < len(line):
# skip everything until a digit is found (start of a number)
if line[j] not in string.digits:
j += 1
continue
# extract the range of the number and its value
k = j + 1
while k < len(line) and line[k] in string.digits:
k += 1
value = int(line[j:k])
# lookup around the number if there is a symbol - we go through the number
# itself but that should not matter since it only contains digits
found = False
for i2 in range(max(0, i - 1), min(i + 1, len(lines) - 1) + 1):
for j2 in range(max(0, j - 1), min(k, len(line) - 1) + 1):
assert i2 >= 0 and i2 < len(lines)
assert j2 >= 0 and j2 < len(line)
if lines[i2][j2] not in NOT_A_SYMBOL:
found = True
if lines[i2][j2] == "*":
gears[i2, j2].append(value)
if found:
values.append(value)
# continue starting from the end of the number
j = k
# part 1
answer_1 = sum(values)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = sum(v1 * v2 for v1, v2 in filter(lambda vs: len(vs) == 2, gears.values()))
print(f"answer 2 is {answer_2}")

41
2023/day4.py Normal file
View File

@@ -0,0 +1,41 @@
import sys
from dataclasses import dataclass
@dataclass(frozen=True)
class Card:
id: int
numbers: list[int]
values: list[int]
lines = sys.stdin.read().splitlines()
cards: list[Card] = []
for line in lines:
id_part, e_part = line.split(":")
numbers_s, values_s = e_part.split("|")
cards.append(
Card(
id=int(id_part.split()[1]),
numbers=[int(v.strip()) for v in numbers_s.strip().split()],
values=[int(v.strip()) for v in values_s.strip().split()],
)
)
winnings = [sum(1 for n in card.values if n in card.numbers) for card in cards]
# part 1
answer_1 = sum(2 ** (winning - 1) for winning in winnings if winning > 0)
print(f"answer 1 is {answer_1}")
# part 2
card2cards = {i: list(range(i + 1, i + w + 1)) for i, w in enumerate(winnings)}
card2values = {i: 0 for i in range(len(cards))}
for i in range(len(cards)):
card2values[i] += 1
for j in card2cards[i]:
card2values[j] += card2values[i]
print(f"answer 2 is {sum(card2values.values())}")

129
2023/day5.py Normal file
View File

@@ -0,0 +1,129 @@
import sys
from typing import Sequence
MAP_ORDER = [
"seed",
"soil",
"fertilizer",
"water",
"light",
"temperature",
"humidity",
"location",
]
lines = sys.stdin.read().splitlines()
# mappings from one category to another, each list contains
# ranges stored as (source, target, length), ordered by start and
# completed to have no "hole"
maps: dict[tuple[str, str], list[tuple[int, int, int]]] = {}
# parsing
index = 2
while index < len(lines):
p1, _, p2 = lines[index].split()[0].split("-")
# extract the existing ranges from the file - we store as (source, target, length)
# whereas the file is in order (target, source, length)
index += 1
values: list[tuple[int, int, int]] = []
while index < len(lines) and lines[index]:
n1, n2, n3 = lines[index].split()
values.append((int(n2), int(n1), int(n3)))
index += 1
# sort by source value
values.sort()
# add a 'fake' interval starting at 0 if missing
if values[0][0] != 0:
values.insert(0, (0, 0, values[0][0]))
# fill gaps between intervals
for i in range(len(values) - 1):
next_start = values[i + 1][0]
end = values[i][0] + values[i][2]
if next_start != end:
values.insert(
i + 1,
(end, end, next_start - end),
)
# add an interval covering values up to at least 2**32 at the end
last_start, _, last_length = values[-1]
values.append((last_start + last_length, last_start + last_length, 2**32))
assert all(v1[0] + v1[2] == v2[0] for v1, v2 in zip(values[:-1], values[1:]))
assert values[0][0] == 0
assert values[-1][0] + values[-1][-1] >= 2**32
maps[p1, p2] = values
index += 1
def find_range(
values: tuple[int, int], map: list[tuple[int, int, int]]
) -> list[tuple[int, int]]:
"""
Given an input range, use the given mapping to find the corresponding list of
ranges in the target domain.
"""
r_start, r_length = values
ranges: list[tuple[int, int]] = []
# find index of the first and last intervals in map that overlaps the input
# interval
index_start, index_end = -1, -1
for index_start, (start, _, length) in enumerate(map):
if start <= r_start and start + length > r_start:
break
for index_end, (start, _, length) in enumerate(
map[index_start:], start=index_start
):
if r_start + r_length >= start and r_start + r_length < start + length:
break
assert index_start >= 0 and index_end >= 0
# special case if one interval contains everything
if index_start == index_end:
start, target, length = map[index_start]
ranges.append((target + r_start - start, r_length))
else:
# add the start interval part
start, target, length = map[index_start]
ranges.append((target + r_start - start, start + length - r_start))
# add all intervals between the first and last (excluding both)
index = index_start + 1
while index < index_end:
start, target, length = map[index]
ranges.append((target, length))
index += 1
# add the last interval
start, target, length = map[index_end]
ranges.append((target, r_start + r_length - start))
return ranges
def find_location_ranges(seeds: Sequence[tuple[int, int]]) -> Sequence[tuple[int, int]]:
for map1, map2 in zip(MAP_ORDER[:-1], MAP_ORDER[1:]):
seeds = [s2 for s1 in seeds for s2 in find_range(s1, maps[map1, map2])]
return seeds
# part 1 - use find_range() with range of length 1
seeds_p1 = [(int(s), 1) for s in lines[0].split(":")[1].strip().split()]
answer_1 = min(start for start, _ in find_location_ranges(seeds_p1))
print(f"answer 1 is {answer_1}")
# # part 2
parts = lines[0].split(":")[1].strip().split()
seeds_p2 = [(int(s), int(e)) for s, e in zip(parts[::2], parts[1::2])]
answer_2 = min(start for start, _ in find_location_ranges(seeds_p2))
print(f"answer 2 is {answer_2}")

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