1 Commits

Author SHA1 Message Date
Mikael CAPELLE
40ab70271e 2023 day 17, v2. 2023-12-19 14:26:16 +01:00
452 changed files with 2905 additions and 19729 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
__pycache__
.ruff_cache
.vscode
build

14
2021/day1.py Normal file
View File

@@ -0,0 +1,14 @@
import sys
lines = sys.stdin.read().splitlines()
values = [int(line) for line in lines]
# part 1
answer_1 = sum(v2 > v1 for v1, v2 in zip(values[:-1], values[1:]))
print(f"answer 1 is {answer_1}")
# part 2
runnings = [sum(values[i : i + 3]) for i in range(len(values) - 2)]
answer_2 = sum(v2 > v1 for v1, v2 in zip(runnings[:-1], runnings[1:]))
print(f"answer 2 is {answer_2}")

13
2021/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
2021/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
2021/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
2021/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
2021/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
2021/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
2021/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
2021/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
2021/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
2021/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}")

40
2021/day2.py Normal file
View File

@@ -0,0 +1,40 @@
import sys
from math import prod
from typing import Literal, cast
lines = sys.stdin.read().splitlines()
commands = [
(cast(Literal["forward", "up", "down"], (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
# part 1
answer_1 = prod(depth_and_position(False))
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = prod(depth_and_position(True))
print(f"answer 2 is {answer_2}")

13
2021/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
2021/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
2021/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
2021/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
2021/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
2021/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}")

39
2021/day3.py Normal file
View File

@@ -0,0 +1,39 @@
import sys
from collections import Counter
from typing import Literal
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]
lines = sys.stdin.read().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)
print(f"answer 1 is {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)
answer_2 = oxygen_generator_rating * co2_scrubber_rating
print(f"answer 2 is {answer_2}")

45
2021/day4.py Normal file
View File

@@ -0,0 +1,45 @@
import sys
import numpy as np
lines = sys.stdin.read().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])
print(f"answer 1 is {score}")
# part 2
(_, score) = max(winning_rounds, key=lambda w: w[0])
print(f"answer 2 is {score}")

48
2021/day5.py Normal file
View File

@@ -0,0 +1,48 @@
import sys
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}")

21
2021/day6.py Normal file
View File

@@ -0,0 +1,21 @@
import sys
values = [int(c) for c in sys.stdin.read().strip().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]
# part 1
answer_1 = sum(v for k, v in lanterns.items() if k < 80) + len(values)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = sum(lanterns.values()) + len(values)
print(f"answer 2 is {answer_2}")

21
2021/day7.py Normal file
View File

@@ -0,0 +1,21 @@
import sys
import numpy as np
positions = np.asarray([int(c) for c in sys.stdin.read().strip().split(",")])
min_position, max_position = positions.min(), positions.max()
# part 1
answer_1 = min(
np.sum(np.abs(positions - position))
for position in range(min_position, max_position + 1)
)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = min(
np.sum(abs(positions - position) * (abs(positions - position) + 1) // 2)
for position in range(min_position, max_position + 1)
)
print(f"answer 2 is {answer_2}")

87
2021/day8.py Normal file
View File

@@ -0,0 +1,87 @@
import itertools
import os
import sys
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
digits = {
"abcefg": 0,
"cf": 1,
"acdeg": 2,
"acdfg": 3,
"bcdf": 4,
"abdfg": 5,
"abdefg": 6,
"acf": 7,
"abcdefg": 8,
"abcdfg": 9,
}
lines = sys.stdin.read().splitlines()
# part 1
lengths = {len(k) for k, v in digits.items() if v in (1, 4, 7, 8)}
answer_1 = sum(
len(p) in lengths for line in lines for p in line.split("|")[1].strip().split()
)
print(f"answer 1 is {answer_1}")
# 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]
if VERBOSE:
print(value)
values.append(value)
answer_2 = sum(values)
print(f"answer 2 is {answer_2}")

13
2021/day9.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}")

7
2022/day1.py Normal file
View File

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

38
2022/day10.py Normal file
View File

