Compare commits

..

No commits in common. "master" and "day16-tmp" have entirely different histories.

296 changed files with 119 additions and 36277 deletions

1
.gitignore vendored
View File

@ -1,2 +1 @@
venv
__pycache__

View File

@ -1,4 +1,7 @@
# -*- encoding: utf-8 -*-
import sys
from collections import defaultdict
import numpy as np
@ -31,6 +34,7 @@ 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)

20
2022/day1.py Normal file
View File

@ -0,0 +1,20 @@
# -*- encoding: utf-8 -*-
import sys
lines = sys.stdin.readlines()
# we store the list of calories for each elf in values, and we use the last element
# of values to accumulate
values: list[int] = [0]
for line in lines:
if not line.strip():
values = values + [0]
else:
values[-1] += int(line.strip())
# part 1
print(f"answer 1 is {max(values)}")
# part 2
print(f"answer 2 is {sum(sorted(values)[-3:])}")

View File

@ -1,3 +1,5 @@
# -*- encoding: utf-8 -*-
import sys
lines = sys.stdin.read().splitlines()

View File

@ -1,3 +1,5 @@
# -*- encoding: utf-8 -*-
import copy
import sys
from functools import reduce
@ -5,6 +7,7 @@ from typing import Callable, Final, Mapping, Sequence
class Monkey:
id: Final[int]
items: Final[Sequence[int]]
worry_fn: Final[Callable[[int], int]]
@ -94,7 +97,8 @@ def run(
# number of inspects
inspects = {monkey: 0 for monkey in monkeys}
for _ in range(n_rounds):
for round in range(n_rounds):
for monkey in monkeys:
for item in items[monkey]:
inspects[monkey] += 1

View File

@ -1,3 +1,5 @@
# -*- encoding: utf-8 -*-
import heapq
import sys
from typing import Callable, Iterator, TypeVar
@ -42,6 +44,7 @@ def dijkstra(
visited.add(current)
for neighbor in neighbors(current):
if neighbor in visited:
continue
@ -57,6 +60,7 @@ def dijkstra(
def make_path(parents: dict[Node, Node], start: Node, end: Node) -> list[Node] | None:
if end not in parents:
return None
@ -105,6 +109,7 @@ def neighbors(
(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

View File

@ -1,27 +1,27 @@
# -*- encoding: utf-8 -*-
import json
import sys
from functools import cmp_to_key
from typing import TypeAlias, cast
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"]]
def compare(lhs: list[int | list], rhs: list[int | list]) -> int:
def compare(lhs: Packet, rhs: Packet) -> int:
for lhs_a, rhs_a in zip(lhs, rhs):
if isinstance(lhs_a, int) and isinstance(rhs_a, int):
if lhs_a != rhs_a:
return rhs_a - lhs_a
else:
if not isinstance(lhs_a, list):
lhs_a = [lhs_a] # type: ignore
lhs_a = [lhs_a]
elif not isinstance(rhs_a, list):
rhs_a = [rhs_a] # type: ignore
rhs_a = [rhs_a]
assert isinstance(rhs_a, list) and isinstance(lhs_a, list)
r = compare(cast(Packet, lhs_a), cast(Packet, rhs_a))
r = compare(lhs_a, rhs_a)
if r != 0:
return r

View File

@ -1,4 +1,7 @@
# -*- encoding: utf-8 -*-
import sys
from collections import defaultdict
from enum import Enum, auto
from typing import Callable, cast
@ -20,10 +23,10 @@ def print_blocks(blocks: dict[tuple[int, int], Cell]):
blocks: Set of blocks to print.
"""
x_min, y_min, x_max, y_max = (
min(x for x, _ in blocks),
min(x for x, y in blocks),
0,
max(x for x, _ in blocks),
max(y for _, y in blocks),
max(x for x, y in blocks),
max(y for x, y in blocks),
)
for y in range(y_min, y_max + 1):
@ -53,12 +56,13 @@ def flow(
The input blocks.
"""
y_max = max(y for _, y in 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:
@ -113,10 +117,10 @@ print_blocks(blocks)
print()
x_min, y_min, x_max, y_max = (
min(x for x, _ in blocks),
min(x for x, y in blocks),
0,
max(x for x, _ in blocks),
max(y for _, y in blocks),
max(x for x, y in blocks),
max(y for x, y in blocks),
)
# === part 1 ===

View File

@ -1,3 +1,5 @@
# -*- encoding: utf-8 -*-
import sys
import numpy as np
@ -5,6 +7,7 @@ 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():
@ -34,7 +37,7 @@ def part2_intervals(
its.append((max(0, sx - dx), min(sx + dx, xy_max)))
its = sorted(its)
_, e = its[0]
s, e = its[0]
for si, ei in its[1:]:
if si > e + 1:

View File

@ -1,3 +1,5 @@
# -*- encoding: utf-8 -*-
from __future__ import annotations
import heapq
@ -31,28 +33,25 @@ class Pipe(NamedTuple):
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.
"""
def breadth_first_search(pipes: dict[str, Pipe], pipe_1: Pipe, pipe_2: Pipe) -> int:
queue = [(0, pipe_1)]
visited = set()
distances: dict[Pipe, int] = {}
while len(distances) < len(pipes):
while queue:
distance, current = heapq.heappop(queue)
if current in visited:
continue
visited.add(current)
distances[current] = distance
if current == pipe_2:
return distance
for tunnel in current.tunnels:
heapq.heappush(queue, (distance + 1, pipes[tunnel]))
return distances
return -1
def update_with_better(
@ -67,6 +66,7 @@ def part_1(
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))
)
@ -76,6 +76,7 @@ def part_1(
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
@ -140,12 +141,8 @@ for line in lines:
# 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()
}
)
for pipe_2 in pipes.values():
distances[pipe_1, pipe_2] = breadth_first_search(pipes, pipe_1, pipe_2)
# valves with flow
relevant_pipes = frozenset(pipe for pipe in pipes.values() if pipe.flow > 0)

View File

@ -1,5 +1,20 @@
# -*- encoding: utf-8 -*-
import sys
lines = sys.stdin.readlines()
# the solution relies on replacing rock / paper / scissor by values 0 / 1 / 2 and using
# modulo-3 arithmetic
#
# in modulo-3 arithmetic, the winning move is 1 + the opponent move (e.g., winning move
# if opponent plays 0 is 1, or 0 if opponent plays 2 (0 = (2 + 1 % 3)))
#
# we read the lines in a Nx2 in array with value 0/1/2 instead of A/B/C or X/Y/Z for
# easier manipulation
values = [(ord(row[0]) - ord("A"), ord(row[2]) - ord("X")) for row in lines]
def score_1(ux: int, vx: int) -> int:
# here ux and vx are both moves: 0 = rock, 1 = paper, 2 = scissor
@ -33,21 +48,8 @@ def score_2(ux: int, vx: int) -> int:
return (ux + vx - 1) % 3 + 1 + vx * 3
lines = sys.stdin.readlines()
# the solution relies on replacing rock / paper / scissor by values 0 / 1 / 2 and using
# modulo-3 arithmetic
#
# in modulo-3 arithmetic, the winning move is 1 + the opponent move (e.g., winning move
# if opponent plays 0 is 1, or 0 if opponent plays 2 (0 = (2 + 1 % 3)))
#
# we read the lines in a Nx2 in array with value 0/1/2 instead of A/B/C or X/Y/Z for
# easier manipulation
values = [(ord(row[0]) - ord("A"), ord(row[2]) - ord("X")) for row in lines]
# part 1 - 13526
print(f"answer 1 is {sum(score_1(*v) for v in values)}")
print(f"score 1 is {sum(score_1(*v) for v in values)}")
# part 2 - 14204
print(f"answer 2 is {sum(score_2(*v) for v in values)}")
print(f"score 2 is {sum(score_2(*v) for v in values)}")

View File

@ -1,3 +1,5 @@
# -*- encoding: utf-8 -*-
import string
import sys
@ -11,13 +13,13 @@ 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}")
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])
for c in set.intersection(*map(set, (lines[i : i + n_per_group])))
)
print(f"answer 2 is {part2}")
print(f"score 2 is {part2}")

View File

@ -1,3 +1,5 @@
# -*- encoding: utf-8 -*-
import sys
lines = [line.strip() for line in sys.stdin.readlines()]
@ -10,8 +12,8 @@ def make_range(value: str) -> set[int]:
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}")
score_1 = sum(s1.issubset(s2) or s2.issubset(s1) for s1, s2 in sections)
print(f"score 1 is {score_1}")
answer_2 = sum(bool(s1.intersection(s2)) for s1, s2 in sections)
print(f"answer 1 is {answer_2}")
score_2 = sum(bool(s1.intersection(s2)) for s1, s2 in sections)
print(f"score 1 is {score_2}")

View File

@ -1,3 +1,5 @@
# -*- encoding: utf-8 -*-
import copy
import sys

View File

@ -1,5 +1,9 @@
# -*- encoding: utf-8 -*-
import sys
data = sys.stdin.read().strip()
def index_of_first_n_differents(data: str, n: int) -> int:
for i in range(len(data)):
@ -8,8 +12,5 @@ def index_of_first_n_differents(data: str, n: int) -> int:
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)}")

View File

@ -1,3 +1,5 @@
# -*- encoding: utf-8 -*-
import sys
from pathlib import Path

View File

@ -1,7 +1,8 @@
# -*- encoding: utf-8 -*-
import sys
import numpy as np
from numpy.typing import NDArray
lines = sys.stdin.read().splitlines()
@ -26,7 +27,7 @@ answer_1 = (highest_trees.min(axis=2) < trees).sum()
print(f"answer 1 is {answer_1}")
def viewing_distance(row_of_trees: NDArray[np.int_], value: int) -> int:
def viewing_distance(row_of_trees: np.ndarray, value: int) -> int:
w = np.where(row_of_trees >= value)[0]
if not w.size:

View File

@ -1,9 +1,12 @@
# -*- encoding: utf-8 -*-
import sys
import numpy as np
def move(head: tuple[int, int], command: str) -> tuple[int, int]:
h_col, h_row = head
if command == "L":
@ -19,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]:
h_col, h_row = head
t_col, t_row = tail
@ -29,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]]:
blocks: list[tuple[int, int]] = [(0, 0) for _ in range(n_blocks)]
blocks = [(0, 0) for _ in range(n_blocks)]
visited = [blocks[-1]]
for command in commands:

View File

@ -1,36 +1,7 @@
# Holt59 - Advent Of Code
# Advent Of Code
Installation (with [`poetry`](https://python-poetry.org/)):
To run any script, you need to pipe the input:
```bash
poetry install
```
To run any day:
```bash
holt59-aoc $day
```
You can use `-v` / `--verbose` for extra outputs in some case, `-t` / `--test` to run
the code on the test data (one of the test data if multiple are present) or even
`-u XXX` / `--user XXX` to run the code on a specific input after putting the input
file under `src/holt59/aoc/inputs/XXX/$year/$day`.
Full usage:
```bash
usage: Holt59 Advent-Of-Code Runner [-h] [-v] [-t] [-u USER] [-i INPUT] [-y YEAR] day
positional arguments:
day day to run
options:
-h, --help show this help message and exit
-v, --verbose verbose mode
-t, --test test mode
-u USER, --user USER user input to use
-i INPUT, --input INPUT
input to use (override user and test)
-y YEAR, --year YEAR year to run
cat 2022/inputs/day2.txt | python 2022/day2.py
```

1395
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,52 +0,0 @@
[tool.poetry]
name = "holt59-advent-of-code"
version = "0.1.0"
description = ""
authors = ["Mikael CAPELLE <capelle.mikael@gmail.com>"]
license = "MIT"
readme = "README.md"
packages = [{ include = "holt59", from = "src" }]
[tool.poetry.dependencies]
python = "^3.10"
numpy = "^1.26.2"
tqdm = "^4.66.1"
parse = "^1.20.0"
scipy = "^1.11.4"
ortools = "^9.8.3296"
sympy = "^1.12"
networkx = "^3.2.1"
[tool.poetry.scripts]
holt59-aoc = "holt59.aoc.__main__:main"
[tool.poe.tasks]
lint-black = "black --check --diff src tests typings"
lint-isort = "isort -c src tests typings"
lint-ruff = "ruff src tests typings"
lint-flake8 = "flake8 src tests typings"
lint-pyright = "pyright src tests"
lint-all.sequence = [
"lint-black",
"lint-isort",
"lint-flake8",
"lint-ruff",
"lint-pyright",
]
lint-all.ignore_fail = "return_non_zero"
[tool.poetry.group.dev.dependencies]
flake8 = "^6.1.0"
flake8-black = "^0.3.6"
black = "^23.12.0"
pyright = "^1.1.341"
mypy = "^1.7.1"
isort = "^5.13.2"
ruff = "^0.1.8"
poethepoet = "^0.24.4"
ipykernel = "^6.27.1"
networkx-stubs = "^0.0.1"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

13
run.ps1
View File

@ -1,12 +1,3 @@
param(
[switch]$Test,
[PSDefaultValue()]
[Parameter(Mandatory = $false)]
$Year = 2023,
[Parameter(Mandatory = $true, Position = 0)]
$Day)
param ($day)
$folder = $Test ? "tests" : "inputs"
$env:AOC_VERBOSE = $VerbosePreference -eq "Continue"
Get-Content ".\$Year\$folder\day$Day.txt" | python ".\$Year\day$Day.py"
Get-Content ".\2022\inputs\day$day.txt" | python ".\2022\day$day.py"

View File

@ -1,10 +0,0 @@
import sys
line = sys.stdin.read().strip()
floor = 0
floors = [(floor := floor + (1 if c == "(" else -1)) for c in line]
print(f"answer 1 is {floors[-1]}")
print(f"answer 2 is {floors.index(-1)}")

View File

@ -1,148 +0,0 @@
import itertools
import sys
line = sys.stdin.read().strip()
# see http://www.se16.info/js/lands2.htm for the explanation of 'atoms' (or elements)
#
# see also https://www.youtube.com/watch?v=ea7lJkEhytA (video link from AOC) and this
# CodeGolf answer https://codegolf.stackexchange.com/a/8479/42148
# fmt: off
atoms = [
("22", (0, )), # 0
("13112221133211322112211213322112", (71, 90, 0, 19, 2, )), # 1
("312211322212221121123222112", (1, )), # 2
("111312211312113221133211322112211213322112", (31, 19, 2, )), # 3
("1321132122211322212221121123222112", (3, )), # 4
("3113112211322112211213322112", (4, )), # 5
("111312212221121123222112", (5, )), # 6
("132112211213322112", (6, )), # 7
("31121123222112", (7, )), # 8
("111213322112", (8, )), # 9
("123222112", (9, )), # 10
("3113322112", (60, 10, )), # 11
("1113222112", (11, )), # 12
("1322112", (12, )), # 13
("311311222112", (66, 13, )), # 14
("1113122112", (14, )), # 15
("132112", (15, )), # 16
("3112", (16, )), # 17
("1112", (17, )), # 18
("12", (18, )), # 19
("3113112221133112", (66, 90, 0, 19, 26, )), # 20
("11131221131112", (20, )), # 21
("13211312", (21, )), # 22
("31132", (22, )), # 23
("111311222112", (23, 13, )), # 24
("13122112", (24, )), # 25
("32112", (25, )), # 26
("11133112", (29, 26, )), # 27
("131112", (27, )), # 28
("312", (28, )), # 29
("13221133122211332", (62, 19, 88, 0, 19, 29, )), # 30
("31131122211311122113222", (66, 30, )), # 31
("11131221131211322113322112", (31, 10, )), # 32
("13211321222113222112", (32, )), # 33
("3113112211322112", (33, )), # 34
("11131221222112", (34, )), # 35
("1321122112", (35, )), # 36
("3112112", (36, )), # 37
("1112133", (37, 91, )), # 38
("12322211331222113112211", (38, 0, 19, 42, )), # 39
("1113122113322113111221131221", (67, 39, )), # 40
("13211322211312113211", (40, )), # 41
("311322113212221", (41, )), # 42
("132211331222113112211", (62, 19, 42, )), # 43
("311311222113111221131221", (66, 43, )), # 44
("111312211312113211", (44, )), # 45
("132113212221", (45, )), # 46
("3113112211", (46, )), # 47
("11131221", (47, )), # 48
("13211", (48, )), # 49
("3112221", (60, 49, )), # 50
("1322113312211", (62, 19, 50, )), # 51
("311311222113111221", (66, 51, )), # 52
("11131221131211", (52, )), # 53
("13211321", (53, )), # 54
("311311", (54, )), # 55
("11131", (55, )), # 56
("1321133112", (56, 0, 19, 26, )), # 57
("31131112", (57, )), # 58
("111312", (58, )), # 59
("132", (59, )), # 60
("311332", (60, 19, 29, )), # 61
("1113222", (61, )), # 62
("13221133112", (62, 19, 26, )), # 63
("3113112221131112", (66, 63, )), # 64
("111312211312", (64, )), # 65
("1321132", (65, )), # 66
("311311222", (66, 60, )), # 67
("11131221133112", (67, 19, 26, )), # 68
("1321131112", (68, )), # 69
("311312", (69, )), # 70
("11132", (70, )), # 71
("13112221133211322112211213322113", (71, 90, 0, 19, 73, )), # 72
("312211322212221121123222113", (72, )), # 73
("111312211312113221133211322112211213322113", (31, 19, 73, )), # 74
("1321132122211322212221121123222113", (74, )), # 75
("3113112211322112211213322113", (75, )), # 76
("111312212221121123222113", (76, )), # 77
("132112211213322113", (77, )), # 78
("31121123222113", (78, )), # 79
("111213322113", (79, )), # 80
("123222113", (80, )), # 81
("3113322113", (60, 81, )), # 82
("1113222113", (82, )), # 83
("1322113", (83, )), # 84
("311311222113", (66, 84, )), # 85
("1113122113", (85, )), # 86
("132113", (86, )), # 87
("3113", (87, )), # 88
("1113", (88, )), # 89
("13", (89, )), # 90
("3", (90, )), # 91
]
# fmt: on
starters = [
"1",
"11",
"21",
"1211",
"111221",
"312211",
"13112221",
"1113213211",
"31131211131221",
]
def look_and_say_length(s: str, n: int) -> int:
if n == 0:
return len(s)
if s in starters:
return look_and_say_length(
"".join(f"{len(list(g))}{k}" for k, g in itertools.groupby(s)), n - 1
)
counts = {i: 0 for i in range(len(atoms))}
idx = next(i for i, (a, _) in enumerate(atoms) if s == a)
counts[idx] = 1
for _ in range(n):
c2 = {i: 0 for i in range(len(atoms))}
for i in counts:
for j in atoms[i][1]:
c2[j] += counts[i]
counts = c2
return sum(counts[i] * len(a[0]) for i, a in enumerate(atoms))
answer_1 = look_and_say_length(line, 40)
print(f"answer 1 is {answer_1}")
answer_2 = look_and_say_length(line, 50)
print(f"answer 2 is {answer_2}")

View File

@ -1,49 +0,0 @@
import itertools
import sys
def is_valid(p: str) -> bool:
if any(c in "iol" for c in p):
return False
if not any(
ord(a) + 1 == ord(b) and ord(b) + 1 == ord(c)
for a, b, c in zip(p, p[1:], p[2:])
):
return False
if sum(len(list(g)) >= 2 for _, g in itertools.groupby(p)) < 2:
return False
return True
assert not is_valid("hijklmmn")
assert not is_valid("abbceffg")
assert not is_valid("abbcegjk")
assert is_valid("abcdffaa")
assert is_valid("ghjaabcc")
def increment(p: str) -> str:
if p[-1] == "z":
return increment(p[:-1]) + "a"
elif p[-1] in "iol":
return p[:-1] + chr(ord(p[-1]) + 2)
else:
return p[:-1] + chr(ord(p[-1]) + 1)
def find_next_password(p: str) -> str:
while not is_valid(p):
p = increment(p)
return p
line = sys.stdin.read().strip()
answer_1 = find_next_password(line)
print(f"answer 1 is {answer_1}")
answer_2 = find_next_password(increment(answer_1))
print(f"answer 2 is {answer_2}")

View File

@ -1,27 +0,0 @@
import json
import sys
from typing import TypeAlias
JsonObject: TypeAlias = dict[str, "JsonObject"] | list["JsonObject"] | int | str
def json_sum(value: JsonObject, ignore: str | None = None) -> int:
if isinstance(value, str):
return 0
elif isinstance(value, int):
return value
elif isinstance(value, list):
return sum(json_sum(v, ignore=ignore) for v in value)
elif ignore not in value.values():
return sum(json_sum(v, ignore=ignore) for v in value.values())
else:
return 0
data: JsonObject = json.load(sys.stdin)
answer_1 = json_sum(data)
print(f"answer 1 is {answer_1}")
answer_2 = json_sum(data, "red")
print(f"answer 2 is {answer_2}")

View File

@ -1,41 +0,0 @@
import itertools
import sys
from collections import defaultdict
from typing import Literal, cast
import parse # type: ignore
def max_change_in_happiness(happiness: dict[str, dict[str, int]]) -> int:
guests = list(happiness)
return max(
sum(
happiness[o][d] + happiness[d][o]
for o, d in zip((guests[0],) + order, order + (guests[0],))
)
for order in map(tuple, itertools.permutations(guests[1:]))
)
lines = sys.stdin.read().splitlines()
happiness: dict[str, dict[str, int]] = defaultdict(dict)
for line in lines:
u1, gain_or_loose, hap, u2 = cast(
tuple[str, Literal["gain", "lose"], int, str],
parse.parse( # type: ignore
"{} would {} {:d} happiness units by sitting next to {}.", line
),
)
happiness[u1][u2] = hap if gain_or_loose == "gain" else -hap
answer_1 = max_change_in_happiness(happiness)
print(f"answer 1 is {answer_1}")
for guest in list(happiness):
happiness["me"][guest] = 0
happiness[guest]["me"] = 0
answer_2 = max_change_in_happiness(happiness)
print(f"answer 2 is {answer_2}")

View File

@ -1,62 +0,0 @@
import sys
from dataclasses import dataclass
from typing import Literal, cast
import parse # type: ignore
@dataclass(frozen=True)
class Reindeer:
name: str
speed: int
fly_time: int
rest_time: int
lines = sys.stdin.read().splitlines()
reindeers: list[Reindeer] = []
for line in lines:
reindeer, speed, speed_time, rest_time = cast(
tuple[str, int, int, int],
parse.parse( # type: ignore
"{} can fly {:d} km/s for {:d} seconds, "
"but then must rest for {:d} seconds.",
line,
),
)
reindeers.append(
Reindeer(name=reindeer, speed=speed, fly_time=speed_time, rest_time=rest_time)
)
target = 1000 if len(reindeers) <= 2 else 2503
states: dict[Reindeer, tuple[Literal["resting", "flying"], int]] = {
reindeer: ("resting", 0) for reindeer in reindeers
}
distances: dict[Reindeer, int] = {reindeer: 0 for reindeer in reindeers}
points: dict[Reindeer, int] = {reindeer: 0 for reindeer in reindeers}
for time in range(target):
for reindeer in reindeers:
if states[reindeer][0] == "flying":
distances[reindeer] += reindeer.speed
top_distance = max(distances.values())
for reindeer in reindeers:
if distances[reindeer] == top_distance:
points[reindeer] += 1
for reindeer in reindeers:
if states[reindeer][1] == time:
if states[reindeer][0] == "resting":
states[reindeer] = ("flying", time + reindeer.fly_time)
else:
states[reindeer] = ("resting", time + reindeer.rest_time)
answer_1 = max(distances.values())
print(f"answer 1 is {answer_1}")
answer_2 = max(points.values()) - 1
print(f"answer 2 is {answer_2}")

View File

@ -1,57 +0,0 @@
import math
import sys
from typing import Sequence, cast
import parse # type: ignore
def score(ingredients: list[list[int]], teaspoons: Sequence[int]) -> int:
return math.prod(
max(
0,
sum(
ingredient[prop] * teaspoon
for ingredient, teaspoon in zip(ingredients, teaspoons)
),
)
for prop in range(len(ingredients[0]) - 1)
)
lines = sys.stdin.read().splitlines()
ingredients: list[list[int]] = []
for line in lines:
_, *scores = cast(
tuple[str, int, int, int, int, int],
parse.parse( # type: ignore
"{}: capacity {:d}, durability {:d}, flavor {:d}, "
"texture {:d}, calories {:d}",
line,
),
)
ingredients.append(scores)
total_teaspoons = 100
calories: list[int] = []
scores: list[int] = []
for a in range(total_teaspoons + 1):
for b in range(total_teaspoons + 1 - a):
for c in range(total_teaspoons + 1 - a - b):
teaspoons = (a, b, c, total_teaspoons - a - b - c)
scores.append(score(ingredients, teaspoons))
calories.append(
sum(
ingredient[-1] * teaspoon
for ingredient, teaspoon in zip(ingredients, teaspoons)
)
)
answer_1 = max(scores)
print(f"answer 1 is {answer_1}")
answer_2 = max(score for score, calory in zip(scores, calories) if calory == 500)
print(f"answer 2 is {answer_2}")

View File

@ -1,51 +0,0 @@
import operator as op
import re
import sys
from collections import defaultdict
from typing import Callable
MFCSAM: dict[str, int] = {
"children": 3,
"cats": 7,
"samoyeds": 2,
"pomeranians": 3,
"akitas": 0,
"vizslas": 0,
"goldfish": 5,
"trees": 3,
"cars": 2,
"perfumes": 1,
}
lines = sys.stdin.readlines()
aunts: list[dict[str, int]] = [
{
match[1]: int(match[2])
for match in re.findall(R"((?P<compound>[^:, ]+): (?P<quantity>\d+))", line)
}
for line in lines
]
def match(operators: dict[str, Callable[[int, int], bool]]) -> int:
return next(
i
for i, aunt in enumerate(aunts, start=1)
if all(operators[k](aunt[k], MFCSAM[k]) for k in aunt)
)
answer_1 = match(defaultdict(lambda: op.eq))
print(f"answer 1 is {answer_1}")
answer_2 = match(
defaultdict(
lambda: op.eq,
trees=op.gt,
cats=op.gt,
pomeranians=op.lt,
goldfish=op.lt,
)
)
print(f"answer 2 is {answer_2}")

View File

@ -1,30 +0,0 @@
import sys
from typing import Iterator
def iter_combinations(value: int, containers: list[int]) -> Iterator[tuple[int, ...]]:
if value < 0:
return
if value == 0:
yield ()
for i in range(len(containers)):
for combination in iter_combinations(
value - containers[i], containers[i + 1 :]
):
yield (containers[i],) + combination
containers = [int(c) for c in sys.stdin.read().split()]
total = 25 if len(containers) <= 5 else 150
combinations = [combination for combination in iter_combinations(total, containers)]
answer_1 = len(combinations)
print(f"answer 1 is {answer_1}")
min_containers = min(len(combination) for combination in combinations)
answer_2 = sum(1 for combination in combinations if len(combination) == min_containers)
print(f"answer 2 is {answer_2}")

View File

@ -1,66 +0,0 @@
import itertools
import sys
import numpy as np
from numpy.typing import NDArray
grid0 = np.array([[c == "#" for c in line] for line in sys.stdin.read().splitlines()])
# add an always off circle around
grid0 = np.concatenate(
[
np.zeros((grid0.shape[0] + 2, 1), dtype=bool),
np.concatenate(
[
np.zeros((1, grid0.shape[1]), dtype=bool),
grid0,
np.zeros((1, grid0.shape[1]), dtype=bool),
]
),
np.zeros((grid0.shape[0] + 2, 1), dtype=bool),
],
axis=1,
)
moves = list(itertools.product([-1, 0, 1], repeat=2))
moves.remove((0, 0))
jjs, iis = np.meshgrid(
np.arange(1, grid0.shape[0] - 1, dtype=int),
np.arange(1, grid0.shape[1] - 1, dtype=int),
)
iis, jjs = iis.flatten(), jjs.flatten()
ins = iis[:, None] + np.array(moves)[:, 0]
jns = jjs[:, None] + np.array(moves)[:, 1]
def game_of_life(grid: NDArray[np.bool_]) -> NDArray[np.bool_]:
neighbors_on = grid[ins, jns].sum(axis=1)
cells_on = grid[iis, jjs]
grid = np.zeros_like(grid)
grid[iis, jjs] = (neighbors_on == 3) | (cells_on & (neighbors_on == 2))
return grid
grid = grid0
n_steps = 4 if len(grid) < 10 else 100
for _ in range(n_steps):
grid = game_of_life(grid)
answer_1 = grid.sum()
print(f"answer 1 is {answer_1}")
n_steps = 5 if len(grid) < 10 else 100
grid = grid0
for _ in range(n_steps):
grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True
grid = game_of_life(grid)
grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True
answer_2 = sum(cell for line in grid for cell in line)
print(f"answer 2 is {answer_2}")

View File

@ -1,56 +0,0 @@
import sys
from collections import defaultdict
replacements_s, molecule = sys.stdin.read().split("\n\n")
REPLACEMENTS: dict[str, list[str]] = defaultdict(list)
for replacement_s in replacements_s.splitlines():
p = replacement_s.split(" => ")
REPLACEMENTS[p[0]].append(p[1])
molecule = molecule.strip()
generated = [
molecule[:i] + replacement + molecule[i + len(symbol) :]
for symbol, replacements in REPLACEMENTS.items()
for replacement in replacements
for i in range(len(molecule))
if molecule[i:].startswith(symbol)
]
answer_1 = len(set(generated))
print(f"answer 1 is {answer_1}")
inversion: dict[str, str] = {
replacement: symbol
for symbol, replacements in REPLACEMENTS.items()
for replacement in replacements
}
# there is actually only one way to create the molecule, and we can greedily replace
# tokens with their replacements, e.g., if H => OH then we can replace OH by H directly
# without thinking
count = 0
while molecule != "e":
i = 0
m2 = ""
while i < len(molecule):
found = False
for replacement in inversion:
if molecule[i:].startswith(replacement):
m2 += inversion[replacement]
i += len(replacement)
count += 1
found = True
break
if not found:
m2 += molecule[i]
i += 1
# print(m2)
molecule = m2
answer_2 = count
print(f"answer 2 is {count}")

View File

@ -1,20 +0,0 @@
import sys
import numpy as np
lines = sys.stdin.read().splitlines()
length, width, height = np.array(
[[int(c) for c in line.split("x")] for line in lines]
).T
lw, wh, hl = (length * width, width * height, height * length)
answer_1 = np.sum(2 * (lw + wh + hl) + np.min(np.stack([lw, wh, hl]), axis=0))
print(f"answer 1 is {answer_1}")
answer_2 = np.sum(
length * width * height
+ 2 * np.min(np.stack([length + width, length + height, height + width]), axis=0)
)
print(f"answer 2 is {answer_2}")

View File

@ -1,28 +0,0 @@
import itertools
import sys
target = int(sys.stdin.read())
def presents(n: int, elf: int, max: int = target) -> int:
count = 0
k = 1
while k * k < n:
if n % k == 0:
if n // k <= max:
count += elf * k
if k <= max:
count += elf * (n // k)
k += 1
if k * k == n and k <= max:
count += elf * k
return count
answer_1 = next(n for n in itertools.count(1) if presents(n, 10) >= target)
print(f"answer 1 is {answer_1}")
answer_2 = next(n for n in itertools.count(1) if presents(n, 11, 50) >= target)
print(f"answer 2 is {answer_2}")

View File

@ -1,66 +0,0 @@
import itertools
import sys
from math import ceil
from typing import TypeAlias
Modifier: TypeAlias = tuple[str, int, int, int]
WEAPONS: list[Modifier] = [
("Dagger", 8, 4, 0),
("Shortsword", 10, 5, 0),
("Warhammer", 25, 6, 0),
("Longsword", 40, 7, 0),
("Greataxe", 74, 8, 0),
]
ARMORS: list[Modifier] = [
("", 0, 0, 0),
("Leather", 13, 0, 1),
("Chainmail", 31, 0, 2),
("Splintmail", 53, 0, 3),
("Bandedmail", 75, 0, 4),
("Platemail", 102, 0, 5),
]
RINGS: list[Modifier] = [
("", 0, 0, 0),
("Damage +1", 25, 1, 0),
("Damage +2", 50, 2, 0),
("Damage +3", 100, 3, 0),
("Defense +1", 20, 0, 1),
("Defense +2", 40, 0, 2),
("Defense +3", 80, 0, 3),
]
lines = sys.stdin.read().splitlines()
player_hp = 100
boss_attack = int(lines[1].split(":")[1].strip())
boss_armor = int(lines[2].split(":")[1].strip())
boss_hp = int(lines[0].split(":")[1].strip())
min_cost, max_cost = 1_000_000, 0
for equipments in itertools.product(WEAPONS, ARMORS, RINGS, RINGS):
if equipments[-1][0] != "" and equipments[-2] == equipments[-1]:
continue
cost, player_attack, player_armor = (
sum(equipment[1:][k] for equipment in equipments) for k in range(3)
)
if ceil(boss_hp / max(1, player_attack - boss_armor)) <= ceil(
player_hp / max(1, boss_attack - player_armor)
):
min_cost = min(cost, min_cost)
else:
max_cost = max(cost, max_cost)
answer_1 = min_cost
print(f"answer 1 is {answer_1}")
answer_2 = max_cost
print(f"answer 2 is {answer_2}")

View File

@ -1,34 +0,0 @@
import sys
from collections import defaultdict
line = sys.stdin.read().strip()
def process(directions: str) -> dict[tuple[int, int], int]:
counts: dict[tuple[int, int], int] = defaultdict(lambda: 0)
counts[0, 0] = 1
x, y = (0, 0)
for c in directions:
match c:
case ">":
x += 1
case "<":
x -= 1
case "^":
y -= 1
case "v":
y += 1
case _:
raise ValueError()
counts[x, y] += 1
return counts
answer_1 = len(process(line))
print(f"answer 1 is {answer_1}")
answer_2 = len(process(line[::2]) | process(line[1::2]))
print(f"answer 2 is {answer_2}")

View File

@ -1,16 +0,0 @@
import hashlib
import itertools
import sys
line = sys.stdin.read().strip()
it = iter(itertools.count(1))
answer_1 = next(
i for i in it if hashlib.md5(f"{line}{i}".encode()).hexdigest().startswith("00000")
)
print(f"answer 1 is {answer_1}")
answer_2 = next(
i for i in it if hashlib.md5(f"{line}{i}".encode()).hexdigest().startswith("000000")
)
print(f"answer 2 is {answer_2}")

View File

@ -1,36 +0,0 @@
import sys
VOWELS = "aeiou"
FORBIDDEN = {"ab", "cd", "pq", "xy"}
def is_nice_1(s: str) -> bool:
if sum(c in VOWELS for c in s) < 3:
return False
if not any(a == b for a, b in zip(s[:-1:], s[1::])):
return False
if any(s.find(f) >= 0 for f in FORBIDDEN):
return False
return True
def is_nice_2(s: str) -> bool:
if not any(s.find(s[i : i + 2], i + 2) >= 0 for i in range(len(s))):
return False
if not any(a == b for a, b in zip(s[:-1:], s[2::])):
return False
return True
lines = sys.stdin.read().splitlines()
answer_1 = sum(map(is_nice_1, lines))
print(f"answer 1 is {answer_1}")
answer_2 = sum(map(is_nice_2, lines))
print(f"answer 2 is {answer_2}")

View File

@ -1,33 +0,0 @@
import sys
from typing import Literal, cast
import numpy as np
import parse # type: ignore
lines = sys.stdin.read().splitlines()
lights_1 = np.zeros((1000, 1000), dtype=bool)
lights_2 = np.zeros((1000, 1000), dtype=int)
for line in lines:
action, sx, sy, ex, ey = cast(
tuple[Literal["turn on", "turn off", "toggle"], int, int, int, int],
parse.parse("{} {:d},{:d} through {:d},{:d}", line), # type: ignore
)
ex, ey = ex + 1, ey + 1
match action:
case "turn on":
lights_1[sx:ex, sy:ey] = True
lights_2[sx:ex, sy:ey] += 1
case "turn off":
lights_1[sx:ex, sy:ey] = False
lights_2[sx:ex, sy:ey] = np.maximum(lights_2[sx:ex, sy:ey] - 1, 0)
case "toggle":
lights_1[sx:ex, sy:ey] = ~lights_1[sx:ex, sy:ey]
lights_2[sx:ex, sy:ey] += 2
answer_1 = lights_1.sum()
print(f"answer 1 is {answer_1}")
answer_2 = lights_2.sum()
print(f"answer 2 is {answer_2}")

View File

@ -1,101 +0,0 @@
import logging
import operator
import os
import sys
from typing import Callable
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
OPERATORS = {
"AND": operator.and_,
"OR": operator.or_,
"LSHIFT": operator.lshift,
"RSHIFT": operator.rshift,
}
ValueGetter = Callable[[dict[str, int]], int]
Signals = dict[
str,
tuple[
tuple[str, str],
tuple[ValueGetter, ValueGetter],
Callable[[int, int], int],
],
]
def zero_op(_a: int, _b: int) -> int:
return 0
def value_of(key: str) -> tuple[str, Callable[[dict[str, int]], int]]:
try:
return "", lambda _p, _v=int(key): _v
except ValueError:
return key, lambda values: values[key]
lines = sys.stdin.read().splitlines()
signals: Signals = {}
values: dict[str, int] = {"": 0}
for line in lines:
command, signal = line.split(" -> ")
if command.startswith("NOT"):
name = command.split(" ")[1]
signals[signal] = (
(name, ""),
(lambda values, _n=name: values[_n], lambda _v: 0),
lambda a, _b: ~a,
)
elif not any(command.find(name) >= 0 for name in OPERATORS):
try:
values[signal] = int(command)
except ValueError:
signals[signal] = (
(command, ""),
(lambda values, _c=command: values[_c], lambda _v: 0),
lambda a, _b: a,
)
else:
op: Callable[[int, int], int] = zero_op
lhs_s, rhs_s = "", ""
for name in OPERATORS:
if command.find(name) >= 0:
op = OPERATORS[name]
lhs_s, rhs_s = command.split(f" {name} ")
break
lhs_s, lhs_fn = value_of(lhs_s)
rhs_s, rhs_fn = value_of(rhs_s)
signals[signal] = ((lhs_s, rhs_s), (lhs_fn, rhs_fn), op)
def process(
signals: Signals,
values: dict[str, int],
) -> dict[str, int]:
while signals:
signal = next(s for s in signals if all(p in values for p in signals[s][0]))
_, deps, command = signals[signal]
values[signal] = command(deps[0](values), deps[1](values)) % 65536
del signals[signal]
return values
values_1 = process(signals.copy(), values.copy())
logging.info("\n" + "\n".join(f"{k}: {values_1[k]}" for k in sorted(values_1)))
answer_1 = values_1["a"]
print(f"answer 1 is {answer_1}")
values_2 = process(signals.copy(), values | {"b": values_1["a"]})
answer_2 = values_2["a"]
print(f"answer 2 is {answer_2}")

View File

@ -1,35 +0,0 @@
import logging
import os
import sys
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
lines = sys.stdin.read().splitlines()
answer_1 = sum(
# left and right quotes (not in memory)
2
# each \\ adds one character in the literals (compared to memory)
+ line.count(R"\\")
# each \" adds one character in the literals (compared to memory)
+ line[1:-1].count(R"\"")
# each \xFF adds 3 characters in the literals (compared to memory), but we must not
# count A\\x (A != \), but we must count A\\\x (A != \) - in practice we should also
# avoid \\\\x, etc., but this does not occur in the examples and the actual input
+ 3 * (line.count(R"\x") - line.count(R"\\x") + line.count(R"\\\x"))
for line in lines
)
print(f"answer 1 is {answer_1}")
answer_2 = sum(
# needs to wrap in quotes (2 characters)
2
# needs to escape every \ with an extra \
+ line.count("\\")
# needs to escape every " with an extra \ (including the first and last ones)
+ line.count('"')
for line in lines
)
print(f"answer 2 is {answer_2}")

View File

@ -1,26 +0,0 @@
import itertools
import sys
from collections import defaultdict
from typing import cast
import parse # type: ignore
lines = sys.stdin.read().splitlines()
distances: dict[str, dict[str, int]] = defaultdict(dict)
for line in lines:
origin, destination, length = cast(
tuple[str, str, int], parse.parse("{} to {} = {:d}", line) # type: ignore
)
distances[origin][destination] = distances[destination][origin] = length
distance_of_routes = {
route: sum(distances[o][d] for o, d in zip(route[:-1], route[1:]))
for route in map(tuple, itertools.permutations(distances))
}
answer_1 = min(distance_of_routes.values())
print(f"answer 1 is {answer_1}")
answer_2 = max(distance_of_routes.values())
print(f"answer 2 is {answer_2}")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,44 +0,0 @@
import sys
from math import prod
values = [[int(c) for c in row] for row in sys.stdin.read().splitlines()]
n_rows, n_cols = len(values), len(values[0])
def neighbors(point: tuple[int, int]):
i, j = point
for di, dj in ((-1, 0), (+1, 0), (0, -1), (0, +1)):
if 0 <= i + di < n_rows and 0 <= j + dj < n_cols:
yield (i + di, j + dj)
def basin(start: tuple[int, int]) -> set[tuple[int, int]]:
visited: set[tuple[int, int]] = set()
queue = [start]
while queue:
i, j = queue.pop()
if (i, j) in visited or values[i][j] == 9:
continue
visited.add((i, j))
queue.extend(neighbors((i, j)))
return visited
low_points = [
(i, j)
for i in range(n_rows)
for j in range(n_cols)
if all(values[ti][tj] > values[i][j] for ti, tj in neighbors((i, j)))
]
# part 1
answer_1 = sum(values[i][j] + 1 for i, j in low_points)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = prod(sorted(len(basin(point)) for point in low_points)[-3:])
print(f"answer 2 is {answer_2}")

View File

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

View File

@ -1,120 +0,0 @@
import sys
from typing import Sequence, TypeVar
import numpy as np
T = TypeVar("T")
def print_tower(tower: np.ndarray, out: str = "#"):
print("-" * (tower.shape[1] + 2))
non_empty = False
for row in reversed(range(1, tower.shape[0])):
if not non_empty and not tower[row, :].any():
continue
non_empty = True
print("|" + "".join(out if c else "." for c in tower[row, :]) + "|")
print("+" + "-" * tower.shape[1] + "+")
def tower_height(tower: np.ndarray) -> int:
return int(tower.shape[0] - tower[::-1, :].argmax(axis=0).min() - 1)
def next_cycle(sequence: Sequence[T], index: int) -> tuple[T, int]:
t = sequence[index]
index = (index + 1) % len(sequence)
return t, index
ROCKS = [
np.array([(0, 0), (0, 1), (0, 2), (0, 3)]),
np.array([(0, 1), (1, 0), (1, 1), (1, 2), (2, 1)]),
np.array([(0, 0), (0, 1), (0, 2), (1, 2), (2, 2)]),
np.array([(0, 0), (1, 0), (2, 0), (3, 0)]),
np.array([(0, 0), (0, 1), (1, 0), (1, 1)]),
]
WIDTH = 7
START_X = 2
EMPTY_BLOCKS = np.zeros((10, WIDTH), dtype=bool)
def build_tower(
n_rocks: int,
jets: str,
early_stop: bool = False,
init: np.ndarray = np.ones(WIDTH, dtype=bool),
) -> tuple[np.ndarray, int, int, dict[int, int]]:
tower = EMPTY_BLOCKS.copy()
tower[0, :] = init
done_at: dict[tuple[int, int], int] = {}
heights: dict[int, int] = {}
i_jet, i_rock = 0, 0
rock_count = 0
for rock_count in range(n_rocks):
if early_stop:
if i_rock == 0 and (i_rock, i_jet) in done_at:
break
done_at[i_rock, i_jet] = rock_count
y_start = tower.shape[0] - tower[::-1, :].argmax(axis=0).min() + 3
rock, i_rock = next_cycle(ROCKS, i_rock)
rock_y = rock[:, 0] + y_start
rock_x = rock[:, 1] + START_X
if rock_y.max() >= tower.shape[0]:
tower = np.concatenate([tower, EMPTY_BLOCKS], axis=0)
while True:
jet, i_jet = next_cycle(jets, i_jet)
dx = 0
if jet == ">" and rock_x.max() < WIDTH - 1:
dx = 1
elif jet == "<" and rock_x.min() > 0:
dx = -1
if dx != 0 and not tower[rock_y, rock_x + dx].any():
rock_x = rock_x + dx
# move down
rock_y -= 1
if tower[rock_y, rock_x].any():
rock_y += 1
break
heights[rock_count] = tower_height(tower)
tower[rock_y, rock_x] = True
return tower, rock_count, done_at.get((i_rock, i_jet), -1), heights
line = sys.stdin.read().strip()
tower, *_ = build_tower(2022, line)
answer_1 = tower_height(tower)
print(f"answer 1 is {answer_1}")
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
# 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
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}")

View File

@ -1,51 +0,0 @@
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,182 +0,0 @@
import sys
from typing import Literal
import numpy as np
import parse
from tqdm import tqdm
Reagent = Literal["ore", "clay", "obsidian", "geode"]
REAGENTS: tuple[Reagent, ...] = (
"ore",
"clay",
"obsidian",
"geode",
)
IntOfReagent = dict[Reagent, int]
class State:
robots: IntOfReagent
reagents: IntOfReagent
def __init__(
self,
robots: IntOfReagent | None = None,
reagents: IntOfReagent | None = None,
):
if robots is None:
assert reagents is None
self.reagents = {reagent: 0 for reagent in REAGENTS}
self.robots = {reagent: 0 for reagent in REAGENTS}
self.robots["ore"] = 1
else:
assert robots is not None and reagents is not None
self.robots = robots
self.reagents = reagents
def __eq__(self, other) -> bool:
return (
isinstance(other, State)
and self.robots == other.robots
and self.reagents == other.reagents
)
def __hash__(self) -> int:
return hash(tuple((self.robots[r], self.reagents[r]) for r in REAGENTS))
def __str__(self) -> str:
return "State({}, {})".format(
"/".join(str(self.robots[k]) for k in REAGENTS),
"/".join(str(self.reagents[k]) for k in REAGENTS),
)
def __repr__(self) -> str:
return str(self)
def dominates(lhs: State, rhs: State):
return all(
lhs.robots[r] >= rhs.robots[r] and lhs.reagents[r] >= rhs.reagents[r]
for r in REAGENTS
)
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.,
# in the first toy blueprint, we need at most 4 ore robots, 14 clay ones and 7
# obsidian ones
maximums = {
name: max(blueprint[r].get(name, 0) for r in REAGENTS) for name in REAGENTS
}
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()
for state in state_after_t[t - 1]:
robots_that_can_be_built = [
robot
for robot in REAGENTS
if all(
state.reagents[reagent] >= blueprint[robot].get(reagent, 0)
for reagent in REAGENTS
)
]
states_for_t.add(
State(
robots=state.robots,
reagents={
reagent: state.reagents[reagent] + state.robots[reagent]
for reagent in REAGENTS
},
)
)
if "geode" in robots_that_can_be_built:
robots_that_can_be_built = ["geode"]
else:
robots_that_can_be_built = [
robot
for robot in robots_that_can_be_built
if state.robots[robot] < maximums[robot]
]
for robot in robots_that_can_be_built:
robots = state.robots.copy()
robots[robot] += 1
reagents = {
reagent: state.reagents[reagent]
+ state.robots[reagent]
- blueprint[robot].get(reagent, 0)
for reagent in REAGENTS
}
states_for_t.add(State(robots=robots, reagents=reagents))
# use numpy to switch computation of dominated states -> store each state
# as a 8 array and use numpy broadcasting to find dominated states
states_after = np.asarray(list(states_for_t))
np_states = np.array(
[
[state.robots[r] for r in REAGENTS]
+ [state.reagents[r] for r in REAGENTS]
for state in states_after
]
)
to_keep = []
while len(np_states) > 0:
first_dom = (np_states[1:] >= np_states[0]).all(axis=1).any()
if first_dom:
np_states = np_states[1:]
else:
to_keep.append(np_states[0])
np_states = np_states[1:][~(np_states[1:] <= np_states[0]).all(axis=1)]
state_after_t[t] = {
State(
robots=dict(zip(REAGENTS, row[:4])),
reagents=dict(zip(REAGENTS, row[4:])),
)
for row in to_keep
}
return max(state.reagents["geode"] for state in state_after_t[max_time])
answer_1 = sum(
(i_blueprint + 1) * run(blueprint, 24)
for i_blueprint, blueprint in enumerate(blueprints)
)
print(f"answer 1 is {answer_1}")
answer_2 = run(blueprints[0], 32) * run(blueprints[1], 32) * run(blueprints[2], 32)
print(f"answer 2 is {answer_2}")

View File

@ -1,74 +0,0 @@
from __future__ import annotations
import sys
class Number:
current: int
value: int
def __init__(self, value: int):
self.current = 0
self.value = value
def __str__(self):
return str(self.value)
def __repr__(self):
return str(self)
def decrypt(numbers: list[Number], key: int, rounds: int) -> int:
numbers = numbers.copy()
original = numbers.copy()
for index, number in enumerate(numbers):
number.current = index
for _ in range(rounds):
for number in original:
index = number.current
offset = (number.value * key) % (len(numbers) - 1)
target = index + offset
# need to wrap
if target >= len(numbers):
target = offset - (len(numbers) - index) + 1
for number_2 in numbers[target:index]:
number_2.current += 1
numbers = (
numbers[:target]
+ [number]
+ numbers[target:index]
+ numbers[index + 1 :]
)
else:
for number_2 in numbers[index : target + 1]:
number_2.current -= 1
numbers = (
numbers[:index]
+ numbers[index + 1 : target + 1]
+ [number]
+ numbers[target + 1 :]
)
number.current = target
index_of_0 = next(
filter(lambda index: numbers[index].value == 0, range(len(numbers)))
)
return sum(
numbers[(index_of_0 + offset) % len(numbers)].value * key
for offset in (1000, 2000, 3000)
)
numbers = [Number(int(x)) for i, x in enumerate(sys.stdin.readlines())]
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,107 +0,0 @@
import operator
import sys
from typing import Callable
def compute(monkeys: dict[str, int | tuple[str, str, str]], monkey: str) -> int:
value = monkeys[monkey]
if isinstance(value, int):
return value
else:
op: dict[str, Callable[[int, int], int]] = {
"+": operator.add,
"-": operator.sub,
"*": operator.mul,
"/": operator.floordiv,
}
value = op[value[1]](compute(monkeys, value[0]), compute(monkeys, value[2]))
monkeys[monkey] = value
return value
def invert(
monkeys: dict[str, int | tuple[str, str, str]], monkey: str, target: int
) -> dict[str, int | tuple[str, str, str]]:
"""
Revert the given mapping from monkey name to value or operation such that
the value from 'monkey' is computable by inverting operation until the root is
found.
Args:
monkeys: Dictionary of monkeys, that will be updated and returned.
monkey: Name of the monkey to start from.
target: Target value to set for the monkey that depends on root.
Returns:
The given dictionary of monkeys.
"""
monkeys = monkeys.copy()
depends: dict[str, str] = {}
for m, v in monkeys.items():
if isinstance(v, int):
continue
op1, _, op2 = v
assert op1 not in depends
assert op2 not in depends
depends[op1] = m
depends[op2] = m
invert_op = {"+": "-", "-": "+", "*": "/", "/": "*"}
current = monkey
while True:
dep = depends[current]
if dep == "root":
monkeys[current] = target
break
val = monkeys[dep]
assert not isinstance(val, int)
op1, ope, op2 = val
if op1 == current:
monkeys[current] = (dep, invert_op[ope], op2)
elif ope in ("+", "*"):
monkeys[current] = (dep, invert_op[ope], op1)
else:
monkeys[current] = (op1, ope, dep)
current = dep
return monkeys
lines = sys.stdin.read().splitlines()
monkeys: dict[str, int | tuple[str, str, str]] = {}
op_monkeys: set[str] = set()
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)
op_monkeys.add(name)
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}")

View File

@ -1,223 +0,0 @@
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,103 +0,0 @@
import itertools
import sys
from collections import defaultdict
Directions = list[
tuple[
str, tuple[int, int], tuple[tuple[int, int], tuple[int, int], tuple[int, int]]
]
]
# (Y, X)
DIRECTIONS: Directions = [
("N", (-1, 0), ((-1, -1), (-1, 0), (-1, 1))),
("S", (1, 0), ((1, -1), (1, 0), (1, 1))),
("W", (0, -1), ((-1, -1), (0, -1), (1, -1))),
("E", (0, 1), ((-1, 1), (0, 1), (1, 1))),
]
def min_max_yx(positions: set[tuple[int, int]]) -> tuple[int, int, int, int]:
ys, xs = {y for y, x in positions}, {x for y, x in positions}
return min(ys), min(xs), max(ys), max(xs)
def print_positions(positions: set[tuple[int, int]]):
min_y, min_x, max_y, max_x = min_max_yx(positions)
print(
"\n".join(
"".join(
"#" if (y, x) in positions else "." for x in range(min_x - 1, max_x + 2)
)
for y in range(min_y - 1, max_y + 2)
)
)
def round(
positions: set[tuple[int, int]],
directions: Directions,
):
to_move: dict[tuple[int, int], list[tuple[int, int]]] = defaultdict(lambda: [])
for y, x in positions:
elves = {
(dy, dx): (y + dy, x + dx) in positions
for dy, dx in itertools.product((-1, 0, 1), (-1, 0, 1))
if (dy, dx) != (0, 0)
}
if not any(elves.values()):
to_move[y, x].append((y, x))
continue
found: str | None = None
for d, (dy, dx), d_yx_check in directions:
if not any(elves[dy, dx] for dy, dx in d_yx_check):
found = d
to_move[y + dy, x + dx].append((y, x))
break
if found is None:
to_move[y, x].append((y, x))
positions.clear()
for ty, tx in to_move:
if len(to_move[ty, tx]) > 1:
positions.update(to_move[ty, tx])
else:
positions.add((ty, tx))
directions.append(directions.pop(0))
POSITIONS = {
(i, j)
for i, row in enumerate(sys.stdin.read().splitlines())
for j, col in enumerate(row)
if col == "#"
}
# === part 1 ===
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)
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 ===
p2, d2 = POSITIONS.copy(), DIRECTIONS.copy()
answer_2 = 0
while True:
answer_2 += 1
backup = p2.copy()
round(p2, d2)
if backup == p2:
break
print(f"answer 2 is {answer_2}")

View File

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

View File

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

View File

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

View File

@ -1,100 +0,0 @@
import os
import sys
from typing import Literal, cast
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
Symbol = Literal["|", "-", "L", "J", "7", "F", ".", "S"]
lines: list[list[Symbol]] = [
[cast(Symbol, symbol) for symbol in line] for line in sys.stdin.read().splitlines()
]
# find starting point
si, sj = next(
(i, j)
for i in range(len(lines))
for j in range(len(lines[0]))
if lines[i][j] == "S"
)
# find one of the two outputs
ni, nj = si, sj
for ni, nj, chars in (
(si - 1, sj, "|7F"),
(si + 1, sj, "|LJ"),
(si, sj - 1, "-LF"),
(si, sj + 1, "-J7"),
):
if lines[ni][nj] in chars:
break
# part 1 - find the loop (re-used in part 2)
loop = [(si, sj), (ni, nj)]
while True:
pi, pj = loop[-2]
i, j = loop[-1]
sym = lines[i][j]
if sym == "|" and pi > i or sym in "JL" and pi == i:
i -= 1
elif sym == "|" and pi < i or sym in "7F" and pi == i:
i += 1
elif sym == "-" and pj > j or sym in "J7" and pj == j:
j -= 1
elif sym == "-" and pj < j or sym in "LF" and pj == j:
j += 1
if (i, j) == (si, sj):
break
loop.append((i, j))
answer_1 = len(loop) // 2
print(f"answer 1 is {answer_1}")
# part 2
# replace S by an appropriate character for the loop below
di1, dj1 = loop[1][0] - loop[0][0], loop[1][1] - loop[0][1]
di2, dj2 = loop[0][0] - loop[-1][0], loop[0][1] - loop[-1][1]
mapping: dict[tuple[int, int], dict[tuple[int, int], Symbol]] = {
(0, 1): {(0, 1): "-", (-1, 0): "F", (1, 0): "L"},
(0, -1): {(0, -1): "-", (-1, 0): "7", (1, 0): "J"},
(1, 0): {(1, 0): "|", (0, 1): "7", (0, -1): "F"},
(-1, 0): {(-1, 0): "|", (0, -1): "L", (0, 1): "J"},
}
lines[si][sj] = mapping[di1, dj1][di2, dj2]
# find the points inside the loop using an adaptation of ray casting for a discrete
# grid (https://stackoverflow.com/a/218081/2666289)
#
# use a set for faster '... in loop' check
#
loop_s = set(loop)
inside: set[tuple[int, int]] = set()
for i in range(len(lines)):
cnt = 0
for j in range(len(lines[0])):
if (i, j) not in loop_s and cnt % 2 == 1:
inside.add((i, j))
if (i, j) in loop_s and lines[i][j] in "|LJ":
cnt += 1
if VERBOSE:
for i in range(len(lines)):
for j in range(len(lines[0])):
if (i, j) == (si, sj):
print("\033[91mS\033[0m", end="")
elif (i, j) in loop:
print(lines[i][j], end="")
elif (i, j) in inside:
print("\033[92mI\033[0m", end="")
else:
print(".", end="")
print()
answer_2 = len(inside)
print(f"answer 2 is {answer_2}")

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