35 Commits

Author SHA1 Message Date
Mikael CAPELLE
b21c50562f Refactor 2024 day 3.
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-03 15:22:37 +01:00
Mikael CAPELLE
211483f679 Add .drone.yml for CI.
All checks were successful
continuous-integration/drone Build is passing
2024-12-03 14:12:42 +01:00
Mikael CAPELLE
acb767184e Fix linting. 2024-12-03 14:11:29 +01:00
Mikael CAPELLE
cb0145baa2 2024 day 3. 2024-12-03 08:29:25 +01:00
Mikaël Capelle
a4ad0259a9 Fix 2024 day 2. 2024-12-02 18:44:50 +01:00
Mikael CAPELLE
82452c0751 Update Python dependencies. 2024-12-02 17:08:50 +01:00
Mikael CAPELLE
79cc208875 2024 day 2. 2024-12-02 15:42:56 +01:00
Mikaël Capelle
4dd2d5d9c9 2024 day 1. 2024-12-01 10:26:02 +01:00
Mikaël Capelle
def4305c1c 2015 day 22, part 1. 2024-12-01 10:25:49 +01:00
Mikaël Capelle
3edaa249fc 2015 day 21. 2024-01-20 17:57:37 +01:00
Mikaël Capelle
57fcb47fe9 2015 day 20. 2024-01-06 22:07:34 +01:00
Mikaël Capelle
cfa7718475 2015 day 19. 2024-01-06 21:35:48 +01:00
Mikaël Capelle
2d23e355b2 2015 day 18. 2024-01-06 16:43:35 +01:00
Mikaël Capelle
fab4899715 2015 day 17. 2024-01-06 15:46:43 +01:00
Mikaël Capelle
b6e20eefa3 2015 day 16. 2024-01-06 15:27:45 +01:00
Mikaël Capelle
872fd72dcd 2015 day 15. 2024-01-06 15:11:47 +01:00
Mikaël Capelle
98f28e96f8 2015 day 12, 13 & 14. 2024-01-06 14:56:30 +01:00
Mikaël Capelle
ed7aba80ad 2015 day 11. 2024-01-06 11:46:59 +01:00
Mikaël Capelle
e507dad5e0 2015 day 10. 2024-01-06 11:30:10 +01:00
Mikael CAPELLE
04172beb5a 2015 day 9. 2024-01-05 14:46:05 +01:00
Mikael CAPELLE
15ef67e757 2015 day 8. 2024-01-05 10:01:02 +01:00
Mikaël Capelle
cd0ada785c 2015 day 4, 5, 6, 7. 2024-01-04 21:05:42 +01:00
Mikael CAPELLE
42bd8d6983 2015 day 3. 2024-01-04 18:36:30 +01:00
Mikael CAPELLE
0567ab7440 2015 day 1 & 2. 2024-01-04 18:27:17 +01:00
Mikaël Capelle
7d2eb6b5ec Faster 2023 day 24 (part 1). 2024-01-01 18:44:13 +01:00
Mikaël Capelle
3a9c7e728b Minor cleaning 2023. 2023-12-30 19:35:06 +01:00
Mikaël Capelle
d002e419c3 2023 day 25. 2023-12-25 10:36:36 +01:00
Mikaël Capelle
19d93e0c1d 2023 day 24. 2023-12-25 10:36:29 +01:00
Mikaël Capelle
5c05ee5c85 2023 day 23. 2023-12-23 11:05:35 +01:00
Mikaël Capelle
103af21915 2021 day 9. 2023-12-22 21:26:13 +01:00
Mikaël Capelle
af2fbf2da1 2023 day 22. 2023-12-22 14:49:31 +01:00
Mikaël Capelle
c496ea25c9 2023 day 21, version 2. 2023-12-21 21:56:38 +01:00
Mikael CAPELLE
5f8c74fd1c 2023 day 21. 2023-12-21 16:47:43 +01:00
Mikael CAPELLE
dda2be2505 2023 day 20. 2023-12-20 14:27:25 +01:00
Mikael CAPELLE
12891194bb Poetry stuff. 2023-12-19 17:45:33 +01:00
393 changed files with 13800 additions and 146 deletions

12
.drone.yml Normal file
View File

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

5
.gitignore vendored
View File

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

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

1284
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

42
pyproject.toml Normal file
View File