@@ -0,0 +1,38 @@
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,8 +1,7 @@
import copy
import sys
from functools import reduce
from typing import Any, Callable, Final, Iterator, Mapping, Sequence
from ..base import BaseSolver
from typing import Callable, Final, Mapping, Sequence
class Monkey:
@@ -120,28 +119,24 @@ def monkey_business(inspects: dict[Monkey, int]) -> int:
return sorted_levels[-2] * sorted_levels[-1]
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
monkeys = [parse_monkey(block.splitlines()) for block in input.split("\n\n")]
monkeys = [parse_monkey(block.splitlines()) for block in sys.stdin.read().split("\n\n")]
# case 1: we simply divide the worry by 3 after applying the monkey worry operation
yield monkey_business(
run(copy.deepcopy(monkeys), 20, me_worry_fn=lambda w: w // 3)
)
# case 1: we simply divide the worry by 3 after applying the monkey worry operation
answer_1 = monkey_business(
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
# 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 % d) + (b % d)) % c --- if d is a multiple of c, which is why here
# we use the product of all test value
#
total_test_value = reduce(lambda w, m: w * m.test_value, monkeys, 1)
yield monkey_business(
run(
copy.deepcopy(monkeys),
10_000,
me_worry_fn=lambda w: w % total_test_value,
)
)
# 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
#
# (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
# we use the product of all test value
#
total_test_value = reduce(lambda w, m: w * m.test_value, monkeys, 1)
answer_2 = monkey_business(
run(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,6 @@
import heapq
from typing import Any, Callable, Iterator, TypeVar
from ..base import BaseSolver
import sys
from typing import Callable, Iterator, TypeVar
Node = TypeVar("Node")
@@ -69,6 +68,30 @@ def make_path(parents: dict[Node, Node], start: Node, end: Node) -> list[Node] |
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]]:
@@ -95,74 +118,43 @@ def neighbors(
# === main code ===
lines = sys.stdin.read().splitlines()
class Solver(BaseSolver):
def print_path(self, path: list[tuple[int, int]], n_rows: int, n_cols: int) -> None:
end = path[-1]
grid = [[ord(cell) - ord("a") for cell in line] for line in lines]
graph = [["." for _c in range(n_cols)] for _r in range(n_rows)]
graph[end[0]][end[1]] = "E"
start: tuple[int, int]
end: tuple[int, int]
for i in range(0, len(path) - 1):
cr, cc = path[i]
nr, nc = path[i + 1]
# for part 2
start_s: list[tuple[int, int]] = []
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])
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))
for row in graph:
self.logger.info("".join(row))
# fix values
grid[start[0]][start[1]] = 0
grid[end[0]][end[1]] = ord("z") - ord("a")
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
grid = [[ord(cell) - ord("a") for cell in line] for line in lines]
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
start: tuple[int, int] | None = None
end: tuple[int, int] | None = None
print_path(path_1, n_rows=len(grid), n_cols=len(grid[0]))
# for part 2
start_s: list[tuple[int, int]] = []
print(f"answer 1 is {lengths_1[end] - 1}")
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(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)
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}")

View File

@@ -1,8 +1,11 @@
import json
import sys
from functools import cmp_to_key
from typing import Any, Iterator, TypeAlias, cast
from typing import TypeAlias, cast
from ..base import BaseSolver
blocks = sys.stdin.read().strip().split("\n\n")
pairs = [tuple(json.loads(p) for p in block.split("\n")) for block in blocks]
Packet: TypeAlias = list[int | list["Packet"]]
@@ -25,18 +28,14 @@ def compare(lhs: Packet, rhs: Packet) -> int:
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]
answer_1 = sum(i + 1 for i, (lhs, rhs) in enumerate(pairs) if compare(lhs, rhs) > 0)
print(f"answer_1 is {answer_1}")
yield sum(i + 1 for i, (lhs, rhs) in enumerate(pairs) if compare(lhs, rhs) > 0)
dividers = [[[2]], [[6]]]
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))))
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]
d_index = [packets.index(d) + 1 for d in dividers]
print(f"answer 2 is {d_index[0] * d_index[1]}")

140
2022/day14.py Normal file
View File

