Refactor 2023 for new system.
This commit is contained in:
parent
a9bcf9ef8f
commit
664dcfe7ba
16
poetry.lock
generated
16
poetry.lock
generated
@ -1245,6 +1245,20 @@ files = [
|
|||||||
docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
|
docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
|
||||||
test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"]
|
test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-networkx"
|
||||||
|
version = "3.4.2.20241115"
|
||||||
|
description = "Typing stubs for networkx"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "types-networkx-3.4.2.20241115.tar.gz", hash = "sha256:d669b650cf6c6c9ec879a825449eb04a5c10742f3109177e1683f57ee49e0f59"},
|
||||||
|
{file = "types_networkx-3.4.2.20241115-py3-none-any.whl", hash = "sha256:f0c382924d6614e06bf0b1ca0b837b8f33faa58982bc086ea762efaf39aa98dd"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
numpy = ">=1.20"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.12.2"
|
version = "4.12.2"
|
||||||
@ -1281,4 +1295,4 @@ files = [
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "b643261f91a781d77735e05f6d2ac1002867600c2df6393a9d1a15f5e1189109"
|
content-hash = "c91bc307ff4a5b3e8cd1976ebea211c9749fe09d563dd80861f70ce26826cda9"
|
||||||
|
@ -23,6 +23,7 @@ ruff = "^0.8.1"
|
|||||||
poethepoet = "^0.31.1"
|
poethepoet = "^0.31.1"
|
||||||
ipykernel = "^6.29.5"
|
ipykernel = "^6.29.5"
|
||||||
networkx-stubs = "^0.0.1"
|
networkx-stubs = "^0.0.1"
|
||||||
|
types-networkx = "^3.4.2.20241115"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
holt59-aoc = "holt59.aoc.__main__:main"
|
holt59-aoc = "holt59.aoc.__main__:main"
|
||||||
|
@ -1,100 +1,100 @@
|
|||||||
import os
|
from typing import Any, Iterator, Literal, cast
|
||||||
import sys
|
|
||||||
from typing import Literal, cast
|
|
||||||
|
|
||||||
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
|
from ..base import BaseSolver
|
||||||
|
|
||||||
Symbol = Literal["|", "-", "L", "J", "7", "F", ".", "S"]
|
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
|
class Solver(BaseSolver):
|
||||||
si, sj = next(
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
(i, j)
|
lines: list[list[Symbol]] = [
|
||||||
for i in range(len(lines))
|
[cast(Symbol, symbol) for symbol in line] for line in input.splitlines()
|
||||||
for j in range(len(lines[0]))
|
]
|
||||||
if lines[i][j] == "S"
|
|
||||||
)
|
|
||||||
|
|
||||||
# find one of the two outputs
|
# find starting point
|
||||||
ni, nj = si, sj
|
si, sj = next(
|
||||||
for ni, nj, chars in (
|
(i, j)
|
||||||
(si - 1, sj, "|7F"),
|
for i in range(len(lines))
|
||||||
(si + 1, sj, "|LJ"),
|
for j in range(len(lines[0]))
|
||||||
(si, sj - 1, "-LF"),
|
if lines[i][j] == "S"
|
||||||
(si, sj + 1, "-J7"),
|
)
|
||||||
):
|
|
||||||
if lines[ni][nj] in chars:
|
|
||||||
break
|
|
||||||
|
|
||||||
# part 1 - find the loop (re-used in part 2)
|
# find one of the two outputs
|
||||||
loop = [(si, sj), (ni, nj)]
|
ni, nj = si, sj
|
||||||
while True:
|
for ni, nj, chars in (
|
||||||
pi, pj = loop[-2]
|
(si - 1, sj, "|7F"),
|
||||||
i, j = loop[-1]
|
(si + 1, sj, "|LJ"),
|
||||||
|
(si, sj - 1, "-LF"),
|
||||||
|
(si, sj + 1, "-J7"),
|
||||||
|
):
|
||||||
|
if lines[ni][nj] in chars:
|
||||||
|
break
|
||||||
|
|
||||||
sym = lines[i][j]
|
# 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]
|
||||||
|
|
||||||
if sym == "|" and pi > i or sym in "JL" and pi == i:
|
sym = lines[i][j]
|
||||||
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):
|
if sym == "|" and pi > i or sym in "JL" and pi == i:
|
||||||
break
|
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
|
||||||
|
|
||||||
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):
|
if (i, j) == (si, sj):
|
||||||
print("\033[91mS\033[0m", end="")
|
break
|
||||||
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)
|
loop.append((i, j))
|
||||||
print(f"answer 2 is {answer_2}")
|
|
||||||
|
yield len(loop) // 2
|
||||||
|
|
||||||
|
# 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 self.verbose:
|
||||||
|
for i in range(len(lines)):
|
||||||
|
s = ""
|
||||||
|
for j in range(len(lines[0])):
|
||||||
|
if (i, j) == (si, sj):
|
||||||
|
s += "\033[91mS\033[0m"
|
||||||
|
elif (i, j) in loop:
|
||||||
|
s += lines[i][j]
|
||||||
|
elif (i, j) in inside:
|
||||||
|
s += "\033[92mI\033[0m"
|
||||||
|
else:
|
||||||
|
s += "."
|
||||||
|
self.logger.info(s)
|
||||||
|
|
||||||
|
yield len(inside)
|
||||||
|
@ -1,41 +1,42 @@
|
|||||||
import sys
|
from typing import Any, Iterator
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
lines = sys.stdin.read().splitlines()
|
from ..base import BaseSolver
|
||||||
|
|
||||||
data = np.array([[c == "#" for c in line] for line in lines])
|
|
||||||
|
|
||||||
rows = {c for c in range(data.shape[0]) if not data[c, :].any()}
|
|
||||||
columns = {c for c in range(data.shape[1]) if not data[:, c].any()}
|
|
||||||
|
|
||||||
galaxies_y, galaxies_x = np.where(data) # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def compute_total_distance(expansion: int) -> int:
|
class Solver(BaseSolver):
|
||||||
distances: list[int] = []
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
for g1 in range(len(galaxies_y)):
|
lines = input.splitlines()
|
||||||
x1, y1 = int(galaxies_x[g1]), int(galaxies_y[g1])
|
|
||||||
for g2 in range(g1 + 1, len(galaxies_y)):
|
|
||||||
x2, y2 = int(galaxies_x[g2]), int(galaxies_y[g2])
|
|
||||||
|
|
||||||
dx = sum(
|
data = np.array([[c == "#" for c in line] for line in lines])
|
||||||
1 + (expansion - 1) * (x in columns)
|
|
||||||
for x in range(min(x1, x2), max(x1, x2))
|
|
||||||
)
|
|
||||||
dy = sum(
|
|
||||||
1 + (expansion - 1) * (y in rows)
|
|
||||||
for y in range(min(y1, y2), max(y1, y2))
|
|
||||||
)
|
|
||||||
|
|
||||||
distances.append(dx + dy)
|
rows = {c for c in range(data.shape[0]) if not data[c, :].any()}
|
||||||
return sum(distances)
|
columns = {c for c in range(data.shape[1]) if not data[:, c].any()}
|
||||||
|
|
||||||
|
galaxies_y, galaxies_x = np.where(data) # type: ignore
|
||||||
|
|
||||||
# part 1
|
def compute_total_distance(expansion: int) -> int:
|
||||||
answer_1 = compute_total_distance(2)
|
distances: list[int] = []
|
||||||
print(f"answer 1 is {answer_1}")
|
for g1 in range(len(galaxies_y)):
|
||||||
|
x1, y1 = int(galaxies_x[g1]), int(galaxies_y[g1])
|
||||||
|
for g2 in range(g1 + 1, len(galaxies_y)):
|
||||||
|
x2, y2 = int(galaxies_x[g2]), int(galaxies_y[g2])
|
||||||
|
|
||||||
# part 2
|
dx = sum(
|
||||||
answer_2 = compute_total_distance(1000000)
|
1 + (expansion - 1) * (x in columns)
|
||||||
print(f"answer 2 is {answer_2}")
|
for x in range(min(x1, x2), max(x1, x2))
|
||||||
|
)
|
||||||
|
dy = sum(
|
||||||
|
1 + (expansion - 1) * (y in rows)
|
||||||
|
for y in range(min(y1, y2), max(y1, y2))
|
||||||
|
)
|
||||||
|
|
||||||
|
distances.append(dx + dy)
|
||||||
|
return sum(distances)
|
||||||
|
|
||||||
|
# part 1
|
||||||
|
yield compute_total_distance(2)
|
||||||
|
|
||||||
|
# part 2
|
||||||
|
yield compute_total_distance(1000000)
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from typing import Iterable
|
from typing import Any, Iterable, Iterator
|
||||||
|
|
||||||
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
@lru_cache
|
||||||
@ -77,31 +75,29 @@ def compute_possible_arrangements(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def compute_all_possible_arrangements(lines: Iterable[str], repeat: int) -> int:
|
class Solver(BaseSolver):
|
||||||
count = 0
|
def compute_all_possible_arrangements(
|
||||||
|
self, lines: Iterable[str], repeat: int
|
||||||
|
) -> int:
|
||||||
|
count = 0
|
||||||
|
|
||||||
if VERBOSE:
|
for i_line, line in enumerate(lines):
|
||||||
from tqdm import tqdm
|
self.logger.info(f"processing line {i_line}: {line}...")
|
||||||
|
parts = line.split(" ")
|
||||||
|
count += compute_possible_arrangements(
|
||||||
|
tuple(
|
||||||
|
filter(len, "?".join(parts[0] for _ in range(repeat)).split("."))
|
||||||
|
),
|
||||||
|
tuple(int(c) for c in parts[1].split(",")) * repeat,
|
||||||
|
)
|
||||||
|
|
||||||
lines = tqdm(lines)
|
return count
|
||||||
|
|
||||||
for line in lines:
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
parts = line.split(" ")
|
lines = input.splitlines()
|
||||||
count += compute_possible_arrangements(
|
|
||||||
tuple(filter(len, "?".join(parts[0] for _ in range(repeat)).split("."))),
|
|
||||||
tuple(int(c) for c in parts[1].split(",")) * repeat,
|
|
||||||
)
|
|
||||||
|
|
||||||
return count
|
# part 1
|
||||||
|
yield self.compute_all_possible_arrangements(lines, 1)
|
||||||
|
|
||||||
|
# part 2
|
||||||
lines = sys.stdin.read().splitlines()
|
yield self.compute_all_possible_arrangements(lines, 5)
|
||||||
|
|
||||||
|
|
||||||
# part 1
|
|
||||||
answer_1 = compute_all_possible_arrangements(lines, 1)
|
|
||||||
print(f"answer 1 is {answer_1}")
|
|
||||||
|
|
||||||
# part 2
|
|
||||||
answer_2 = compute_all_possible_arrangements(lines, 5)
|
|
||||||
print(f"answer 2 is {answer_2}")
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import sys
|
from typing import Any, Callable, Iterator, Literal
|
||||||
from typing import Callable, Literal
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
def split(block: list[str], axis: Literal[0, 1], count: int) -> int:
|
def split(block: list[str], axis: Literal[0, 1], count: int) -> int:
|
||||||
@ -25,19 +26,18 @@ def split(block: list[str], axis: Literal[0, 1], count: int) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
blocks = [block.splitlines() for block in sys.stdin.read().split("\n\n")]
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
blocks = [block.splitlines() for block in input.split("\n\n")]
|
||||||
|
|
||||||
|
# part 1
|
||||||
|
yield sum(
|
||||||
|
split(block, axis=1, count=0) + 100 * split(block, axis=0, count=0)
|
||||||
|
for block in blocks
|
||||||
|
)
|
||||||
|
|
||||||
# part 1
|
# part 2
|
||||||
answer_1 = sum(
|
yield sum(
|
||||||
split(block, axis=1, count=0) + 100 * split(block, axis=0, count=0)
|
split(block, axis=1, count=1) + 100 * split(block, axis=0, count=1)
|
||||||
for block in blocks
|
for block in blocks
|
||||||
)
|
)
|
||||||
print(f"answer 1 is {answer_1}")
|
|
||||||
|
|
||||||
# part 2
|
|
||||||
answer_2 = sum(
|
|
||||||
split(block, axis=1, count=1) + 100 * split(block, axis=0, count=1)
|
|
||||||
for block in blocks
|
|
||||||
)
|
|
||||||
print(f"answer 2 is {answer_2}")
|
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import sys
|
from typing import Any, Iterator, TypeAlias
|
||||||
from typing import TypeAlias
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
RockGrid: TypeAlias = list[list[str]]
|
RockGrid: TypeAlias = list[list[str]]
|
||||||
|
|
||||||
rocks0 = [list(line) for line in sys.stdin.read().splitlines()]
|
|
||||||
|
|
||||||
|
|
||||||
def slide_rocks_top(rocks: RockGrid) -> RockGrid:
|
def slide_rocks_top(rocks: RockGrid) -> RockGrid:
|
||||||
top = [0 if c == "." else 1 for c in rocks[0]]
|
top = [0 if c == "." else 1 for c in rocks[0]]
|
||||||
@ -34,35 +33,38 @@ def cycle(rocks: RockGrid) -> RockGrid:
|
|||||||
return rocks
|
return rocks
|
||||||
|
|
||||||
|
|
||||||
rocks = slide_rocks_top([[c for c in r] for r in rocks0])
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
rocks0 = [list(line) for line in input.splitlines()]
|
||||||
|
|
||||||
# part 1
|
rocks = slide_rocks_top([[c for c in r] for r in rocks0])
|
||||||
answer_1 = sum(
|
|
||||||
(len(rocks) - i) * sum(1 for c in row if c == "O") for i, row in enumerate(rocks)
|
|
||||||
)
|
|
||||||
print(f"answer 1 is {answer_1}")
|
|
||||||
|
|
||||||
# part 2
|
# part 1
|
||||||
rocks = rocks0
|
yield sum(
|
||||||
|
(len(rocks) - i) * sum(1 for c in row if c == "O")
|
||||||
|
for i, row in enumerate(rocks)
|
||||||
|
)
|
||||||
|
|
||||||
N = 1000000000
|
# part 2
|
||||||
cycles: list[RockGrid] = []
|
rocks = rocks0
|
||||||
i_cycle: int = -1
|
|
||||||
for i_cycle in range(N):
|
|
||||||
rocks = cycle(rocks)
|
|
||||||
|
|
||||||
if any(rocks == c for c in cycles):
|
N = 1000000000
|
||||||
break
|
cycles: list[RockGrid] = []
|
||||||
|
i_cycle: int = -1
|
||||||
|
for i_cycle in range(N):
|
||||||
|
rocks = cycle(rocks)
|
||||||
|
|
||||||
cycles.append([[c for c in r] for r in rocks])
|
if any(rocks == c for c in cycles):
|
||||||
|
break
|
||||||
|
|
||||||
cycle_start = next(i for i in range(len(cycles)) if (rocks == cycles[i]))
|
cycles.append([[c for c in r] for r in rocks])
|
||||||
cycle_length = i_cycle - cycle_start
|
|
||||||
|
|
||||||
ci = cycle_start + (N - cycle_start) % cycle_length - 1
|
cycle_start = next(i for i in range(len(cycles)) if (rocks == cycles[i]))
|
||||||
|
cycle_length = i_cycle - cycle_start
|
||||||
|
|
||||||
answer_2 = sum(
|
ci = cycle_start + (N - cycle_start) % cycle_length - 1
|
||||||
(len(rocks) - i) * sum(1 for c in row if c == "O")
|
|
||||||
for i, row in enumerate(cycles[ci])
|
yield sum(
|
||||||
)
|
(len(rocks) - i) * sum(1 for c in row if c == "O")
|
||||||
print(f"answer 2 is {answer_2}")
|
for i, row in enumerate(cycles[ci])
|
||||||
|
)
|
||||||
|
@ -1,31 +1,33 @@
|
|||||||
import sys
|
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
steps = sys.stdin.read().strip().split(",")
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
def _hash(s: str) -> int:
|
def _hash(s: str) -> int:
|
||||||
return reduce(lambda v, u: ((v + ord(u)) * 17) % 256, s, 0)
|
return reduce(lambda v, u: ((v + ord(u)) * 17) % 256, s, 0)
|
||||||
|
|
||||||
|
|
||||||
# part 1
|
class Solver(BaseSolver):
|
||||||
answer_1 = sum(map(_hash, steps))
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
print(f"answer 1 is {answer_1}")
|
steps = input.split(",")
|
||||||
|
|
||||||
# part 2
|
# part 1
|
||||||
boxes: list[dict[str, int]] = [{} for _ in range(256)]
|
yield sum(map(_hash, steps))
|
||||||
|
|
||||||
for step in steps:
|
# part 2
|
||||||
if (i := step.find("=")) >= 0:
|
boxes: list[dict[str, int]] = [{} for _ in range(256)]
|
||||||
label, length = step[:i], int(step[i + 1 :])
|
|
||||||
boxes[_hash(label)][label] = length
|
|
||||||
else:
|
|
||||||
label = step[:-1]
|
|
||||||
boxes[_hash(label)].pop(label, None)
|
|
||||||
|
|
||||||
answer_2 = sum(
|
for step in steps:
|
||||||
i_box * i_lens * length
|
if (i := step.find("=")) >= 0:
|
||||||
for i_box, box in enumerate(boxes, start=1)
|
label, length = step[:i], int(step[i + 1 :])
|
||||||
for i_lens, length in enumerate(box.values(), start=1)
|
boxes[_hash(label)][label] = length
|
||||||
)
|
else:
|
||||||
print(f"answer 2 is {answer_2}")
|
label = step[:-1]
|
||||||
|
boxes[_hash(label)].pop(label, None)
|
||||||
|
|
||||||
|
yield sum(
|
||||||
|
i_box * i_lens * length
|
||||||
|
for i_box, box in enumerate(boxes, start=1)
|
||||||
|
for i_lens, length in enumerate(box.values(), start=1)
|
||||||
|
)
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import os
|
from typing import Any, Iterator, Literal, TypeAlias, cast
|
||||||
import sys
|
|
||||||
from typing import Literal, TypeAlias, cast
|
|
||||||
|
|
||||||
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
|
from ..base import BaseSolver
|
||||||
|
|
||||||
CellType: TypeAlias = Literal[".", "|", "-", "\\", "/"]
|
CellType: TypeAlias = Literal[".", "|", "-", "\\", "/"]
|
||||||
Direction: TypeAlias = Literal["R", "L", "U", "D"]
|
Direction: TypeAlias = Literal["R", "L", "U", "D"]
|
||||||
@ -78,33 +76,33 @@ def propagate(
|
|||||||
return beams
|
return beams
|
||||||
|
|
||||||
|
|
||||||
layout: list[list[CellType]] = [
|
class Solver(BaseSolver):
|
||||||
[cast(CellType, col) for col in row] for row in sys.stdin.read().splitlines()
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
]
|
layout: list[list[CellType]] = [
|
||||||
|
[cast(CellType, col) for col in row] for row in input.splitlines()
|
||||||
|
]
|
||||||
|
|
||||||
|
beams = propagate(layout, (0, 0), "R")
|
||||||
|
|
||||||
beams = propagate(layout, (0, 0), "R")
|
if self.verbose:
|
||||||
|
for row in beams:
|
||||||
|
self.logger.info("".join("#" if col else "." for col in row))
|
||||||
|
|
||||||
if VERBOSE:
|
# part 1
|
||||||
print("\n".join(["".join("#" if col else "." for col in row) for row in beams]))
|
yield sum(sum(map(bool, row)) for row in beams)
|
||||||
|
|
||||||
# part 1
|
# part 2
|
||||||
answer_1 = sum(sum(map(bool, row)) for row in beams)
|
n_rows, n_cols = len(layout), len(layout[0])
|
||||||
print(f"answer 1 is {answer_1}")
|
cases: list[tuple[tuple[int, int], Direction]] = []
|
||||||
|
|
||||||
# part 2
|
for row in range(n_rows):
|
||||||
n_rows, n_cols = len(layout), len(layout[0])
|
cases.append(((row, 0), "R"))
|
||||||
cases: list[tuple[tuple[int, int], Direction]] = []
|
cases.append(((row, n_cols - 1), "L"))
|
||||||
|
for col in range(n_cols):
|
||||||
|
cases.append(((0, col), "D"))
|
||||||
|
cases.append(((n_rows - 1, col), "U"))
|
||||||
|
|
||||||
for row in range(n_rows):
|
yield max(
|
||||||
cases.append(((row, 0), "R"))
|
sum(sum(map(bool, row)) for row in propagate(layout, start, direction))
|
||||||
cases.append(((row, n_cols - 1), "L"))
|
for start, direction in cases
|
||||||
for col in range(n_cols):
|
)
|
||||||
cases.append(((0, col), "D"))
|
|
||||||
cases.append(((n_rows - 1, col), "U"))
|
|
||||||
|
|
||||||
answer_2 = max(
|
|
||||||
sum(sum(map(bool, row)) for row in propagate(layout, start, direction))
|
|
||||||
for start, direction in cases
|
|
||||||
)
|
|
||||||
print(f"answer 2 is {answer_2}")
|
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import heapq
|
import heapq
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Literal, TypeAlias
|
from typing import Any, Iterator, Literal, TypeAlias
|
||||||
|
|
||||||
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
|
from ..base import BaseSolver
|
||||||
|
|
||||||
Direction: TypeAlias = Literal[">", "<", "^", "v"]
|
Direction: TypeAlias = Literal[">", "<", "^", "v"]
|
||||||
|
|
||||||
@ -32,202 +30,204 @@ MAPPINGS: dict[Direction, tuple[int, int, Direction]] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def print_shortest_path(
|
class Solver(BaseSolver):
|
||||||
grid: list[list[int]],
|
def print_shortest_path(
|
||||||
target: tuple[int, int],
|
self,
|
||||||
per_cell: dict[tuple[int, int], list[tuple[Label, int]]],
|
grid: list[list[int]],
|
||||||
):
|
target: tuple[int, int],
|
||||||
assert len(per_cell[target]) == 1
|
per_cell: dict[tuple[int, int], list[tuple[Label, int]]],
|
||||||
label = per_cell[target][0][0]
|
):
|
||||||
|
assert len(per_cell[target]) == 1
|
||||||
|
label = per_cell[target][0][0]
|
||||||
|
|
||||||
path: list[Label] = []
|
path: list[Label] = []
|
||||||
while True:
|
while True:
|
||||||
path.insert(0, label)
|
path.insert(0, label)
|
||||||
if label.parent is None:
|
if label.parent is None:
|
||||||
break
|
break
|
||||||
label = label.parent
|
label = label.parent
|
||||||
|
|
||||||
p_grid = [[str(c) for c in r] for r in grid]
|
p_grid = [[str(c) for c in r] for r in grid]
|
||||||
|
|
||||||
for i in range(len(grid)):
|
for i in range(len(grid)):
|
||||||
for j in range(len(grid[0])):
|
for j in range(len(grid[0])):
|
||||||
if per_cell[i, j]:
|
if per_cell[i, j]:
|
||||||
p_grid[i][j] = f"\033[94m{grid[i][j]}\033[0m"
|
p_grid[i][j] = f"\033[94m{grid[i][j]}\033[0m"
|
||||||
|
|
||||||
prev_label = path[0]
|
prev_label = path[0]
|
||||||
for label in path[1:]:
|
for label in path[1:]:
|
||||||
for r in range(
|
for r in range(
|
||||||
min(prev_label.row, label.row), max(prev_label.row, label.row) + 1
|
min(prev_label.row, label.row), max(prev_label.row, label.row) + 1
|
||||||
):
|
|
||||||
for c in range(
|
|
||||||
min(prev_label.col, label.col),
|
|
||||||
max(prev_label.col, label.col) + 1,
|
|
||||||
):
|
):
|
||||||
if (r, c) != (prev_label.row, prev_label.col):
|
for c in range(
|
||||||
p_grid[r][c] = f"\033[93m{grid[r][c]}\033[0m"
|
min(prev_label.col, label.col),
|
||||||
|
max(prev_label.col, label.col) + 1,
|
||||||
|
):
|
||||||
|
if (r, c) != (prev_label.row, prev_label.col):
|
||||||
|
p_grid[r][c] = f"\033[93m{grid[r][c]}\033[0m"
|
||||||
|
|
||||||
p_grid[label.row][label.col] = f"\033[91m{grid[label.row][label.col]}\033[0m"
|
p_grid[label.row][label.col] = (
|
||||||
|
f"\033[91m{grid[label.row][label.col]}\033[0m"
|
||||||
|
)
|
||||||
|
|
||||||
prev_label = label
|
prev_label = label
|
||||||
|
|
||||||
p_grid[0][0] = f"\033[92m{grid[0][0]}\033[0m"
|
p_grid[0][0] = f"\033[92m{grid[0][0]}\033[0m"
|
||||||
|
|
||||||
print("\n".join("".join(row) for row in p_grid))
|
for row in p_grid:
|
||||||
|
self.logger.info("".join(row))
|
||||||
|
|
||||||
|
def shortest_many_paths(self, grid: list[list[int]]) -> dict[tuple[int, int], int]:
|
||||||
|
n_rows, n_cols = len(grid), len(grid[0])
|
||||||
|
|
||||||
def shortest_many_paths(grid: list[list[int]]) -> dict[tuple[int, int], int]:
|
visited: dict[tuple[int, int], tuple[Label, int]] = {}
|
||||||
n_rows, n_cols = len(grid), len(grid[0])
|
|
||||||
|
|
||||||
visited: dict[tuple[int, int], tuple[Label, int]] = {}
|
queue: list[tuple[int, Label]] = [
|
||||||
|
(0, Label(row=n_rows - 1, col=n_cols - 1, direction="^", count=0))
|
||||||
|
]
|
||||||
|
|
||||||
queue: list[tuple[int, Label]] = [
|
while queue and len(visited) != n_rows * n_cols:
|
||||||
(0, Label(row=n_rows - 1, col=n_cols - 1, direction="^", count=0))
|
distance, label = heapq.heappop(queue)
|
||||||
]
|
|
||||||
|
|
||||||
while queue and len(visited) != n_rows * n_cols:
|
if (label.row, label.col) in visited:
|
||||||
distance, label = heapq.heappop(queue)
|
|
||||||
|
|
||||||
if (label.row, label.col) in visited:
|
|
||||||
continue
|
|
||||||
|
|
||||||
visited[label.row, label.col] = (label, distance)
|
|
||||||
|
|
||||||
for direction, (c_row, c_col, i_direction) in MAPPINGS.items():
|
|
||||||
if label.direction == i_direction:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
row, col = (label.row + c_row, label.col + c_col)
|
|
||||||
|
|
||||||
# exclude labels outside the grid or with too many moves in the same
|
|
||||||
# direction
|
|
||||||
if row not in range(0, n_rows) or col not in range(0, n_cols):
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
heapq.heappush(
|
visited[label.row, label.col] = (label, distance)
|
||||||
queue,
|
|
||||||
(
|
for direction, (c_row, c_col, i_direction) in MAPPINGS.items():
|
||||||
|
if label.direction == i_direction:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
row, col = (label.row + c_row, label.col + c_col)
|
||||||
|
|
||||||
|
# exclude labels outside the grid or with too many moves in the same
|
||||||
|
# direction
|
||||||
|
if row not in range(0, n_rows) or col not in range(0, n_cols):
|
||||||
|
continue
|
||||||
|
|
||||||
|
heapq.heappush(
|
||||||
|
queue,
|
||||||
|
(
|
||||||
|
distance
|
||||||
|
+ sum(
|
||||||
|
grid[r][c]
|
||||||
|
for r in range(min(row, label.row), max(row, label.row) + 1)
|
||||||
|
for c in range(min(col, label.col), max(col, label.col) + 1)
|
||||||
|
)
|
||||||
|
- grid[row][col],
|
||||||
|
Label(
|
||||||
|
row=row,
|
||||||
|
col=col,
|
||||||
|
direction=direction,
|
||||||
|
count=0,
|
||||||
|
parent=label,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {(r, c): visited[r, c][1] for r in range(n_rows) for c in range(n_cols)}
|
||||||
|
|
||||||
|
def shortest_path(
|
||||||
|
self,
|
||||||
|
grid: list[list[int]],
|
||||||
|
min_straight: int,
|
||||||
|
max_straight: int,
|
||||||
|
lower_bounds: dict[tuple[int, int], int],
|
||||||
|
) -> int:
|
||||||
|
n_rows, n_cols = len(grid), len(grid[0])
|
||||||
|
|
||||||
|
target = (len(grid) - 1, len(grid[0]) - 1)
|
||||||
|
|
||||||
|
# for each tuple (row, col, direction, count), the associated label when visited
|
||||||
|
visited: dict[tuple[int, int, str, int], Label] = {}
|
||||||
|
|
||||||
|
# list of all visited labels for a cell (with associated distance)
|
||||||
|
per_cell: dict[tuple[int, int], list[tuple[Label, int]]] = defaultdict(list)
|
||||||
|
|
||||||
|
# need to add two start labels, otherwise one of the two possible direction will
|
||||||
|
# not be possible
|
||||||
|
queue: list[tuple[int, int, Label]] = [
|
||||||
|
(lower_bounds[0, 0], 0, Label(row=0, col=0, direction="^", count=0)),
|
||||||
|
(lower_bounds[0, 0], 0, Label(row=0, col=0, direction="<", count=0)),
|
||||||
|
]
|
||||||
|
|
||||||
|
while queue:
|
||||||
|
_, distance, label = heapq.heappop(queue)
|
||||||
|
|
||||||
|
if (label.row, label.col, label.direction, label.count) in visited:
|
||||||
|
continue
|
||||||
|
|
||||||
|
visited[label.row, label.col, label.direction, label.count] = label
|
||||||
|
per_cell[label.row, label.col].append((label, distance))
|
||||||
|
|
||||||
|
if (label.row, label.col) == target:
|
||||||
|
break
|
||||||
|
|
||||||
|
for direction, (c_row, c_col, i_direction) in MAPPINGS.items():
|
||||||
|
# cannot move in the opposite direction
|
||||||
|
if label.direction == i_direction:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# other direction, move 'min_straight' in the new direction
|
||||||
|
elif label.direction != direction:
|
||||||
|
row, col, count = (
|
||||||
|
label.row + min_straight * c_row,
|
||||||
|
label.col + min_straight * c_col,
|
||||||
|
min_straight,
|
||||||
|
)
|
||||||
|
|
||||||
|
# same direction, too many count
|
||||||
|
elif label.count == max_straight:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# same direction, keep going and increment count
|
||||||
|
else:
|
||||||
|
row, col, count = (
|
||||||
|
label.row + c_row,
|
||||||
|
label.col + c_col,
|
||||||
|
label.count + 1,
|
||||||
|
)
|
||||||
|
# exclude labels outside the grid or with too many moves in the same
|
||||||
|
# direction
|
||||||
|
if row not in range(0, n_rows) or col not in range(0, n_cols):
|
||||||
|
continue
|
||||||
|
|
||||||
|
distance_to = (
|
||||||
distance
|
distance
|
||||||
+ sum(
|
+ sum(
|
||||||
grid[r][c]
|
grid[r][c]
|
||||||
for r in range(min(row, label.row), max(row, label.row) + 1)
|
for r in range(min(row, label.row), max(row, label.row) + 1)
|
||||||
for c in range(min(col, label.col), max(col, label.col) + 1)
|
for c in range(min(col, label.col), max(col, label.col) + 1)
|
||||||
)
|
)
|
||||||
- grid[row][col],
|
- grid[label.row][label.col]
|
||||||
Label(
|
)
|
||||||
row=row,
|
|
||||||
col=col,
|
heapq.heappush(
|
||||||
direction=direction,
|
queue,
|
||||||
count=0,
|
(
|
||||||
parent=label,
|
distance_to + lower_bounds[row, col],
|
||||||
|
distance_to,
|
||||||
|
Label(
|
||||||
|
row=row,
|
||||||
|
col=col,
|
||||||
|
direction=direction,
|
||||||
|
count=count,
|
||||||
|
parent=label,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return {(r, c): visited[r, c][1] for r in range(n_rows) for c in range(n_cols)}
|
|
||||||
|
|
||||||
|
|
||||||
def shortest_path(
|
|
||||||
grid: list[list[int]],
|
|
||||||
min_straight: int,
|
|
||||||
max_straight: int,
|
|
||||||
lower_bounds: dict[tuple[int, int], int],
|
|
||||||
) -> int:
|
|
||||||
n_rows, n_cols = len(grid), len(grid[0])
|
|
||||||
|
|
||||||
target = (len(grid) - 1, len(grid[0]) - 1)
|
|
||||||
|
|
||||||
# for each tuple (row, col, direction, count), the associated label when visited
|
|
||||||
visited: dict[tuple[int, int, str, int], Label] = {}
|
|
||||||
|
|
||||||
# list of all visited labels for a cell (with associated distance)
|
|
||||||
per_cell: dict[tuple[int, int], list[tuple[Label, int]]] = defaultdict(list)
|
|
||||||
|
|
||||||
# need to add two start labels, otherwise one of the two possible direction will
|
|
||||||
# not be possible
|
|
||||||
queue: list[tuple[int, int, Label]] = [
|
|
||||||
(lower_bounds[0, 0], 0, Label(row=0, col=0, direction="^", count=0)),
|
|
||||||
(lower_bounds[0, 0], 0, Label(row=0, col=0, direction="<", count=0)),
|
|
||||||
]
|
|
||||||
|
|
||||||
while queue:
|
|
||||||
_, distance, label = heapq.heappop(queue)
|
|
||||||
|
|
||||||
if (label.row, label.col, label.direction, label.count) in visited:
|
|
||||||
continue
|
|
||||||
|
|
||||||
visited[label.row, label.col, label.direction, label.count] = label
|
|
||||||
per_cell[label.row, label.col].append((label, distance))
|
|
||||||
|
|
||||||
if (label.row, label.col) == target:
|
|
||||||
break
|
|
||||||
|
|
||||||
for direction, (c_row, c_col, i_direction) in MAPPINGS.items():
|
|
||||||
# cannot move in the opposite direction
|
|
||||||
if label.direction == i_direction:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# other direction, move 'min_straight' in the new direction
|
|
||||||
elif label.direction != direction:
|
|
||||||
row, col, count = (
|
|
||||||
label.row + min_straight * c_row,
|
|
||||||
label.col + min_straight * c_col,
|
|
||||||
min_straight,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# same direction, too many count
|
if self.verbose:
|
||||||
elif label.count == max_straight:
|
self.print_shortest_path(grid, target, per_cell)
|
||||||
continue
|
|
||||||
|
|
||||||
# same direction, keep going and increment count
|
return per_cell[target][0][1]
|
||||||
else:
|
|
||||||
row, col, count = (
|
|
||||||
label.row + c_row,
|
|
||||||
label.col + c_col,
|
|
||||||
label.count + 1,
|
|
||||||
)
|
|
||||||
# exclude labels outside the grid or with too many moves in the same
|
|
||||||
# direction
|
|
||||||
if row not in range(0, n_rows) or col not in range(0, n_cols):
|
|
||||||
continue
|
|
||||||
|
|
||||||
distance_to = (
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
distance
|
data = [[int(c) for c in r] for r in input.splitlines()]
|
||||||
+ sum(
|
estimates = self.shortest_many_paths(data)
|
||||||
grid[r][c]
|
|
||||||
for r in range(min(row, label.row), max(row, label.row) + 1)
|
|
||||||
for c in range(min(col, label.col), max(col, label.col) + 1)
|
|
||||||
)
|
|
||||||
- grid[label.row][label.col]
|
|
||||||
)
|
|
||||||
|
|
||||||
heapq.heappush(
|
# part 1
|
||||||
queue,
|
yield self.shortest_path(data, 1, 3, lower_bounds=estimates)
|
||||||
(
|
|
||||||
distance_to + lower_bounds[row, col],
|
|
||||||
distance_to,
|
|
||||||
Label(
|
|
||||||
row=row,
|
|
||||||
col=col,
|
|
||||||
direction=direction,
|
|
||||||
count=count,
|
|
||||||
parent=label,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
if VERBOSE:
|
# part 2
|
||||||
print_shortest_path(grid, target, per_cell)
|
yield self.shortest_path(data, 4, 10, lower_bounds=estimates)
|
||||||
|
|
||||||
return per_cell[target][0][1]
|
|
||||||
|
|
||||||
|
|
||||||
data = [[int(c) for c in r] for r in sys.stdin.read().splitlines()]
|
|
||||||
estimates = shortest_many_paths(data)
|
|
||||||
|
|
||||||
# part 1
|
|
||||||
answer_1 = shortest_path(data, 1, 3, lower_bounds=estimates)
|
|
||||||
print(f"answer 1 is {answer_1}")
|
|
||||||
|
|
||||||
# part 2
|
|
||||||
answer_2 = shortest_path(data, 4, 10, lower_bounds=estimates)
|
|
||||||
print(f"answer 2 is {answer_2}")
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import sys
|
from typing import Any, Iterator, Literal, TypeAlias, cast
|
||||||
from typing import Literal, TypeAlias, cast
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
Direction: TypeAlias = Literal["R", "L", "U", "D"]
|
Direction: TypeAlias = Literal["R", "L", "U", "D"]
|
||||||
|
|
||||||
@ -33,22 +34,23 @@ def polygon(values: list[tuple[Direction, int]]) -> tuple[list[tuple[int, int]],
|
|||||||
return corners, perimeter
|
return corners, perimeter
|
||||||
|
|
||||||
|
|
||||||
lines = sys.stdin.read().splitlines()
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
|
||||||
|
# part 1
|
||||||
|
yield area(
|
||||||
|
*polygon(
|
||||||
|
[(cast(Direction, (p := line.split())[0]), int(p[1])) for line in lines]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# part 1
|
# part 2
|
||||||
answer_1 = area(
|
yield area(
|
||||||
*polygon([(cast(Direction, (p := line.split())[0]), int(p[1])) for line in lines])
|
*polygon(
|
||||||
)
|
[
|
||||||
print(f"answer 1 is {answer_1}")
|
(DIRECTIONS[int((h := line.split()[-1])[-2])], int(h[2:-2], 16))
|
||||||
|
for line in lines
|
||||||
# part 2
|
]
|
||||||
answer_2 = area(
|
)
|
||||||
*polygon(
|
)
|
||||||
[
|
|
||||||
(DIRECTIONS[int((h := line.split()[-1])[-2])], int(h[2:-2], 16))
|
|
||||||
for line in lines
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
print(f"answer 2 is {answer_2}")
|
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
import logging
|
|
||||||
import operator
|
import operator
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from math import prod
|
from math import prod
|
||||||
from typing import Literal, TypeAlias, cast
|
from typing import Any, Iterator, Literal, TypeAlias, cast
|
||||||
|
|
||||||
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
|
from ..base import BaseSolver
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
|
|
||||||
|
|
||||||
Category: TypeAlias = Literal["x", "m", "a", "s"]
|
Category: TypeAlias = Literal["x", "m", "a", "s"]
|
||||||
Part: TypeAlias = dict[Category, int]
|
Part: TypeAlias = dict[Category, int]
|
||||||
@ -22,119 +17,118 @@ Check: TypeAlias = tuple[Category, Literal["<", ">"], int] | None
|
|||||||
Workflow: TypeAlias = list[tuple[Check, str]]
|
Workflow: TypeAlias = list[tuple[Check, str]]
|
||||||
|
|
||||||
|
|
||||||
def accept(workflows: dict[str, Workflow], part: Part) -> bool:
|
class Solver(BaseSolver):
|
||||||
workflow = "in"
|
def accept(self, workflows: dict[str, Workflow], part: Part) -> bool:
|
||||||
decision: bool | None = None
|
workflow = "in"
|
||||||
|
decision: bool | None = None
|
||||||
|
|
||||||
while decision is None:
|
while decision is None:
|
||||||
for check, target in workflows[workflow]:
|
for check, target in workflows[workflow]:
|
||||||
passed = check is None
|
passed = check is None
|
||||||
if check is not None:
|
if check is not None:
|
||||||
category, sense, value = check
|
category, sense, value = check
|
||||||
passed = OPERATORS[sense](part[category], value)
|
passed = OPERATORS[sense](part[category], value)
|
||||||
|
|
||||||
if passed:
|
if passed:
|
||||||
if target in workflows:
|
if target in workflows:
|
||||||
workflow = target
|
workflow = target
|
||||||
else:
|
else:
|
||||||
decision = target == "A"
|
decision = target == "A"
|
||||||
break
|
break
|
||||||
|
|
||||||
return decision
|
return decision
|
||||||
|
|
||||||
|
def propagate(self, workflows: dict[str, Workflow], start: PartWithBounds) -> int:
|
||||||
|
def _fmt(meta: PartWithBounds) -> str:
|
||||||
|
return "{" + ", ".join(f"{k}={v}" for k, v in meta.items()) + "}"
|
||||||
|
|
||||||
def propagate(workflows: dict[str, Workflow], start: PartWithBounds) -> int:
|
def transfer_or_accept(
|
||||||
def _fmt(meta: PartWithBounds) -> str:
|
target: str, meta: PartWithBounds, queue: list[tuple[PartWithBounds, str]]
|
||||||
return "{" + ", ".join(f"{k}={v}" for k, v in meta.items()) + "}"
|
) -> int:
|
||||||
|
count = 0
|
||||||
def transfer_or_accept(
|
if target in workflows:
|
||||||
target: str, meta: PartWithBounds, queue: list[tuple[PartWithBounds, str]]
|
self.logger.info(f" transfer to {target}")
|
||||||
) -> int:
|
queue.append((meta, target))
|
||||||
count = 0
|
elif target == "A":
|
||||||
if target in workflows:
|
count = prod((high - low + 1) for low, high in meta.values())
|
||||||
logging.info(f" transfer to {target}")
|
self.logger.info(f" accepted ({count})")
|
||||||
queue.append((meta, target))
|
|
||||||
elif target == "A":
|
|
||||||
count = prod((high - low + 1) for low, high in meta.values())
|
|
||||||
logging.info(f" accepted ({count})")
|
|
||||||
else:
|
|
||||||
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}: {_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" checking {_fmt(meta)} against {category} {sense} {value}")
|
|
||||||
|
|
||||||
if not op(bounds[0], value) and not op(bounds[1], value):
|
|
||||||
logging.info(" reject, always false")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if op(meta[category][0], value) and op(meta[category][1], value):
|
|
||||||
logging.info(" accept, always true")
|
|
||||||
accepted += transfer_or_accept(target, meta, queue)
|
|
||||||
break
|
|
||||||
|
|
||||||
meta2 = meta.copy()
|
|
||||||
low, high = meta[category]
|
|
||||||
if sense == "<":
|
|
||||||
meta[category], meta2[category] = (value, high), (low, value - 1)
|
|
||||||
else:
|
else:
|
||||||
meta[category], meta2[category] = (low, value), (value + 1, high)
|
self.logger.info(" rejected")
|
||||||
logging.info(f" split {_fmt(meta2)} ({target}), {_fmt(meta)}")
|
return count
|
||||||
|
|
||||||
accepted += transfer_or_accept(target, meta2, queue)
|
accepted = 0
|
||||||
|
queue: list[tuple[PartWithBounds, str]] = [(start, "in")]
|
||||||
|
|
||||||
logging.info(f"run took {n_iterations} iterations")
|
n_iterations = 0
|
||||||
return accepted
|
|
||||||
|
|
||||||
|
while queue:
|
||||||
|
n_iterations += 1
|
||||||
|
meta, workflow = queue.pop()
|
||||||
|
self.logger.info(f"{workflow}: {_fmt(meta)}")
|
||||||
|
for check, target in workflows[workflow]:
|
||||||
|
if check is None:
|
||||||
|
self.logger.info(" end-of-workflow")
|
||||||
|
accepted += transfer_or_accept(target, meta, queue)
|
||||||
|
continue
|
||||||
|
|
||||||
workflows_s, parts_s = sys.stdin.read().strip().split("\n\n")
|
category, sense, value = check
|
||||||
|
bounds, op = meta[category], OPERATORS[sense]
|
||||||
|
|
||||||
workflows: dict[str, Workflow] = {}
|
self.logger.info(
|
||||||
for workflow_s in workflows_s.split("\n"):
|
f" checking {_fmt(meta)} against {category} {sense} {value}"
|
||||||
name, block_s = workflow_s.split("{")
|
)
|
||||||
workflows[name] = []
|
|
||||||
|
|
||||||
for block in block_s[:-1].split(","):
|
if not op(bounds[0], value) and not op(bounds[1], value):
|
||||||
check: Check
|
self.logger.info(" reject, always false")
|
||||||
if (i := block.find(":")) >= 0:
|
continue
|
||||||
check = (
|
|
||||||
cast(Category, block[0]),
|
|
||||||
cast(Literal["<", ">"], block[1]),
|
|
||||||
int(block[2:i]),
|
|
||||||
)
|
|
||||||
target = block[i + 1 :]
|
|
||||||
else:
|
|
||||||
check, target = None, block
|
|
||||||
workflows[name].append((check, target))
|
|
||||||
|
|
||||||
# part 1
|
if op(meta[category][0], value) and op(meta[category][1], value):
|
||||||
parts: list[Part] = [
|
self.logger.info(" accept, always true")
|
||||||
{cast(Category, s[0]): int(s[2:]) for s in part_s[1:-1].split(",")}
|
accepted += transfer_or_accept(target, meta, queue)
|
||||||
for part_s in parts_s.split("\n")
|
break
|
||||||
]
|
|
||||||
answer_1 = sum(sum(part.values()) for part in parts if accept(workflows, part))
|
|
||||||
print(f"answer 1 is {answer_1}")
|
|
||||||
|
|
||||||
|
meta2 = meta.copy()
|
||||||
|
low, high = meta[category]
|
||||||
|
if sense == "<":
|
||||||
|
meta[category], meta2[category] = (value, high), (low, value - 1)
|
||||||
|
else:
|
||||||
|
meta[category], meta2[category] = (low, value), (value + 1, high)
|
||||||
|
self.logger.info(f" split {_fmt(meta2)} ({target}), {_fmt(meta)}")
|
||||||
|
|
||||||
# part 2
|
accepted += transfer_or_accept(target, meta2, queue)
|
||||||
answer_2 = propagate(
|
|
||||||
workflows, {cast(Category, c): (1, 4000) for c in ["x", "m", "a", "s"]}
|
self.logger.info(f"run took {n_iterations} iterations")
|
||||||
)
|
return accepted
|
||||||
print(f"answer 2 is {answer_2}")
|
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
workflows_s, parts_s = input.split("\n\n")
|
||||||
|
|
||||||
|
workflows: dict[str, Workflow] = {}
|
||||||
|
for workflow_s in workflows_s.split("\n"):
|
||||||
|
name, block_s = workflow_s.split("{")
|
||||||
|
workflows[name] = []
|
||||||
|
|
||||||
|
for block in block_s[:-1].split(","):
|
||||||
|
check: Check
|
||||||
|
if (i := block.find(":")) >= 0:
|
||||||
|
check = (
|
||||||
|
cast(Category, block[0]),
|
||||||
|
cast(Literal["<", ">"], block[1]),
|
||||||
|
int(block[2:i]),
|
||||||
|
)
|
||||||
|
target = block[i + 1 :]
|
||||||
|
else:
|
||||||
|
check, target = None, block
|
||||||
|
workflows[name].append((check, target))
|
||||||
|
|
||||||
|
# part 1
|
||||||
|
parts: list[Part] = [
|
||||||
|
{cast(Category, s[0]): int(s[2:]) for s in part_s[1:-1].split(",")}
|
||||||
|
for part_s in parts_s.split("\n")
|
||||||
|
]
|
||||||
|
yield sum(sum(part.values()) for part in parts if self.accept(workflows, part))
|
||||||
|
|
||||||
|
# part 2
|
||||||
|
yield self.propagate(
|
||||||
|
workflows, {cast(Category, c): (1, 4000) for c in ["x", "m", "a", "s"]}
|
||||||
|
)
|
||||||
|
@ -1,161 +1,172 @@
|
|||||||
import logging
|
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from math import lcm
|
from math import lcm
|
||||||
from typing import Literal, TypeAlias
|
from typing import Any, Iterator, Literal, TypeAlias
|
||||||
|
|
||||||
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
|
|
||||||
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
|
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
ModuleType: TypeAlias = Literal["broadcaster", "conjunction", "flip-flop"]
|
ModuleType: TypeAlias = Literal["broadcaster", "conjunction", "flip-flop"]
|
||||||
PulseType: TypeAlias = Literal["high", "low"]
|
PulseType: TypeAlias = Literal["high", "low"]
|
||||||
|
|
||||||
modules: dict[str, tuple[ModuleType, list[str]]] = {}
|
|
||||||
|
|
||||||
lines = sys.stdin.read().splitlines()
|
class Solver(BaseSolver):
|
||||||
|
_modules: dict[str, tuple[ModuleType, list[str]]]
|
||||||
|
|
||||||
for line in lines:
|
def _process(
|
||||||
name, outputs_s = line.split(" -> ")
|
self,
|
||||||
outputs = outputs_s.split(", ")
|
start: tuple[str, str, PulseType],
|
||||||
if name == "broadcaster":
|
flip_flop_states: dict[str, Literal["on", "off"]],
|
||||||
modules["broadcaster"] = ("broadcaster", outputs)
|
conjunction_states: dict[str, dict[str, PulseType]],
|
||||||
else:
|
) -> tuple[dict[PulseType, int], dict[str, dict[PulseType, int]]]:
|
||||||
modules[name[1:]] = (
|
pulses: list[tuple[str, str, PulseType]] = [start]
|
||||||
"conjunction" if name.startswith("&") else "flip-flop",
|
counts: dict[PulseType, int] = {"low": 0, "high": 0}
|
||||||
outputs,
|
inputs: dict[str, dict[PulseType, int]] = defaultdict(
|
||||||
|
lambda: {"low": 0, "high": 0}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.logger.info("starting process... ")
|
||||||
|
|
||||||
def process(
|
while pulses:
|
||||||
start: tuple[str, str, PulseType],
|
input, name, pulse = pulses.pop(0)
|
||||||
flip_flop_states: dict[str, Literal["on", "off"]],
|
self.logger.info(f"{input} -{pulse}-> {name}")
|
||||||
conjunction_states: dict[str, dict[str, PulseType]],
|
counts[pulse] += 1
|
||||||
) -> 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... ")
|
inputs[name][pulse] += 1
|
||||||
|
|
||||||
while pulses:
|
if name not in self._modules:
|
||||||
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
|
continue
|
||||||
|
|
||||||
if flip_flop_states[name] == "off":
|
type, outputs = self._modules[name]
|
||||||
flip_flop_states[name] = "on"
|
|
||||||
pulse = "high"
|
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:
|
else:
|
||||||
flip_flop_states[name] = "off"
|
conjunction_states[name][input] = pulse
|
||||||
pulse = "low"
|
|
||||||
|
|
||||||
else:
|
if all(state == "high" for state in conjunction_states[name].values()):
|
||||||
conjunction_states[name][input] = pulse
|
pulse = "low"
|
||||||
|
else:
|
||||||
|
pulse = "high"
|
||||||
|
|
||||||
if all(state == "high" for state in conjunction_states[name].values()):
|
pulses.extend((name, output, pulse) for output in outputs)
|
||||||
pulse = "low"
|
|
||||||
|
return counts, inputs
|
||||||
|
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
self._modules = {}
|
||||||
|
|
||||||
|
lines = sys.stdin.read().splitlines()
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
name, outputs_s = line.split(" -> ")
|
||||||
|
outputs = outputs_s.split(", ")
|
||||||
|
if name == "broadcaster":
|
||||||
|
self._modules["broadcaster"] = ("broadcaster", outputs)
|
||||||
else:
|
else:
|
||||||
pulse = "high"
|
self._modules[name[1:]] = (
|
||||||
|
"conjunction" if name.startswith("&") else "flip-flop",
|
||||||
|
outputs,
|
||||||
|
)
|
||||||
|
|
||||||
pulses.extend((name, output, pulse) for output in outputs)
|
if self.outputs:
|
||||||
|
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 self._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 self._modules.items():
|
||||||
|
for output in outputs:
|
||||||
|
fp.write(f"{name} -> {output};\n")
|
||||||
|
fp.write("}\n")
|
||||||
|
|
||||||
return counts, inputs
|
# part 1
|
||||||
|
flip_flop_states: dict[str, Literal["on", "off"]] = {
|
||||||
|
name: "off"
|
||||||
|
for name, (type, _) in self._modules.items()
|
||||||
|
if type == "flip-flop"
|
||||||
|
}
|
||||||
|
conjunction_states: dict[str, dict[str, PulseType]] = {
|
||||||
|
name: {
|
||||||
|
input: "low"
|
||||||
|
for input, (_, outputs) in self._modules.items()
|
||||||
|
if name in outputs
|
||||||
|
}
|
||||||
|
for name, (type, _) in self._modules.items()
|
||||||
|
if type == "conjunction"
|
||||||
|
}
|
||||||
|
counts: dict[PulseType, int] = {"low": 0, "high": 0}
|
||||||
|
for _ in range(1000):
|
||||||
|
result, _ = self._process(
|
||||||
|
("button", "broadcaster", "low"), flip_flop_states, conjunction_states
|
||||||
|
)
|
||||||
|
for pulse in ("low", "high"):
|
||||||
|
counts[pulse] += result[pulse]
|
||||||
|
yield counts["low"] * counts["high"]
|
||||||
|
|
||||||
|
# part 2
|
||||||
|
|
||||||
with open("./day20.dot", "w") as fp:
|
# reset states
|
||||||
fp.write("digraph G {\n")
|
for name in flip_flop_states:
|
||||||
fp.write("rx [shape=circle, color=red, style=filled];\n")
|
flip_flop_states[name] = "off"
|
||||||
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
|
for name in conjunction_states:
|
||||||
flip_flop_states: dict[str, Literal["on", "off"]] = {
|
for input in conjunction_states[name]:
|
||||||
name: "off" for name, (type, _) in modules.items() if type == "flip-flop"
|
conjunction_states[name][input] = "low"
|
||||||
}
|
|
||||||
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
|
# find the conjunction connected to rx
|
||||||
|
to_rx = [
|
||||||
|
name for name, (_, outputs) in self._modules.items() if "rx" in outputs
|
||||||
|
]
|
||||||
|
assert len(to_rx) == 1, "cannot handle multiple module inputs for rx"
|
||||||
|
assert (
|
||||||
|
self._modules[to_rx[0]][0] == "conjunction"
|
||||||
|
), "can only handle conjunction as input to rx"
|
||||||
|
|
||||||
# reset states
|
to_rx_inputs = [
|
||||||
for name in flip_flop_states:
|
name for name, (_, outputs) in self._modules.items() if to_rx[0] in outputs
|
||||||
flip_flop_states[name] = "off"
|
]
|
||||||
|
assert all(
|
||||||
|
self._modules[i][0] == "conjunction" and len(self._modules[i][1]) == 1
|
||||||
|
for i in to_rx_inputs
|
||||||
|
), "can only handle inversion as second-order inputs to rx"
|
||||||
|
|
||||||
for name in conjunction_states:
|
count = 1
|
||||||
for input in conjunction_states[name]:
|
cycles: dict[str, int] = {}
|
||||||
conjunction_states[name][input] = "low"
|
second: dict[str, int] = {}
|
||||||
|
while len(second) != len(to_rx_inputs):
|
||||||
|
_, inputs = self._process(
|
||||||
|
("button", "broadcaster", "low"), flip_flop_states, conjunction_states
|
||||||
|
)
|
||||||
|
|
||||||
# find the conjunction connected to rx
|
for node in to_rx_inputs:
|
||||||
to_rx = [name for name, (_, outputs) in modules.items() if "rx" in outputs]
|
if inputs[node]["low"] == 1:
|
||||||
assert len(to_rx) == 1, "cannot handle multiple module inputs for rx"
|
if node not in cycles:
|
||||||
assert (
|
cycles[node] = count
|
||||||
modules[to_rx[0]][0] == "conjunction"
|
elif node not in second:
|
||||||
), "can only handle conjunction as input to rx"
|
second[node] = count
|
||||||
|
|
||||||
to_rx_inputs = [name for name, (_, outputs) in modules.items() if to_rx[0] in outputs]
|
count += 1
|
||||||
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"
|
|
||||||
|
|
||||||
|
assert all(
|
||||||
|
second[k] == cycles[k] * 2 for k in to_rx_inputs
|
||||||
|
), "cannot only handle cycles starting at the beginning"
|
||||||
|
|
||||||
count = 1
|
yield lcm(*cycles.values())
|
||||||
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}")
|
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
import logging
|
from typing import Any, Iterator
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
|
from ..base import BaseSolver
|
||||||
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
|
|
||||||
|
|
||||||
|
|
||||||
def reachable(
|
def reachable(
|
||||||
@ -21,129 +18,133 @@ def reachable(
|
|||||||
return tiles
|
return tiles
|
||||||
|
|
||||||
|
|
||||||
map = sys.stdin.read().splitlines()
|
class Solver(BaseSolver):
|
||||||
start = next(
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
(i, j) for i in range(len(map)) for j in range(len(map[i])) if map[i][j] == "S"
|
map = input.splitlines()
|
||||||
)
|
start = next(
|
||||||
|
(i, j)
|
||||||
# part 1
|
for i in range(len(map))
|
||||||
answer_1 = len(reachable(map, {start}, 6 if len(map) < 20 else 64))
|
for j in range(len(map[i]))
|
||||||
print(f"answer 1 is {answer_1}")
|
if map[i][j] == "S"
|
||||||
|
|
||||||
# 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
|
# part 1
|
||||||
A = counts[2][2] if radius % 2 == 0 else counts[2][1]
|
yield len(reachable(map, {start}, 6 if len(map) < 20 else 64))
|
||||||
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
|
# part 2
|
||||||
#
|
|
||||||
# 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
|
# the initial map is a square and contains an empty rhombus whose diameter is
|
||||||
answer_2 = a * n * n + b * n + c
|
# the size of the map, and has only empty cells around the middle row and column
|
||||||
print(f"answer 2 (v2) is {answer_2}")
|
#
|
||||||
|
# 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 self.verbose:
|
||||||
|
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"
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
self.logger.info("".join(row))
|
||||||
|
|
||||||
|
self.logger.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
|
||||||
|
yield a * n * n + b * n + c
|
||||||
|
@ -1,111 +1,109 @@
|
|||||||
import itertools
|
import itertools
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import string
|
import string
|
||||||
import sys
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
|
from ..base import BaseSolver
|
||||||
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
|
|
||||||
|
|
||||||
|
|
||||||
lines = sys.stdin.read().splitlines()
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
|
||||||
|
def _name(i: int) -> str:
|
||||||
|
if len(lines) < 26:
|
||||||
|
return string.ascii_uppercase[i]
|
||||||
|
return f"B{i:04d}"
|
||||||
|
|
||||||
def _name(i: int) -> str:
|
def build_supports(
|
||||||
if len(lines) < 26:
|
bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]],
|
||||||
return string.ascii_uppercase[i]
|
) -> tuple[dict[int, set[int]], dict[int, set[int]]]:
|
||||||
return f"B{i:04d}"
|
# 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)
|
||||||
|
|
||||||
def build_supports(
|
for z in range(sz - 1, 0, -1):
|
||||||
bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]],
|
if any(levels[x, y, z] >= 0 for x, y in itertools.product(xs, ys)):
|
||||||
) -> tuple[dict[int, set[int]], dict[int, set[int]]]:
|
break
|
||||||
# 1. compute locations where a brick of sand will land after falling by processing
|
sz, ez = sz - 1, ez - 1
|
||||||
# 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)
|
bricks[i_brick] = ((sx, sy, sz), (ex, ey, ez))
|
||||||
|
zs = range(sz, ez + 1)
|
||||||
|
|
||||||
for z in range(sz - 1, 0, -1):
|
for x, y, z in itertools.product(xs, ys, zs):
|
||||||
if any(levels[x, y, z] >= 0 for x, y in itertools.product(xs, ys)):
|
levels[x, y, z] = i_brick
|
||||||
break
|
|
||||||
sz, ez = sz - 1, ez - 1
|
|
||||||
|
|
||||||
bricks[i_brick] = ((sx, sy, sz), (ex, ey, ez))
|
# 2. compute the bricks that supports any brick
|
||||||
zs = range(sz, ez + 1)
|
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)
|
||||||
|
|
||||||
for x, y, z in itertools.product(xs, ys, zs):
|
supported_by[i_brick] = {
|
||||||
levels[x, y, z] = 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
|
||||||
|
}
|
||||||
|
self.logger.info(
|
||||||
|
f"{name} supported by {', '.join(map(_name, supported_by[i_brick]))}"
|
||||||
|
)
|
||||||
|
|
||||||
# 2. compute the bricks that supports any brick
|
for support in supported_by[i_brick]:
|
||||||
supported_by: dict[int, set[int]] = {}
|
supports[support].add(i_brick)
|
||||||
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] = {
|
return supported_by, supports
|
||||||
v
|
|
||||||
for x, y in itertools.product(range(sx, ex + 1), range(sy, ey + 1))
|
bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]] = []
|
||||||
if (v := levels[x, y, sz - 1]) != -1
|
for line in lines:
|
||||||
}
|
bricks.append(
|
||||||
logging.info(
|
(
|
||||||
f"{name} supported by {', '.join(map(_name, supported_by[i_brick]))}"
|
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
|
||||||
|
yield len(bricks) - sum(
|
||||||
|
any(len(supported_by[supported]) == 1 for supported in supports_to)
|
||||||
|
for supports_to in supports.values()
|
||||||
)
|
)
|
||||||
|
|
||||||
for support in supported_by[i_brick]:
|
# part 2
|
||||||
supports[support].add(i_brick)
|
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
|
||||||
|
}
|
||||||
|
|
||||||
return supported_by, supports
|
supported_by_copy = dict(supported_by)
|
||||||
|
|
||||||
|
falling_in_chain[i_brick] = set()
|
||||||
|
while to_disintegrate:
|
||||||
|
falling_in_chain[i_brick].update(to_disintegrate)
|
||||||
|
|
||||||
bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]] = []
|
to_disintegrate_v: set[int] = set()
|
||||||
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
|
for d_brick in to_disintegrate:
|
||||||
bricks = sorted(bricks, key=lambda b: b[0][-1])
|
for supported in supports[d_brick]:
|
||||||
supported_by, supports = build_supports(bricks)
|
supported_by_copy[supported] = supported_by_copy[supported] - {
|
||||||
|
d_brick
|
||||||
|
}
|
||||||
|
|
||||||
# part 1
|
if not supported_by_copy[supported]:
|
||||||
answer_1 = len(bricks) - sum(
|
to_disintegrate_v.add(supported)
|
||||||
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
|
to_disintegrate = to_disintegrate_v
|
||||||
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)
|
yield sum(len(falling) for falling in falling_in_chain.values())
|
||||||
|
|
||||||
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}")
|
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
import logging
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Literal, Sequence, TypeAlias, cast
|
from typing import Any, Iterator, Literal, Sequence, TypeAlias, cast
|
||||||
|
|
||||||
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
|
from ..base import BaseSolver
|
||||||
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
|
|
||||||
|
|
||||||
DirectionType: TypeAlias = Literal[">", "<", "^", "v", ".", "#"]
|
DirectionType: TypeAlias = Literal[">", "<", "^", "v", ".", "#"]
|
||||||
|
|
||||||
@ -35,6 +31,7 @@ def neighbors(
|
|||||||
Compute neighbors of the given node, ignoring the given set of nodes and considering
|
Compute neighbors of the given node, ignoring the given set of nodes and considering
|
||||||
that you can go uphill on slopes.
|
that you can go uphill on slopes.
|
||||||
"""
|
"""
|
||||||
|
n_rows, n_cols = len(grid), len(grid[0])
|
||||||
i, j = node
|
i, j = node
|
||||||
|
|
||||||
for di, dj in Neighbors[grid[i][j]]:
|
for di, dj in Neighbors[grid[i][j]]:
|
||||||
@ -103,65 +100,66 @@ def compute_direct_links(
|
|||||||
return direct
|
return direct
|
||||||
|
|
||||||
|
|
||||||
def longest_path_length(
|
class Solver(BaseSolver):
|
||||||
links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]],
|
def longest_path_length(
|
||||||
start: tuple[int, int],
|
self,
|
||||||
target: tuple[int, int],
|
links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]],
|
||||||
) -> int:
|
start: tuple[int, int],
|
||||||
max_distance: int = -1
|
target: tuple[int, int],
|
||||||
queue: list[tuple[tuple[int, int], int, frozenset[tuple[int, int]]]] = [
|
) -> int:
|
||||||
(start, 0, frozenset({start}))
|
max_distance: int = -1
|
||||||
]
|
queue: list[tuple[tuple[int, int], int, frozenset[tuple[int, int]]]] = [
|
||||||
|
(start, 0, frozenset({start}))
|
||||||
|
]
|
||||||
|
|
||||||
nodes = 0
|
nodes = 0
|
||||||
while queue:
|
while queue:
|
||||||
node, distance, path = queue.pop()
|
node, distance, path = queue.pop()
|
||||||
|
|
||||||
nodes += 1
|
nodes += 1
|
||||||
|
|
||||||
if node == target:
|
if node == target:
|
||||||
max_distance = max(distance, max_distance)
|
max_distance = max(distance, max_distance)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
queue.extend(
|
queue.extend(
|
||||||
(reach, distance + length, path | {reach})
|
(reach, distance + length, path | {reach})
|
||||||
for reach, length in links.get(node, [])
|
for reach, length in links.get(node, [])
|
||||||
if reach not in path
|
if reach not in path
|
||||||
|
)
|
||||||
|
|
||||||
|
self.logger.info(f"processed {nodes} nodes")
|
||||||
|
|
||||||
|
return max_distance
|
||||||
|
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = cast(list[Sequence[DirectionType]], input.splitlines())
|
||||||
|
|
||||||
|
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)
|
||||||
)
|
)
|
||||||
|
|
||||||
logging.info(f"processed {nodes} nodes")
|
# part 1
|
||||||
|
yield self.longest_path_length(direct_links, start, target)
|
||||||
|
|
||||||
return max_distance
|
# 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()
|
||||||
|
}
|
||||||
|
|
||||||
lines = cast(list[Sequence[DirectionType]], sys.stdin.read().splitlines())
|
yield self.longest_path_length(links, start, target)
|
||||||
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}")
|
|
||||||
|
@ -1,63 +1,68 @@
|
|||||||
import sys
|
from typing import Any, Iterator
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from sympy import solve, symbols
|
from sympy import solve, symbols
|
||||||
|
|
||||||
lines = sys.stdin.read().splitlines()
|
from ..base import BaseSolver
|
||||||
|
|
||||||
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
|
class Solver(BaseSolver):
|
||||||
print(f"answer 1 is {answer_1}")
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
|
||||||
# part 2
|
positions = np.array(
|
||||||
# equation
|
[[int(c) for c in line.split("@")[0].strip().split(", ")] for line in lines]
|
||||||
# p1 + t1 * v1 == p0 + t1 * v0
|
)
|
||||||
# p2 + t2 * v2 == p0 + t2 * v0
|
velocities = np.array(
|
||||||
# p3 + t3 * v3 == p0 + t3 * v0
|
[[int(c) for c in line.split("@")[1].strip().split(", ")] for line in lines]
|
||||||
# ...
|
)
|
||||||
# pn + tn * vn == p0 + tn * v0
|
|
||||||
#
|
|
||||||
|
|
||||||
# we can solve with only 3 lines since each lines contains 3
|
# part 1
|
||||||
# equations (x / y / z), so 3 lines give 9 equations and 9
|
low, high = (
|
||||||
# variables: position (3), velocities (3) and times (3).
|
[7, 27] if len(positions) <= 10 else [200000000000000, 400000000000000]
|
||||||
n = 3
|
)
|
||||||
|
|
||||||
x, y, z, vx, vy, vz, *ts = symbols(
|
count = 0
|
||||||
"x y z vx vy vz " + " ".join(f"t{i}" for i in range(n + 1))
|
for i1, (p1, v1) in enumerate(zip(positions, velocities)):
|
||||||
)
|
p, r = p1[:2], v1[:2]
|
||||||
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]
|
q, s = positions[i1 + 1 :, :2], velocities[i1 + 1 :, :2]
|
||||||
|
|
||||||
answer_2 = r[x] + r[y] + r[z]
|
rs = np.cross(r, s)
|
||||||
print(f"answer 2 is {answer_2}")
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
yield count
|
||||||
|
|
||||||
|
# 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]
|
||||||
|
yield r[x] + r[y] + r[z]
|
||||||
|
@ -1,25 +1,23 @@
|
|||||||
import sys
|
from typing import Any, Iterator
|
||||||
|
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
|
|
||||||
components = {
|
from ..base import BaseSolver
|
||||||
(p := line.split(": "))[0]: p[1].split() for line in sys.stdin.read().splitlines()
|
|
||||||
}
|
|
||||||
|
|
||||||
targets = {t for c in components for t in components[c] if t not in components}
|
|
||||||
|
|
||||||
graph = nx.Graph()
|
class Solver(BaseSolver):
|
||||||
graph.add_edges_from((u, v) for u, vs in components.items() for v in vs)
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
components = {
|
||||||
|
(p := line.split(": "))[0]: p[1].split() for line in input.splitlines()
|
||||||
|
}
|
||||||
|
|
||||||
cut = nx.minimum_edge_cut(graph)
|
graph: "nx.Graph[str]" = nx.Graph()
|
||||||
graph.remove_edges_from(cut)
|
graph.add_edges_from((u, v) for u, vs in components.items() for v in vs)
|
||||||
|
|
||||||
c1, c2 = nx.connected_components(graph)
|
cut = nx.minimum_edge_cut(graph)
|
||||||
|
graph.remove_edges_from(cut)
|
||||||
|
|
||||||
# part 1
|
c1, c2 = nx.connected_components(graph)
|
||||||
answer_1 = len(c1) * len(c2)
|
|
||||||
print(f"answer 1 is {answer_1}")
|
|
||||||
|
|
||||||
# part 2
|
# part 1
|
||||||
answer_2 = ...
|
yield len(c1) * len(c2)
|
||||||
print(f"answer 2 is {answer_2}")
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import math
|
import math
|
||||||
import sys
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
def extreme_times_to_beat(time: int, distance: int) -> tuple[int, int]:
|
def extreme_times_to_beat(time: int, distance: int) -> tuple[int, int]:
|
||||||
@ -25,23 +27,23 @@ def extreme_times_to_beat(time: int, distance: int) -> tuple[int, int]:
|
|||||||
return t1, t2
|
return t1, t2
|
||||||
|
|
||||||
|
|
||||||
lines = sys.stdin.read().splitlines()
|
class Solver(BaseSolver):
|
||||||
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
|
||||||
# part 1
|
# part 1
|
||||||
times = list(map(int, lines[0].split()[1:]))
|
times = list(map(int, lines[0].split()[1:]))
|
||||||
distances = list(map(int, lines[1].split()[1:]))
|
distances = list(map(int, lines[1].split()[1:]))
|
||||||
answer_1 = math.prod(
|
yield math.prod(
|
||||||
t2 - t1 + 1
|
t2 - t1 + 1
|
||||||
for t1, t2 in (
|
for t1, t2 in (
|
||||||
extreme_times_to_beat(time, distance)
|
extreme_times_to_beat(time, distance)
|
||||||
for time, distance in zip(times, distances)
|
for time, distance in zip(times, distances)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
print(f"answer 1 is {answer_1}")
|
|
||||||
|
|
||||||
# part 2
|
# part 2
|
||||||
time = int(lines[0].split(":")[1].strip().replace(" ", ""))
|
time = int(lines[0].split(":")[1].strip().replace(" ", ""))
|
||||||
distance = int(lines[1].split(":")[1].strip().replace(" ", ""))
|
distance = int(lines[1].split(":")[1].strip().replace(" ", ""))
|
||||||
t1, t2 = extreme_times_to_beat(time, distance)
|
t1, t2 = extreme_times_to_beat(time, distance)
|
||||||
answer_2 = t2 - t1 + 1
|
yield t2 - t1 + 1
|
||||||
print(f"answer 2 is {answer_2}")
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import sys
|
|
||||||
from collections import Counter, defaultdict
|
from collections import Counter, defaultdict
|
||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from ..base import BaseSolver
|
||||||
|
|
||||||
|
|
||||||
class HandTypes:
|
class HandTypes:
|
||||||
@ -32,18 +34,17 @@ def extract_key(hand: str, values: dict[str, int], joker: str = "0") -> tuple[in
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
lines = sys.stdin.read().splitlines()
|
class Solver(BaseSolver):
|
||||||
cards = [(t[0], int(t[1])) for line in lines if (t := line.split())]
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
|
lines = input.splitlines()
|
||||||
|
cards = [(t[0], int(t[1])) for line in lines if (t := line.split())]
|
||||||
|
|
||||||
|
# part 1
|
||||||
|
values = {card: value for value, card in enumerate("23456789TJQKA")}
|
||||||
|
cards.sort(key=lambda cv: extract_key(cv[0], values=values))
|
||||||
|
yield sum(rank * value for rank, (_, value) in enumerate(cards, start=1))
|
||||||
|
|
||||||
# part 1
|
# part 2
|
||||||
values = {card: value for value, card in enumerate("23456789TJQKA")}
|
values = {card: value for value, card in enumerate("J23456789TQKA")}
|
||||||
cards.sort(key=lambda cv: extract_key(cv[0], values=values))
|
cards.sort(key=lambda cv: extract_key(cv[0], values=values, joker="J"))
|
||||||
answer_1 = sum(rank * value for rank, (_, value) in enumerate(cards, start=1))
|
yield sum(rank * value for rank, (_, value) in enumerate(cards, start=1))
|
||||||
print(f"answer 1 is {answer_1}")
|
|
||||||
|
|
||||||
# part 2
|
|
||||||
values = {card: value for value, card in enumerate("J23456789TQKA")}
|
|
||||||
cards.sort(key=lambda cv: extract_key(cv[0], values=values, joker="J"))
|
|
||||||
answer_2 = sum(rank * value for rank, (_, value) in enumerate(cards, start=1))
|
|
||||||
print(f"answer 2 is {answer_2}")
|
|
||||||
|
@ -1,29 +1,30 @@
|
|||||||
import itertools
|
import itertools
|
||||||
import math
|
import math
|
||||||
import sys
|
from typing import Any, Iterator
|
||||||
|
|
||||||
lines = sys.stdin.read().splitlines()
|
from ..base import BaseSolver
|
||||||
|
|
||||||
sequence = lines[0]
|
|
||||||
nodes = {
|
|
||||||
p[0]: {d: n for d, n in zip("LR", p[1].strip("()").split(", "))}
|
|
||||||
for line in lines[2:]
|
|
||||||
if (p := line.split(" = "))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def path(start: str):
|
class Solver(BaseSolver):
|
||||||
path = [start]
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
it_seq = iter(itertools.cycle(sequence))
|
lines = input.splitlines()
|
||||||
while not path[-1].endswith("Z"):
|
|
||||||
path.append(nodes[path[-1]][next(it_seq)])
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
sequence = lines[0]
|
||||||
|
nodes = {
|
||||||
|
p[0]: {d: n for d, n in zip("LR", p[1].strip("()").split(", "))}
|
||||||
|
for line in lines[2:]
|
||||||
|
if (p := line.split(" = "))
|
||||||
|
}
|
||||||
|
|
||||||
# part 1
|
def path(start: str):
|
||||||
answer_1 = len(path(next(node for node in nodes if node.endswith("A")))) - 1
|
path = [start]
|
||||||
print(f"answer 1 is {answer_1}")
|
it_seq = iter(itertools.cycle(sequence))
|
||||||
|
while not path[-1].endswith("Z"):
|
||||||
|
path.append(nodes[path[-1]][next(it_seq)])
|
||||||
|
return path
|
||||||
|
|
||||||
# part 2
|
# part 1
|
||||||
answer_2 = math.lcm(*(len(path(node)) - 1 for node in nodes if node.endswith("A")))
|
yield len(path(next(node for node in nodes if node.endswith("A")))) - 1
|
||||||
print(f"answer 2 is {answer_2}")
|
|
||||||
|
# part 2
|
||||||
|
yield math.lcm(*(len(path(node)) - 1 for node in nodes if node.endswith("A")))
|
||||||
|
@ -1,29 +1,34 @@
|
|||||||
import sys
|
from typing import Any, Iterator
|
||||||
|
|
||||||
lines = sys.stdin.read().splitlines()
|
from ..base import BaseSolver
|
||||||
|
|
||||||
data = [[int(c) for c in line.split()] for line in lines]
|
|
||||||
|
|
||||||
right_values: list[int] = []
|
class Solver(BaseSolver):
|
||||||
left_values: list[int] = []
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
for values in data:
|
lines = input.splitlines()
|
||||||
diffs = [values]
|
|
||||||
while any(d != 0 for d in diffs[-1]):
|
|
||||||
diffs.append([rhs - lhs for lhs, rhs in zip(diffs[-1][:-1], diffs[-1][1:])])
|
|
||||||
|
|
||||||
rhs: list[int] = [0]
|
data = [[int(c) for c in line.split()] for line in lines]
|
||||||
lhs: list[int] = [0]
|
|
||||||
for cx in range(len(diffs) - 1):
|
|
||||||
rhs.append(diffs[-cx - 2][-1] + rhs[cx])
|
|
||||||
lhs.append(diffs[-cx - 2][0] - lhs[cx])
|
|
||||||
|
|
||||||
right_values.append(rhs[-1])
|
right_values: list[int] = []
|
||||||
left_values.append(lhs[-1])
|
left_values: list[int] = []
|
||||||
|
for values in data:
|
||||||
|
diffs = [values]
|
||||||
|
while any(d != 0 for d in diffs[-1]):
|
||||||
|
diffs.append(
|
||||||
|
[rhs - lhs for lhs, rhs in zip(diffs[-1][:-1], diffs[-1][1:])]
|
||||||
|
)
|
||||||
|
|
||||||
# part 1
|
rhs: list[int] = [0]
|
||||||
answer_1 = sum(right_values)
|
lhs: list[int] = [0]
|
||||||
print(f"answer 1 is {answer_1}")
|
for cx in range(len(diffs) - 1):
|
||||||
|
rhs.append(diffs[-cx - 2][-1] + rhs[cx])
|
||||||
|
lhs.append(diffs[-cx - 2][0] - lhs[cx])
|
||||||
|
|
||||||
# part 2
|
right_values.append(rhs[-1])
|
||||||
answer_2 = sum(left_values)
|
left_values.append(lhs[-1])
|
||||||
print(f"answer 2 is {answer_2}")
|
|
||||||
|
# part 1
|
||||||
|
yield sum(right_values)
|
||||||
|
|
||||||
|
# part 2
|
||||||
|
yield sum(left_values)
|
||||||
|
@ -54,7 +54,7 @@ def main():
|
|||||||
f".{year}.day{day}", __package__
|
f".{year}.day{day}", __package__
|
||||||
).Solver
|
).Solver
|
||||||
|
|
||||||
solver = solver_class(logging.getLogger("AOC"), year, day)
|
solver = solver_class(logging.getLogger("AOC"), verbose, year, day)
|
||||||
|
|
||||||
data: str
|
data: str
|
||||||
if stdin:
|
if stdin:
|
||||||
|
@ -4,10 +4,14 @@ from typing import Any, Final, Iterator
|
|||||||
|
|
||||||
|
|
||||||
class BaseSolver:
|
class BaseSolver:
|
||||||
def __init__(self, logger: Logger, year: int, day: int):
|
def __init__(
|
||||||
|
self, logger: Logger, verbose: bool, year: int, day: int, outputs: bool = False
|
||||||
|
):
|
||||||
self.logger: Final = logger
|
self.logger: Final = logger
|
||||||
|
self.verbose: Final = verbose
|
||||||
self.year: Final = year
|
self.year: Final = year
|
||||||
self.day: Final = day
|
self.day: Final = day
|
||||||
|
self.outputs = outputs
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def solve(self, input: str) -> Iterator[Any]: ...
|
def solve(self, input: str) -> Iterator[Any]: ...
|
||||||
|
Loading…
Reference in New Issue
Block a user