@@ -0,0 +1,42 @@
[tool.poetry]
name = "holt59-advent-of-code"
version = "0.1.0"
description = ""
authors = ["Mikael CAPELLE <capelle.mikael@gmail.com>"]
license = "MIT"
readme = "README.md"
packages = [{ include = "holt59", from = "src" }]
[tool.poetry.dependencies]
python = "^3.10"
numpy = "^2.1.3"
tqdm = "^4.67.1"
parse = "^1.20.2"
scipy = "^1.14.1"
sympy = "^1.13.3"
networkx = "^3.4.2"
pandas = "^2.2.3"
[tool.poetry.group.dev.dependencies]
pyright = "^1.1.389"
ruff = "^0.8.1"
poethepoet = "^0.31.1"
ipykernel = "^6.29.5"
networkx-stubs = "^0.0.1"
[tool.poetry.scripts]
holt59-aoc = "holt59.aoc.__main__:main"
[tool.poe.tasks]
format-imports = "ruff check --select I src --fix"
format-ruff = "ruff format src"
format.sequence = ["format-imports", "format-ruff"]
lint-ruff = "ruff check src"
lint-ruff-format = "ruff format --check src"
lint-pyright = "pyright src"
lint.sequence = ["lint-ruff", "lint-ruff-format", "lint-pyright"]
lint.ignore_fail = "return_non_zero"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

12
run.ps1
View File

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,177 @@
from __future__ import annotations
import heapq
import sys
from typing import Literal, TypeAlias, cast
PlayerType: TypeAlias = Literal["player", "boss"]
SpellType: TypeAlias = Literal["magic missile", "drain", "shield", "poison", "recharge"]
BuffType: TypeAlias = Literal["shield", "poison", "recharge"]
Node: TypeAlias = tuple[
PlayerType,
int,
int,
int,
int,
int,
tuple[tuple[BuffType, int], ...],
tuple[tuple[SpellType, int], ...],
]
ATTACK_SPELLS: list[tuple[SpellType, int, int, int]] = [
("magic missile", 53, 4, 0),
("drain", 73, 2, 2),
]
BUFF_SPELLS: list[tuple[BuffType, int, int]] = [
("shield", 113, 6),
("poison", 173, 6),
("recharge", 229, 5),
]
def play(
player_hp: int,
player_mana: int,
player_armor: int,
boss_hp: int,
boss_attack: int,
hard_mode: bool,
) -> tuple[tuple[SpellType, int], ...]:
winning_node: tuple[tuple[SpellType, int], ...] | None = None
visited: set[
tuple[PlayerType, int, int, int, int, tuple[tuple[BuffType, int], ...]]
] = set()
nodes: list[Node] = [
("player", 0, player_hp, player_mana, player_armor, boss_hp, (), ())
]
while winning_node is None:
(
player,
mana,
player_hp,
player_mana,
player_armor,
boss_hp,
buffs,
spells,
) = heapq.heappop(nodes)
if (player, player_hp, player_mana, player_armor, boss_hp, buffs) in visited:
continue
visited.add((player, player_hp, player_mana, player_armor, boss_hp, buffs))
if hard_mode and player == "player":
player_hp = max(0, player_hp - 1)
if player_hp == 0:
continue
if boss_hp == 0:
winning_node = spells
continue
new_buffs: list[tuple[BuffType, int]] = []
for buff, length in buffs:
length = length - 1
match buff:
case "poison":
boss_hp = max(boss_hp - 3, 0)
case "shield":
if length == 0:
player_armor -= 7
case "recharge":
player_mana += 101
if length > 0:
new_buffs.append((buff, length))
buffs = tuple(new_buffs)
if player == "boss":
heapq.heappush(
nodes,
(
"player",
mana,
max(0, player_hp - max(boss_attack - player_armor, 1)),
player_mana,
player_armor,
boss_hp,
buffs,
spells,
),
)
else:
buff_types = {b for b, _ in buffs}
for spell, cost, damage, regeneration in ATTACK_SPELLS:
if player_mana < cost:
continue
heapq.heappush(
nodes,
(
"boss",
mana + cost,
player_hp + regeneration,
player_mana - cost,
player_armor,
max(0, boss_hp - damage),
buffs,
spells + cast("tuple[tuple[SpellType, int]]", ((spell, cost),)),
),
)
for buff_type, buff_cost, buff_length in BUFF_SPELLS:
if buff_type in buff_types:
continue
if player_mana < buff_cost:
continue
heapq.heappush(
nodes,
(
"boss",
mana + buff_cost,
player_hp,
player_mana - buff_cost,
player_armor + 7 * (buff_type == "shield"),
boss_hp,
buffs
+ cast(
"tuple[tuple[BuffType, int]]", ((buff_type, buff_length),)
),
spells
+ cast(
"tuple[tuple[SpellType, int]]", ((buff_type, buff_cost),)
),
),
)
return winning_node
lines = sys.stdin.read().splitlines()
player_hp = 50
player_mana = 500
player_armor = 0
boss_hp = int(lines[0].split(":")[1].strip())
boss_attack = int(lines[1].split(":")[1].strip())
answer_1 = sum(
c
for _, c in play(player_hp, player_mana, player_armor, boss_hp, boss_attack, False)
)
print(f"answer 1 is {answer_1}")
# 1242 (not working)
answer_2 = sum(
c for _, c in play(player_hp, player_mana, player_armor, boss_hp, boss_attack, True)
)
print(f"answer 2 is {answer_2}")