@@ -0,0 +1,140 @@
import sys
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, _ in blocks),
0,
max(x for x, _ in blocks),
max(y for _, 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 _, 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, _ in blocks),
0,
max(x for x, _ in blocks),
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
)
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())}")

87
2022/day15.py Normal file
View File

@@ -0,0 +1,87 @@
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)
_, 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})")

View File

@@ -3,13 +3,12 @@ from __future__ import annotations
import heapq
import itertools
import re
import sys
from collections import defaultdict
from typing import Any, FrozenSet, Iterator, NamedTuple
from typing import FrozenSet, NamedTuple
from tqdm import tqdm
from ..base import BaseSolver
class Pipe(NamedTuple):
name: str
@@ -37,8 +36,8 @@ 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()
queue = [(0, pipe_1)]
visited = set()
distances: dict[Pipe, int] = {}
while len(distances) < len(pipes):
@@ -123,37 +122,37 @@ def part_2(
# === MAIN ===
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = [line.strip() for line in input.splitlines()]
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: 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
pipes[g[0]] = Pipe(g[0], int(g[1]), g[2].split(", "))
g = r.groups()
# 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()
}
)
pipes[g[0]] = Pipe(g[0], int(g[1]), g[2].split(", "))
# valves with flow
relevant_pipes = frozenset(pipe for pipe in pipes.values() if pipe.flow > 0)
# 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()
}
)
# 1651, 1653
yield part_1(pipes["AA"], 30, distances, relevant_pipes)
# valves with flow
relevant_pipes = frozenset(pipe for pipe in pipes.values() if pipe.flow > 0)
# 1707, 2223
yield part_2(pipes["AA"], 26, distances, relevant_pipes)
# 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,16 +1,12 @@
from typing import Any, Iterator, Sequence, TypeAlias, TypeVar
import sys
from typing import Sequence, TypeVar
import numpy as np
from numpy.typing import NDArray
from ..base import BaseSolver
T = TypeVar("T")
Tower: TypeAlias = NDArray[np.bool]
def print_tower(tower: Tower, out: str = "#"):
def print_tower(tower: np.ndarray, out: str = "#"):
print("-" * (tower.shape[1] + 2))
non_empty = False
for row in reversed(range(1, tower.shape[0])):
@@ -21,7 +17,7 @@ def print_tower(tower: Tower, out: str = "#"):
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)
@@ -49,8 +45,8 @@ def build_tower(
n_rocks: int,
jets: str,
early_stop: bool = False,
init: Tower = np.ones(WIDTH, dtype=bool),
) -> tuple[Tower, int, int, dict[int, int]]:
init: np.ndarray = np.ones(WIDTH, dtype=bool),
) -> tuple[np.ndarray, int, int, dict[int, int]]:
tower = EMPTY_BLOCKS.copy()
tower[0, :] = init
@@ -99,24 +95,26 @@ def build_tower(
return tower, rock_count, done_at.get((i_rock, i_jet), -1), heights
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
tower, *_ = build_tower(2022, input)
yield tower_height(tower)
line = sys.stdin.read().strip()
TOTAL_ROCKS = 1_000_000_000_000
_tower_1, n_rocks_1, prev_1, heights_1 = build_tower(TOTAL_ROCKS, input, True)
assert prev_1 > 0
tower, *_ = build_tower(2022, line)
answer_1 = tower_height(tower)
print(f"answer 1 is {answer_1}")
# 2767 1513
remaining_rocks = TOTAL_ROCKS - n_rocks_1
n_repeat_rocks = n_rocks_1 - prev_1
n_repeat_towers = remaining_rocks // n_repeat_rocks
TOTAL_ROCKS = 1_000_000_000_000
tower_1, n_rocks_1, prev_1, heights_1 = build_tower(TOTAL_ROCKS, line, True)
assert prev_1 > 0
base_height = heights_1[prev_1]
repeat_height = heights_1[prev_1 + n_repeat_rocks - 1] - heights_1[prev_1]
remaining_height = (
heights_1[prev_1 + remaining_rocks % n_repeat_rocks] - heights_1[prev_1]
)
# 2767 1513
remaining_rocks = TOTAL_ROCKS - n_rocks_1
n_repeat_rocks = n_rocks_1 - prev_1
n_repeat_towers = remaining_rocks // n_repeat_rocks
yield base_height + (n_repeat_towers + 1) * repeat_height + remaining_height
base_height = heights_1[prev_1]
repeat_height = heights_1[prev_1 + n_repeat_rocks - 1] - heights_1[prev_1]
remaining_height = (
heights_1[prev_1 + remaining_rocks % n_repeat_rocks] - heights_1[prev_1]
)
answer_2 = base_height + (n_repeat_towers + 1) * repeat_height + remaining_height
print(f"answer 2 is {answer_2}")

51
2022/day18.py Normal file
View File

@@ -0,0 +1,51 @@
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,9 @@
from typing import Any, Iterator, Literal
import sys
from typing import Literal
import numpy as np
import parse # pyright: ignore[reportMissingTypeStubs]
from numpy.typing import NDArray
from ..base import BaseSolver
import parse
from tqdm import tqdm
Reagent = Literal["ore", "clay", "obsidian", "geode"]
REAGENTS: tuple[Reagent, ...] = (
@@ -36,7 +35,7 @@ class State:
self.robots = robots
self.reagents = reagents
def __eq__(self, other: object) -> bool:
def __eq__(self, other) -> bool:
return (
isinstance(other, State)
and self.robots == other.robots
@@ -63,6 +62,29 @@ 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:
# 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.,
@@ -72,12 +94,11 @@ 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
}
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):
# list of new states at the end of step t that we are going to prune later
states_for_t: set[State] = set()
robots_that_can_be_built: list[Reagent]
for state in state_after_t[t - 1]:
robots_that_can_be_built = [
@@ -111,7 +132,7 @@ def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int:
for robot in robots_that_can_be_built:
robots = state.robots.copy()
robots[robot] += 1
reagents: IntOfReagent = {
reagents = {
reagent: state.reagents[reagent]
+ state.robots[reagent]
- blueprint[robot].get(reagent, 0)
@@ -130,7 +151,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:
first_dom = (np_states[1:] >= np_states[0]).all(axis=1).any()
@@ -151,31 +172,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])
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
blueprints: list[dict[Reagent, IntOfReagent]] = []
for line in input.splitlines():
r: list[int] = parse.parse( # type: ignore
"Blueprint {}: "
"Each ore robot costs {:d} ore. "
"Each clay robot costs {:d} ore. "
"Each obsidian robot costs {:d} ore and {:d} clay. "
"Each geode robot costs {:d} ore and {:d} obsidian.",
line,
)
answer_1 = sum(
(i_blueprint + 1) * run(blueprint, 24)
for i_blueprint, blueprint in enumerate(blueprints)
)
print(f"answer 1 is {answer_1}")
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)
for i_blueprint, blueprint in enumerate(blueprints)
)
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,4 @@
from typing import Any, Iterator
from ..base import BaseSolver
import sys
def score_1(ux: int, vx: int) -> int:
@@ -35,23 +33,21 @@ def score_2(ux: int, vx: int) -> int:
return (ux + vx - 1) % 3 + 1 + vx * 3
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
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)))
#
# 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]
# 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 1 - 13526
print(f"answer 1 is {sum(score_1(*v) for v in values)}")
# part 2 - 14204
yield sum(score_2(*v) for v in values)
# part 2 - 14204
print(f"answer 2 is {sum(score_2(*v) for v in values)}")

View File

@@ -1,8 +1,6 @@
from __future__ import annotations
from typing import Any, Iterator
from ..base import BaseSolver
import sys
class Number:
@@ -67,9 +65,10 @@ def decrypt(numbers: list[Number], key: int, rounds: int) -> int:
)
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
numbers = [Number(int(x)) for x in input.splitlines()]
numbers = [Number(int(x)) for i, x in enumerate(sys.stdin.readlines())]
yield decrypt(numbers, 1, 1)
yield decrypt(numbers, 811589153, 10)
answer_1 = decrypt(numbers, 1, 1)
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,6 @@
import operator
from typing import Any, Callable, Iterator
from ..base import BaseSolver
import sys
from typing import Callable
def compute(monkeys: dict[str, int | tuple[str, str, str]], monkey: str) -> int:
@@ -78,31 +77,31 @@ def invert(
return monkeys
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = [line.strip() for line in input.splitlines()]
lines = sys.stdin.read().splitlines()
monkeys: dict[str, int | tuple[str, str, str]] = {}
monkeys: dict[str, int | tuple[str, str, str]] = {}
op_monkeys: set[str] = set()
op_monkeys: set[str] = set()
for line in lines:
parts = line.split(":")
name = parts[0].strip()
for line in lines:
parts = line.split(":")
name = parts[0].strip()
try:
value = int(parts[1].strip())
monkeys[name] = value
except ValueError:
op1, ope, op2 = parts[1].strip().split()
monkeys[name] = (op1, ope, op2)
try:
value = int(parts[1].strip())
monkeys[name] = value
except ValueError:
op1, ope, op2 = parts[1].strip().split()
monkeys[name] = (op1, ope, op2)
op_monkeys.add(name)
op_monkeys.add(name)
yield compute(monkeys.copy(), "root")
# 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
assert isinstance(monkeys["root"], tuple)
p1, _, p2 = monkeys["root"] # type: ignore
yield compute(invert(monkeys, "humn", compute(monkeys.copy(), p2)), "humn")
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
# humn, which is the case is my input and the test input
p1, _, p2 = monkeys["root"] # type: ignore
answer_2 = compute(invert(monkeys, "humn", compute(monkeys.copy(), p2)), "humn")
print(f"answer 2 is {answer_2}")

223
2022/day22.py Normal file
View File

@@ -0,0 +1,223 @@
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,6 @@
import itertools
import sys
from collections import defaultdict
from typing import Any, Iterator
from ..base import BaseSolver
Directions = list[
tuple[
@@ -20,7 +18,7 @@ DIRECTIONS: Directions = [
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)
@@ -71,38 +69,35 @@ def round(
directions.append(directions.pop(0))
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
POSITIONS = {
(i, j)
for i, row in enumerate(input.splitlines())
for j, col in enumerate(row)
if col == "#"
}
POSITIONS = {
(i, j)
for i, row in enumerate(sys.stdin.read().splitlines())
for j, col in enumerate(row)
if col == "#"
}
# === part 1 ===
# === part 1 ===
p1, d1 = POSITIONS.copy(), DIRECTIONS.copy()
for _ in range(10):
round(p1, d1)
p1, d1 = POSITIONS.copy(), DIRECTIONS.copy()
for r in range(10):
round(p1, d1)
min_y, min_x, max_y, max_x = min_max_yx(p1)
yield sum(
(y, x) not in p1
for y in range(min_y, max_y + 1)
for x in range(min_x, max_x + 1)
)
min_y, min_x, max_y, max_x = min_max_yx(p1)
answer_1 = sum(
(y, x) not in p1 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 ===
p2, d2 = POSITIONS.copy(), DIRECTIONS.copy()
answer_2 = 0
while True:
answer_2 += 1
backup = p2.copy()
round(p2, d2)
p2, d2 = POSITIONS.copy(), DIRECTIONS.copy()
answer_2 = 0
while True:
answer_2 += 1
backup = p2.copy()
round(p2, d2)
if backup == p2:
break
if backup == p2:
break
yield answer_2
print(f"answer 2 is {answer_2}")

98
2022/day24.py Normal file
View File

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

27
2022/day25.py Normal file
View File

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

23
2022/day3.py Normal file
View File

@@ -0,0 +1,23 @@
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"answer 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"answer 2 is {part2}")

17
2022/day4.py Normal file
View File

@@ -0,0 +1,17 @@
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]
answer_1 = sum(s1.issubset(s2) or s2.issubset(s1) for s1, s2 in sections)
print(f"answer 1 is {answer_1}")
answer_2 = sum(bool(s1.intersection(s2)) for s1, s2 in sections)
print(f"answer 1 is {answer_2}")

41
2022/day5.py Normal file
View File

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

15
2022/day6.py Normal file
View File

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

80
2022/day7.py Normal file
View File

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

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