View File

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

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

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

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

101
src/holt59/aoc/2015/day7.py Normal file
View File

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

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

@@ -0,0 +1,27 @@
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,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,12 +1,13 @@
import sys
from math import prod
from typing import Literal, cast
from typing import Literal, TypeAlias, cast
lines = sys.stdin.read().splitlines()
commands = [
(cast(Literal["forward", "up", "down"], (p := line.split())[0]), int(p[1]))
for line in lines
Command: TypeAlias = Literal["forward", "up", "down"]
commands: list[tuple[Command, int]] = [
(cast(Command, (p := line.split())[0]), int(p[1])) for line in lines
]

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

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

View File

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

@@ -122,8 +122,8 @@ lines = sys.stdin.read().splitlines()
grid = [[ord(cell) - ord("a") for cell in line] for line in lines]
start: tuple[int, int]
end: tuple[int, int]
start: tuple[int, int] | None = None
end: tuple[int, int] | None = None
# for part 2
start_s: list[tuple[int, int]] = []
@@ -138,6 +138,9 @@ for i_row, row in enumerate(grid):
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")

View File

@@ -1,20 +1,22 @@
import sys
from typing import Any
import numpy as np
import parse
import parse # type: ignore
from numpy.typing import NDArray
def part1(sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], row: int) -> int:
no_beacons_row_l: list[np.ndarray] = []
no_beacons_row_l: list[NDArray[np.floating[Any]]] = []
for (sx, sy), (bx, by) in sensor_to_beacon.items():
d = abs(sx - bx) + abs(sy - by) # closest
no_beacons_row_l.append(sx - np.arange(0, d - abs(sy - row) + 1))
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)) # type: ignore
no_beacons_row_l.append(sx + np.arange(0, d - abs(sy - row) + 1)) # type: ignore
beacons_at_row = set(bx for (bx, by) in sensor_to_beacon.values() if by == row)
no_beacons_row = set(np.concatenate(no_beacons_row_l)).difference(beacons_at_row)
no_beacons_row = set(np.concatenate(no_beacons_row_l)).difference(beacons_at_row) # type: ignore
return len(no_beacons_row)
@@ -56,11 +58,12 @@ def part2_cplex(
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.add_constraint(m.abs(x - sx) + m.abs(y - sy) >= d + 1, ctname=f"ct_{sx}_{sy}") # type: ignore
m.set_objective("min", x + y)
s = m.solve()
assert s is not None
vx = int(s.get_value(x))
vy = int(s.get_value(y))
@@ -72,7 +75,7 @@ lines = sys.stdin.read().splitlines()
sensor_to_beacon: dict[tuple[int, int], tuple[int, int]] = {}
for line in lines:
r = parse.parse(
r: dict[str, str] = parse.parse( # type: ignore
"Sensor at x={sx}, y={sy}: closest beacon is at x={bx}, y={by}", line
)
sensor_to_beacon[int(r["sx"]), int(r["sy"])] = (int(r["bx"]), int(r["by"]))

View File

@@ -1,5 +1,4 @@
import sys
from typing import FrozenSet
import numpy as np

View File

@@ -1,9 +1,9 @@
import sys
from typing import Literal
from typing import Any, Literal
import numpy as np
import parse
from tqdm import tqdm
import parse # pyright: ignore[reportMissingTypeStubs]
from numpy.typing import NDArray
Reagent = Literal["ore", "clay", "obsidian", "geode"]
REAGENTS: tuple[Reagent, ...] = (
@@ -35,7 +35,7 @@ class State:
self.robots = robots
self.reagents = reagents
def __eq__(self, other) -> bool:
def __eq__(self, other: object) -> bool:
return (
isinstance(other, State)
and self.robots == other.robots
@@ -66,7 +66,7 @@ lines = sys.stdin.read().splitlines()
blueprints: list[dict[Reagent, IntOfReagent]] = []
for line in lines:
r = parse.parse(
r: list[int] = parse.parse( # type: ignore
"Blueprint {}: "
"Each ore robot costs {:d} ore. "
"Each clay robot costs {:d} ore. "
@@ -94,11 +94,12 @@ def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int:
name: max(blueprint[r].get(name, 0) for r in REAGENTS) for name in REAGENTS
}
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 = [
@@ -132,7 +133,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 = {
reagents: IntOfReagent = {
reagent: state.reagents[reagent]
+ state.robots[reagent]
- blueprint[robot].get(reagent, 0)
@@ -151,7 +152,7 @@ def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int:
]
)
to_keep = []
to_keep: list[NDArray[np.integer[Any]]] = []
while len(np_states) > 0:
first_dom = (np_states[1:] >= np_states[0]).all(axis=1).any()

View File

@@ -56,7 +56,7 @@ def propagate(
[() for _ in range(len(layout[0]))] for _ in range(len(layout))
]
queue = [(start, direction)]
queue: list[tuple[tuple[int, int], Direction]] = [(start, direction)]
while queue:
(row, col), direction = queue.pop()

View File

@@ -28,12 +28,12 @@ def accept(workflows: dict[str, Workflow], part: Part) -> bool:
while decision is None:
for check, target in workflows[workflow]:
ok = check is None
passed = check is None
if check is not None:
category, sense, value = check
ok = OPERATORS[sense](part[category], value)
passed = OPERATORS[sense](part[category], value)
if ok:
if passed:
if target in workflows:
workflow = target
else:
@@ -44,56 +44,63 @@ def accept(workflows: dict[str, Workflow], part: Part) -> bool:
def propagate(workflows: dict[str, Workflow], start: PartWithBounds) -> int:
def _fmt(meta: PartWithBounds) -> str:
return "{" + ", ".join(f"{k}={v}" for k, v in meta.items()) + "}"
def transfer_or_accept(
target: str, meta: PartWithBounds, queue: list[tuple[PartWithBounds, str]]
) -> int:
count = 0
if target in workflows:
logging.info(f" transfer to {target}")
logging.info(f" transfer to {target}")
queue.append((meta, target))
return 0
elif target == "A":
logging.info(" accepted")
return prod((high - low + 1) for low, high in meta.values())
count = prod((high - low + 1) for low, high in meta.values())
logging.info(f" accepted ({count})")
else:
logging.info(" rejected")
return 0
logging.info(" rejected")
return count
accepted = 0
queue: list[tuple[PartWithBounds, str]] = [(start, "in")]
n_iterations = 0
while queue:
n_iterations += 1
meta, workflow = queue.pop()
logging.info(f"{workflow}: {meta}")
logging.info(f"{workflow}: {_fmt(meta)}")
for check, target in workflows[workflow]:
if check is None:
logging.info(" end-of-workflow")
accepted += transfer_or_accept(target, meta, queue)
continue
category, sense, value = check
bounds, op = meta[category], OPERATORS[sense]
logging.info(f" splitting {meta} into {category} {sense} {value}")
logging.info(f" checking {_fmt(meta)} against {category} {sense} {value}")
if not op(bounds[0], value) and not op(bounds[1], value):
logging.info(" reject, always false")
logging.info(" reject, always false")
continue
if op(meta[category][0], value) and op(meta[category][1], value):
logging.info(" accept, always true")
logging.info(" accept, always true")
accepted += transfer_or_accept(target, meta, queue)
break
meta2 = meta.copy()
low, high = meta[category]
if sense == "<":
meta2[category] = (meta[category][0], value - 1)
meta[category] = (value, meta[category][1])
meta[category], meta2[category] = (value, high), (low, value - 1)
else:
meta2[category] = (value + 1, meta[category][1])
meta[category] = (meta[category][0], value)
logging.info(f" split {meta2} ({target}), {meta}")
meta[category], meta2[category] = (low, value), (value + 1, high)
logging.info(f" split {_fmt(meta2)} ({target}), {_fmt(meta)}")
accepted += transfer_or_accept(target, meta2, queue)
logging.info(f"run took {n_iterations} iterations")
return accepted
@@ -107,11 +114,12 @@ for workflow_s in workflows_s.split("\n"):
for block in block_s[:-1].split(","):
check: Check
if (i := block.find(":")) >= 0:
check, target = (
check = (
cast(Category, block[0]),
cast(Literal["<", ">"], block[1]),
int(block[2:i]),
), block[i + 1 :]
)
target = block[i + 1 :]
else:
check, target = None, block
workflows[name].append((check, target))

View File

@@ -0,0 +1,161 @@
import logging
import os
import sys
from collections import defaultdict
from math import lcm
from typing import Literal, TypeAlias
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
ModuleType: TypeAlias = Literal["broadcaster", "conjunction", "flip-flop"]
PulseType: TypeAlias = Literal["high", "low"]
modules: dict[str, tuple[ModuleType, list[str]]] = {}
lines = sys.stdin.read().splitlines()
for line in lines:
name, outputs_s = line.split(" -> ")
outputs = outputs_s.split(", ")
if name == "broadcaster":
modules["broadcaster"] = ("broadcaster", outputs)
else:
modules[name[1:]] = (
"conjunction" if name.startswith("&") else "flip-flop",
outputs,
)
def process(
start: tuple[str, str, PulseType],
flip_flop_states: dict[str, Literal["on", "off"]],
conjunction_states: dict[str, dict[str, PulseType]],
) -> tuple[dict[PulseType, int], dict[str, dict[PulseType, int]]]:
pulses: list[tuple[str, str, PulseType]] = [start]
counts: dict[PulseType, int] = {"low": 0, "high": 0}
inputs: dict[str, dict[PulseType, int]] = defaultdict(lambda: {"low": 0, "high": 0})
logging.info("starting process... ")
while pulses:
input, name, pulse = pulses.pop(0)
logging.info(f"{input} -{pulse}-> {name}")
counts[pulse] += 1
inputs[name][pulse] += 1
if name not in modules:
continue
type, outputs = modules[name]
if type == "broadcaster":
...
elif type == "flip-flop":
if pulse == "high":
continue
if flip_flop_states[name] == "off":
flip_flop_states[name] = "on"
pulse = "high"
else:
flip_flop_states[name] = "off"
pulse = "low"
else:
conjunction_states[name][input] = pulse
if all(state == "high" for state in conjunction_states[name].values()):
pulse = "low"
else:
pulse = "high"
pulses.extend((name, output, pulse) for output in outputs)
return counts, inputs
with open("./day20.dot", "w") as fp:
fp.write("digraph G {\n")
fp.write("rx [shape=circle, color=red, style=filled];\n")
for name, (type, outputs) in modules.items():
if type == "conjunction":
shape = "diamond"
elif type == "flip-flop":
shape = "box"
else:
shape = "circle"
fp.write(f"{name} [shape={shape}];\n")
for name, (type, outputs) in modules.items():
for output in outputs:
fp.write(f"{name} -> {output};\n")
fp.write("}\n")
# part 1
flip_flop_states: dict[str, Literal["on", "off"]] = {
name: "off" for name, (type, _) in modules.items() if type == "flip-flop"
}
conjunction_states: dict[str, dict[str, PulseType]] = {
name: {input: "low" for input, (_, outputs) in modules.items() if name in outputs}
for name, (type, _) in modules.items()
if type == "conjunction"
}
counts: dict[PulseType, int] = {"low": 0, "high": 0}
for _ in range(1000):
result, _ = process(
("button", "broadcaster", "low"), flip_flop_states, conjunction_states
)
for pulse in ("low", "high"):
counts[pulse] += result[pulse]
answer_1 = counts["low"] * counts["high"]
print(f"answer 1 is {answer_1}")
# part 2
# reset states
for name in flip_flop_states:
flip_flop_states[name] = "off"
for name in conjunction_states:
for input in conjunction_states[name]:
conjunction_states[name][input] = "low"
# find the conjunction connected to rx
to_rx = [name for name, (_, outputs) in modules.items() if "rx" in outputs]
assert len(to_rx) == 1, "cannot handle multiple module inputs for rx"
assert (
modules[to_rx[0]][0] == "conjunction"
), "can only handle conjunction as input to rx"
to_rx_inputs = [name for name, (_, outputs) in modules.items() if to_rx[0] in outputs]
assert all(
modules[i][0] == "conjunction" and len(modules[i][1]) == 1 for i in to_rx_inputs
), "can only handle inversion as second-order inputs to rx"
count = 1
cycles: dict[str, int] = {}
second: dict[str, int] = {}
while len(second) != len(to_rx_inputs):
_, inputs = process(
("button", "broadcaster", "low"), flip_flop_states, conjunction_states
)
for node in to_rx_inputs:
if inputs[node]["low"] == 1:
if node not in cycles:
cycles[node] = count
elif node not in second:
second[node] = count
count += 1
assert all(
second[k] == cycles[k] * 2 for k in to_rx_inputs
), "cannot only handle cycles starting at the beginning"
answer_2 = lcm(*cycles.values())
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,149 @@
import logging
import os
import sys
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
def reachable(
map: list[str], tiles: set[tuple[int, int]], steps: int
) -> set[tuple[int, int]]:
n_rows, n_cols = len(map), len(map[0])
for _ in range(steps):
tiles = {
(i + di, j + dj)
for i, j in tiles
for di, dj in ((-1, 0), (+1, 0), (0, -1), (0, +1))
if map[(i + di) % n_rows][(j + dj) % n_cols] != "#"
}
return tiles
map = sys.stdin.read().splitlines()
start = next(
(i, j) for i in range(len(map)) for j in range(len(map[i])) if map[i][j] == "S"
)
# part 1
answer_1 = len(reachable(map, {start}, 6 if len(map) < 20 else 64))
print(f"answer 1 is {answer_1}")
# part 2
# the initial map is a square and contains an empty rhombus whose diameter is the size
# of the map, and has only empty cells around the middle row and column
#
# after ~n/2 steps, the first map is filled with a rhombus, after that we get a bigger
# rhombus every n steps
#
# we are going to find the number of cells reached for the initial rhombus, n steps
# after and n * 2 steps after
#
cycle = len(map)
rhombus = (len(map) - 3) // 2 + 1
values: list[int] = []
values.append(len(tiles := reachable(map, {start}, rhombus)))
values.append(len(tiles := reachable(map, tiles, cycle)))
values.append(len(tiles := reachable(map, tiles, cycle)))
if logging.root.getEffectiveLevel() == logging.INFO:
n_rows, n_cols = len(map), len(map[0])
rows = [
[
map[i % n_rows][j % n_cols] if (i, j) not in tiles else "O"
for j in range(-2 * cycle, 3 * cycle)
]
for i in range(-2 * cycle, 3 * cycle)
]
for i in range(len(rows)):
for j in range(len(rows[i])):
if (i // cycle) % 2 == (j // cycle) % 2:
rows[i][j] = f"\033[91m{rows[i][j]}\033[0m"
print("\n".join("".join(row) for row in rows))
logging.info(f"values to fit: {values}")
# version 1:
#
# after 3 cycles, the figure looks like the following:
#
# I M D
# I J A K D
# H A F A L
# C E A K B
# C G B
#
# after 4 cycles, the figure looks like the following:
#
# I M D
# I J A K D
# I J A B A K D
# H A B A B A L
# C E A B A N F
# C E A N F
# C G F
#
# the 'radius' of the rhombus is the number of cycles minus 1
#
# the 4 'corner' (M, H, L, G) are counted once, the blocks with a corner triangle (D, I,
# C, B) are each counted radius times, the blocks with everything but one corner (J, K,
# E, N) are each counted radius - 1 times
#
# there are two versions of the whole block, A and B in the above (or odd and even),
# depending on the number of cycles, either A or B will be in the center
#
counts = [
[
sum(
(i, j) in tiles
for i in range(ci * cycle, (ci + 1) * cycle)
for j in range(cj * cycle, (cj + 1) * cycle)
)
for cj in range(-2, 3)
]
for ci in range(-2, 3)
]
radius = (26501365 - rhombus) // cycle - 1
A = counts[2][2] if radius % 2 == 0 else counts[2][1]
B = counts[2][2] if radius % 2 == 1 else counts[2][1]
answer_2 = (
(radius + 1) * A
+ radius * B
+ 2 * radius * (radius + 1) // 2 * A
+ 2 * radius * (radius - 1) // 2 * B
+ sum(counts[i][j] for i, j in ((0, 2), (-1, 2), (2, 0), (2, -1)))
+ sum(counts[i][j] for i, j in ((0, 1), (0, 3), (-1, 1), (-1, 3))) * (radius + 1)
+ sum(counts[i][j] for i, j in ((1, 1), (1, 3), (-2, 1), (-2, 3))) * radius
)
print(f"answer 2 (v1) is {answer_2}")
# version 2: fitting a polynomial
#
# the value we are interested in (26501365) can be written as R + K * C where R is the
# step at which we find the first rhombus, and K the repeat step, so instead of fitting
# for X values (R, R + K, R + 2 K), we are going to fit for (0, 1, 2), giving us much
# simpler equation for the a, b and c coefficient
#
# we get:
# - (a * 0² + b * 0 + c) = y1 => c = y1
# - (a * 1² + b * 1 + c) = y2 => a + b = y2 - y1
# => b = y2 - y1 - a
# - (a * 2² + b * 2 + c) = y3 => 4a + 2b = y3 - y1
# => 4a + 2(y2 - y1 - a) = y3 - y1
# => a = (y1 + y3) / 2 - y2
#
y1, y2, y3 = values
a, b, c = (y1 + y3) // 2 - y2, 2 * y2 - (3 * y1 + y3) // 2, y1
n = (26501365 - rhombus) // cycle
answer_2 = a * n * n + b * n + c
print(f"answer 2 (v2) is {answer_2}")

View File

@@ -0,0 +1,111 @@
import itertools
import logging
import os
import string
import sys
from collections import defaultdict
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
lines = sys.stdin.read().splitlines()
def _name(i: int) -> str:
if len(lines) < 26:
return string.ascii_uppercase[i]
return f"B{i:04d}"
def build_supports(
bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]],
) -> tuple[dict[int, set[int]], dict[int, set[int]]]:
# 1. compute locations where a brick of sand will land after falling by processing
# them in sorted order of bottom z location
levels: dict[tuple[int, int, int], int] = defaultdict(lambda: -1)
for i_brick, ((sx, sy, sz), (ex, ey, ez)) in enumerate(bricks):
assert sx <= ex and sy <= ey and sz <= ez
xs, ys = range(sx, ex + 1), range(sy, ey + 1)
for z in range(sz - 1, 0, -1):
if any(levels[x, y, z] >= 0 for x, y in itertools.product(xs, ys)):
break
sz, ez = sz - 1, ez - 1
bricks[i_brick] = ((sx, sy, sz), (ex, ey, ez))
zs = range(sz, ez + 1)
for x, y, z in itertools.product(xs, ys, zs):
levels[x, y, z] = i_brick
# 2. compute the bricks that supports any brick
supported_by: dict[int, set[int]] = {}
supports: dict[int, set[int]] = {i_brick: set() for i_brick in range(len(bricks))}
for i_brick, ((sx, sy, sz), (ex, ey, ez)) in enumerate(bricks):
name = _name(i_brick)
supported_by[i_brick] = {
v
for x, y in itertools.product(range(sx, ex + 1), range(sy, ey + 1))
if (v := levels[x, y, sz - 1]) != -1
}
logging.info(
f"{name} supported by {', '.join(map(_name, supported_by[i_brick]))}"
)
for support in supported_by[i_brick]:
supports[support].add(i_brick)
return supported_by, supports
bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]] = []
for line in lines:
bricks.append(
(
tuple(int(c) for c in line.split("~")[0].split(",")), # type: ignore
tuple(int(c) for c in line.split("~")[1].split(",")), # type: ignore
)
)
# sort bricks by bottom z position to compute supports
bricks = sorted(bricks, key=lambda b: b[0][-1])
supported_by, supports = build_supports(bricks)
# part 1
answer_1 = len(bricks) - sum(
any(len(supported_by[supported]) == 1 for supported in supports_to)
for supports_to in supports.values()
)
print(f"answer 1 is {answer_1}")
# part 2
falling_in_chain: dict[int, set[int]] = {}
for i_brick in range(len(bricks)):
to_disintegrate: set[int] = {
supported
for supported in supports[i_brick]
if len(supported_by[supported]) == 1
}
supported_by_copy = dict(supported_by)
falling_in_chain[i_brick] = set()
while to_disintegrate:
falling_in_chain[i_brick].update(to_disintegrate)
to_disintegrate_v: set[int] = set()
for d_brick in to_disintegrate:
for supported in supports[d_brick]:
supported_by_copy[supported] = supported_by_copy[supported] - {d_brick}
if not supported_by_copy[supported]:
to_disintegrate_v.add(supported)
to_disintegrate = to_disintegrate_v
answer_2 = sum(len(falling) for falling in falling_in_chain.values())
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,167 @@
import logging
import os
import sys
from collections import defaultdict
from typing import Literal, Sequence, TypeAlias, cast
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
DirectionType: TypeAlias = Literal[">", "<", "^", "v", ".", "#"]
Direction: dict[DirectionType, tuple[int, int]] = {
">": (0, +1),
"<": (0, -1),
"^": (-1, 0),
"v": (+1, 0),
}
Neighbors = cast(
"dict[DirectionType, tuple[tuple[int, int], ...]]",
{
".": ((+1, 0), (-1, 0), (0, +1), (0, -1)),
"#": (),
}
| {k: (v,) for k, v in Direction.items()},
)
def neighbors(
grid: list[Sequence[DirectionType]],
node: tuple[int, int],
ignore: set[tuple[int, int]] = set(),
):
"""
Compute neighbors of the given node, ignoring the given set of nodes and considering
that you can go uphill on slopes.
"""
i, j = node
for di, dj in Neighbors[grid[i][j]]:
ti, tj = di + i, dj + j
if ti < 0 or ti >= n_rows or tj < 0 or tj >= n_cols:
continue
if (ti, tj) in ignore:
continue
v = grid[ti][tj]
if (
v == "#"
or (v == "v" and di == -1)
or (v == "^" and di == 1)
or (v == ">" and dj == -1)
or (v == "<" and dj == 1)
):
continue
yield ti, tj
def reachable(
grid: list[Sequence[DirectionType]], start: tuple[int, int], target: tuple[int, int]
) -> tuple[tuple[int, int], int]:
"""
Compute the next 'reachable' node in the grid, starting at the given node.
The next 'reachable' node is the first node after a slope in the path starting from
the given node (not going uphill).
"""
distance, path = 0, {start}
while True:
i, j = start
if (i, j) == target:
return (target, distance)
if grid[i][j] != ".":
di, dj = Direction[grid[i][j]]
return ((i + di, j + dj), distance + 1)
start = next(neighbors(grid, start, path := path | {(i, j)}))
distance += 1
def compute_direct_links(
grid: list[Sequence[DirectionType]], start: tuple[int, int], target: tuple[int, int]
) -> dict[tuple[int, int], list[tuple[tuple[int, int], int]]]:
if start == target:
return {}
direct: dict[tuple[int, int], list[tuple[tuple[int, int], int]]] = {start: []}
for neighbor in neighbors(grid, start):
i, j = neighbor
di, dj = Direction[grid[i][j]]
reach, distance = reachable(grid, (i + di, j + dj), target)
direct[start].append((reach, distance + 2))
direct.update(compute_direct_links(grid, reach, target))
return direct
def longest_path_length(
links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]],
start: tuple[int, int],
target: tuple[int, int],
) -> int:
max_distance: int = -1
queue: list[tuple[tuple[int, int], int, frozenset[tuple[int, int]]]] = [
(start, 0, frozenset({start}))
]
nodes = 0
while queue:
node, distance, path = queue.pop()
nodes += 1
if node == target:
max_distance = max(distance, max_distance)
continue
queue.extend(
(reach, distance + length, path | {reach})
for reach, length in links.get(node, [])
if reach not in path
)
logging.info(f"processed {nodes} nodes")
return max_distance
lines = cast(list[Sequence[DirectionType]], sys.stdin.read().splitlines())
n_rows, n_cols = len(lines), len(lines[0])
start = (0, 1)
target = (len(lines) - 1, len(lines[0]) - 2)
direct_links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]] = {
start: [reachable(lines, start, target)]
}
direct_links.update(compute_direct_links(lines, direct_links[start][0][0], target))
# part 1
answer_1 = longest_path_length(direct_links, start, target)
print(f"answer 1 is {answer_1}")
# part 2
reverse_links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]] = defaultdict(
list
)
for origin, links in direct_links.items():
for destination, distance in links:
if origin != start:
reverse_links[destination].append((origin, distance))
links = {
k: direct_links.get(k, []) + reverse_links.get(k, [])
for k in direct_links.keys() | reverse_links.keys()
}
answer_2 = longest_path_length(links, start, target)
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,63 @@
import sys
import numpy as np
from sympy import solve, symbols
lines = sys.stdin.read().splitlines()
positions = np.array(
[[int(c) for c in line.split("@")[0].strip().split(", ")] for line in lines]
)
velocities = np.array(
[[int(c) for c in line.split("@")[1].strip().split(", ")] for line in lines]
)
# part 1
low, high = [7, 27] if len(positions) <= 10 else [200000000000000, 400000000000000]
count = 0
for i1, (p1, v1) in enumerate(zip(positions, velocities)):
p, r = p1[:2], v1[:2]
q, s = positions[i1 + 1 :, :2], velocities[i1 + 1 :, :2]
rs = np.cross(r, s)
q, s, rs = q[m := (rs != 0)], s[m], rs[m]
t = np.cross((q - p), s) / rs
u = np.cross((q - p), r) / rs
t, u = t[m := ((t >= 0) & (u >= 0))], u[m]
c = p + np.expand_dims(t, 1) * r
count += np.all((low <= c) & (c <= high), axis=1).sum()
answer_1 = count
print(f"answer 1 is {answer_1}")
# part 2
# equation
# p1 + t1 * v1 == p0 + t1 * v0
# p2 + t2 * v2 == p0 + t2 * v0
# p3 + t3 * v3 == p0 + t3 * v0
# ...
# pn + tn * vn == p0 + tn * v0
#
# we can solve with only 3 lines since each lines contains 3
# equations (x / y / z), so 3 lines give 9 equations and 9
# variables: position (3), velocities (3) and times (3).
n = 3
x, y, z, vx, vy, vz, *ts = symbols(
"x y z vx vy vz " + " ".join(f"t{i}" for i in range(n + 1))
)
equations = []
for i1, ti in zip(range(n), ts):
for p, d, pi, di in zip((x, y, z), (vx, vy, vz), positions[i1], velocities[i1]):
equations.append(p + ti * d - pi - ti * di)
r = solve(equations, [x, y, z, vx, vy, vz] + list(ts), dict=True)[0]
answer_2 = r[x] + r[y] + r[z]
print(f"answer 2 is {answer_2}")

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