Refactor code for API #3
							
								
								
									
										16
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										16
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							@@ -1245,6 +1245,20 @@ files = [
 | 
			
		||||
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"]
 | 
			
		||||
 | 
			
		||||
[[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]]
 | 
			
		||||
name = "typing-extensions"
 | 
			
		||||
version = "4.12.2"
 | 
			
		||||
@@ -1281,4 +1295,4 @@ files = [
 | 
			
		||||
[metadata]
 | 
			
		||||
lock-version = "2.0"
 | 
			
		||||
python-versions = "^3.10"
 | 
			
		||||
content-hash = "b643261f91a781d77735e05f6d2ac1002867600c2df6393a9d1a15f5e1189109"
 | 
			
		||||
content-hash = "c91bc307ff4a5b3e8cd1976ebea211c9749fe09d563dd80861f70ce26826cda9"
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ ruff = "^0.8.1"
 | 
			
		||||
poethepoet = "^0.31.1"
 | 
			
		||||
ipykernel = "^6.29.5"
 | 
			
		||||
networkx-stubs = "^0.0.1"
 | 
			
		||||
types-networkx = "^3.4.2.20241115"
 | 
			
		||||
 | 
			
		||||
[tool.poetry.scripts]
 | 
			
		||||
holt59-aoc = "holt59.aoc.__main__:main"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,100 +1,100 @@
 | 
			
		||||
import os
 | 
			
		||||
import sys
 | 
			
		||||
from typing import Literal, cast
 | 
			
		||||
from typing import Any, Iterator, Literal, cast
 | 
			
		||||
 | 
			
		||||
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
 | 
			
		||||
from ..base import BaseSolver
 | 
			
		||||
 | 
			
		||||
Symbol = Literal["|", "-", "L", "J", "7", "F", ".", "S"]
 | 
			
		||||
 | 
			
		||||
lines: list[list[Symbol]] = [
 | 
			
		||||
    [cast(Symbol, symbol) for symbol in line] for line in sys.stdin.read().splitlines()
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
# find starting point
 | 
			
		||||
si, sj = next(
 | 
			
		||||
    (i, j)
 | 
			
		||||
    for i in range(len(lines))
 | 
			
		||||
    for j in range(len(lines[0]))
 | 
			
		||||
    if lines[i][j] == "S"
 | 
			
		||||
)
 | 
			
		||||
class Solver(BaseSolver):
 | 
			
		||||
    def solve(self, input: str) -> Iterator[Any]:
 | 
			
		||||
        lines: list[list[Symbol]] = [
 | 
			
		||||
            [cast(Symbol, symbol) for symbol in line] for line in input.splitlines()
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
# find one of the two outputs
 | 
			
		||||
ni, nj = si, sj
 | 
			
		||||
for ni, nj, chars in (
 | 
			
		||||
    (si - 1, sj, "|7F"),
 | 
			
		||||
    (si + 1, sj, "|LJ"),
 | 
			
		||||
    (si, sj - 1, "-LF"),
 | 
			
		||||
    (si, sj + 1, "-J7"),
 | 
			
		||||
):
 | 
			
		||||
    if lines[ni][nj] in chars:
 | 
			
		||||
        break
 | 
			
		||||
        # find starting point
 | 
			
		||||
        si, sj = next(
 | 
			
		||||
            (i, j)
 | 
			
		||||
            for i in range(len(lines))
 | 
			
		||||
            for j in range(len(lines[0]))
 | 
			
		||||
            if lines[i][j] == "S"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
# 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]
 | 
			
		||||
        # find one of the two outputs
 | 
			
		||||
        ni, nj = si, sj
 | 
			
		||||
        for ni, nj, chars in (
 | 
			
		||||
            (si - 1, sj, "|7F"),
 | 
			
		||||
            (si + 1, sj, "|LJ"),
 | 
			
		||||
            (si, sj - 1, "-LF"),
 | 
			
		||||
            (si, sj + 1, "-J7"),
 | 
			
		||||
        ):
 | 
			
		||||
            if lines[ni][nj] in chars:
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
    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:
 | 
			
		||||
        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
 | 
			
		||||
            sym = lines[i][j]
 | 
			
		||||
 | 
			
		||||
    if (i, j) == (si, sj):
 | 
			
		||||
        break
 | 
			
		||||
            if sym == "|" and pi > i or sym in "JL" and pi == i:
 | 
			
		||||
                i -= 1
 | 
			
		||||
            elif sym == "|" and pi < i or sym in "7F" and pi == i:
 | 
			
		||||
                i += 1
 | 
			
		||||
            elif sym == "-" and pj > j or sym in "J7" and pj == j:
 | 
			
		||||
                j -= 1
 | 
			
		||||
            elif sym == "-" and pj < j or sym in "LF" and pj == j:
 | 
			
		||||
                j += 1
 | 
			
		||||
 | 
			
		||||
    loop.append((i, j))
 | 
			
		||||
 | 
			
		||||
answer_1 = len(loop) // 2
 | 
			
		||||
print(f"answer 1 is {answer_1}")
 | 
			
		||||
 | 
			
		||||
# part 2
 | 
			
		||||
 | 
			
		||||
# replace S by an appropriate character for the loop below
 | 
			
		||||
di1, dj1 = loop[1][0] - loop[0][0], loop[1][1] - loop[0][1]
 | 
			
		||||
di2, dj2 = loop[0][0] - loop[-1][0], loop[0][1] - loop[-1][1]
 | 
			
		||||
mapping: dict[tuple[int, int], dict[tuple[int, int], Symbol]] = {
 | 
			
		||||
    (0, 1): {(0, 1): "-", (-1, 0): "F", (1, 0): "L"},
 | 
			
		||||
    (0, -1): {(0, -1): "-", (-1, 0): "7", (1, 0): "J"},
 | 
			
		||||
    (1, 0): {(1, 0): "|", (0, 1): "7", (0, -1): "F"},
 | 
			
		||||
    (-1, 0): {(-1, 0): "|", (0, -1): "L", (0, 1): "J"},
 | 
			
		||||
}
 | 
			
		||||
lines[si][sj] = mapping[di1, dj1][di2, dj2]
 | 
			
		||||
 | 
			
		||||
# find the points inside the loop using an adaptation of ray casting for a discrete
 | 
			
		||||
# grid (https://stackoverflow.com/a/218081/2666289)
 | 
			
		||||
#
 | 
			
		||||
# use a set for faster '... in loop' check
 | 
			
		||||
#
 | 
			
		||||
loop_s = set(loop)
 | 
			
		||||
inside: set[tuple[int, int]] = set()
 | 
			
		||||
for i in range(len(lines)):
 | 
			
		||||
    cnt = 0
 | 
			
		||||
    for j in range(len(lines[0])):
 | 
			
		||||
        if (i, j) not in loop_s and cnt % 2 == 1:
 | 
			
		||||
            inside.add((i, j))
 | 
			
		||||
 | 
			
		||||
        if (i, j) in loop_s and lines[i][j] in "|LJ":
 | 
			
		||||
            cnt += 1
 | 
			
		||||
 | 
			
		||||
if VERBOSE:
 | 
			
		||||
    for i in range(len(lines)):
 | 
			
		||||
        for j in range(len(lines[0])):
 | 
			
		||||
            if (i, j) == (si, sj):
 | 
			
		||||
                print("\033[91mS\033[0m", end="")
 | 
			
		||||
            elif (i, j) in loop:
 | 
			
		||||
                print(lines[i][j], end="")
 | 
			
		||||
            elif (i, j) in inside:
 | 
			
		||||
                print("\033[92mI\033[0m", end="")
 | 
			
		||||
            else:
 | 
			
		||||
                print(".", end="")
 | 
			
		||||
        print()
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
answer_2 = len(inside)
 | 
			
		||||
print(f"answer 2 is {answer_2}")
 | 
			
		||||
            loop.append((i, j))
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
lines = sys.stdin.read().splitlines()
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
from ..base import BaseSolver
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def compute_total_distance(expansion: int) -> int:
 | 
			
		||||
    distances: list[int] = []
 | 
			
		||||
    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])
 | 
			
		||||
class Solver(BaseSolver):
 | 
			
		||||
    def solve(self, input: str) -> Iterator[Any]:
 | 
			
		||||
        lines = input.splitlines()
 | 
			
		||||
 | 
			
		||||
            dx = sum(
 | 
			
		||||
                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))
 | 
			
		||||
            )
 | 
			
		||||
        data = np.array([[c == "#" for c in line] for line in lines])
 | 
			
		||||
 | 
			
		||||
            distances.append(dx + dy)
 | 
			
		||||
    return sum(distances)
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
# part 1
 | 
			
		||||
answer_1 = compute_total_distance(2)
 | 
			
		||||
print(f"answer 1 is {answer_1}")
 | 
			
		||||
        def compute_total_distance(expansion: int) -> int:
 | 
			
		||||
            distances: list[int] = []
 | 
			
		||||
            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
 | 
			
		||||
answer_2 = compute_total_distance(1000000)
 | 
			
		||||
print(f"answer 2 is {answer_2}")
 | 
			
		||||
                    dx = sum(
 | 
			
		||||
                        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)
 | 
			
		||||
            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 typing import Iterable
 | 
			
		||||
from typing import Any, Iterable, Iterator
 | 
			
		||||
 | 
			
		||||
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
 | 
			
		||||
from ..base import BaseSolver
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@lru_cache
 | 
			
		||||
@@ -77,31 +75,29 @@ def compute_possible_arrangements(
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def compute_all_possible_arrangements(lines: Iterable[str], repeat: int) -> int:
 | 
			
		||||
    count = 0
 | 
			
		||||
class Solver(BaseSolver):
 | 
			
		||||
    def compute_all_possible_arrangements(
 | 
			
		||||
        self, lines: Iterable[str], repeat: int
 | 
			
		||||
    ) -> int:
 | 
			
		||||
        count = 0
 | 
			
		||||
 | 
			
		||||
    if VERBOSE:
 | 
			
		||||
        from tqdm import tqdm
 | 
			
		||||
        for i_line, line in enumerate(lines):
 | 
			
		||||
            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:
 | 
			
		||||
        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,
 | 
			
		||||
        )
 | 
			
		||||
    def solve(self, input: str) -> Iterator[Any]:
 | 
			
		||||
        lines = input.splitlines()
 | 
			
		||||
 | 
			
		||||
    return count
 | 
			
		||||
        # part 1
 | 
			
		||||
        yield self.compute_all_possible_arrangements(lines, 1)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
lines = sys.stdin.read().splitlines()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# 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}")
 | 
			
		||||
        # part 2
 | 
			
		||||
        yield self.compute_all_possible_arrangements(lines, 5)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import sys
 | 
			
		||||
from typing import Callable, Literal
 | 
			
		||||
from typing import Any, Callable, Iterator, Literal
 | 
			
		||||
 | 
			
		||||
from ..base import BaseSolver
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
answer_1 = sum(
 | 
			
		||||
    split(block, axis=1, count=0) + 100 * split(block, axis=0, count=0)
 | 
			
		||||
    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}")
 | 
			
		||||
        # part 2
 | 
			
		||||
        yield sum(
 | 
			
		||||
            split(block, axis=1, count=1) + 100 * split(block, axis=0, count=1)
 | 
			
		||||
            for block in blocks
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,9 @@
 | 
			
		||||
import sys
 | 
			
		||||
from typing import TypeAlias
 | 
			
		||||
from typing import Any, Iterator, TypeAlias
 | 
			
		||||
 | 
			
		||||
from ..base import BaseSolver
 | 
			
		||||
 | 
			
		||||
RockGrid: TypeAlias = list[list[str]]
 | 
			
		||||
 | 
			
		||||
rocks0 = [list(line) for line in sys.stdin.read().splitlines()]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def slide_rocks_top(rocks: RockGrid) -> RockGrid:
 | 
			
		||||
    top = [0 if c == "." else 1 for c in rocks[0]]
 | 
			
		||||
@@ -34,35 +33,38 @@ def cycle(rocks: RockGrid) -> RockGrid:
 | 
			
		||||
    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
 | 
			
		||||
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}")
 | 
			
		||||
        rocks = slide_rocks_top([[c for c in r] for r in rocks0])
 | 
			
		||||
 | 
			
		||||
# part 2
 | 
			
		||||
rocks = rocks0
 | 
			
		||||
        # part 1
 | 
			
		||||
        yield sum(
 | 
			
		||||
            (len(rocks) - i) * sum(1 for c in row if c == "O")
 | 
			
		||||
            for i, row in enumerate(rocks)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
N = 1000000000
 | 
			
		||||
cycles: list[RockGrid] = []
 | 
			
		||||
i_cycle: int = -1
 | 
			
		||||
for i_cycle in range(N):
 | 
			
		||||
    rocks = cycle(rocks)
 | 
			
		||||
        # part 2
 | 
			
		||||
        rocks = rocks0
 | 
			
		||||
 | 
			
		||||
    if any(rocks == c for c in cycles):
 | 
			
		||||
        break
 | 
			
		||||
        N = 1000000000
 | 
			
		||||
        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]))
 | 
			
		||||
cycle_length = i_cycle - cycle_start
 | 
			
		||||
            cycles.append([[c for c in r] for r in rocks])
 | 
			
		||||
 | 
			
		||||
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(
 | 
			
		||||
    (len(rocks) - i) * sum(1 for c in row if c == "O")
 | 
			
		||||
    for i, row in enumerate(cycles[ci])
 | 
			
		||||
)
 | 
			
		||||
print(f"answer 2 is {answer_2}")
 | 
			
		||||
        ci = cycle_start + (N - cycle_start) % cycle_length - 1
 | 
			
		||||
 | 
			
		||||
        yield sum(
 | 
			
		||||
            (len(rocks) - i) * sum(1 for c in row if c == "O")
 | 
			
		||||
            for i, row in enumerate(cycles[ci])
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -1,31 +1,33 @@
 | 
			
		||||
import sys
 | 
			
		||||
from functools import reduce
 | 
			
		||||
from typing import Any, Iterator
 | 
			
		||||
 | 
			
		||||
steps = sys.stdin.read().strip().split(",")
 | 
			
		||||
from ..base import BaseSolver
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _hash(s: str) -> int:
 | 
			
		||||
    return reduce(lambda v, u: ((v + ord(u)) * 17) % 256, s, 0)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# part 1
 | 
			
		||||
answer_1 = sum(map(_hash, steps))
 | 
			
		||||
print(f"answer 1 is {answer_1}")
 | 
			
		||||
class Solver(BaseSolver):
 | 
			
		||||
    def solve(self, input: str) -> Iterator[Any]:
 | 
			
		||||
        steps = input.split(",")
 | 
			
		||||
 | 
			
		||||
# part 2
 | 
			
		||||
boxes: list[dict[str, int]] = [{} for _ in range(256)]
 | 
			
		||||
        # part 1
 | 
			
		||||
        yield sum(map(_hash, steps))
 | 
			
		||||
 | 
			
		||||
for step in steps:
 | 
			
		||||
    if (i := step.find("=")) >= 0:
 | 
			
		||||
        label, length = step[:i], int(step[i + 1 :])
 | 
			
		||||
        boxes[_hash(label)][label] = length
 | 
			
		||||
    else:
 | 
			
		||||
        label = step[:-1]
 | 
			
		||||
        boxes[_hash(label)].pop(label, None)
 | 
			
		||||
        # part 2
 | 
			
		||||
        boxes: list[dict[str, int]] = [{} for _ in range(256)]
 | 
			
		||||
 | 
			
		||||
answer_2 = 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)
 | 
			
		||||
)
 | 
			
		||||
print(f"answer 2 is {answer_2}")
 | 
			
		||||
        for step in steps:
 | 
			
		||||
            if (i := step.find("=")) >= 0:
 | 
			
		||||
                label, length = step[:i], int(step[i + 1 :])
 | 
			
		||||
                boxes[_hash(label)][label] = length
 | 
			
		||||
            else:
 | 
			
		||||
                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
 | 
			
		||||
import sys
 | 
			
		||||
from typing import Literal, TypeAlias, cast
 | 
			
		||||
from typing import Any, Iterator, Literal, TypeAlias, cast
 | 
			
		||||
 | 
			
		||||
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
 | 
			
		||||
from ..base import BaseSolver
 | 
			
		||||
 | 
			
		||||
CellType: TypeAlias = Literal[".", "|", "-", "\\", "/"]
 | 
			
		||||
Direction: TypeAlias = Literal["R", "L", "U", "D"]
 | 
			
		||||
@@ -78,33 +76,33 @@ def propagate(
 | 
			
		||||
    return beams
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
layout: list[list[CellType]] = [
 | 
			
		||||
    [cast(CellType, col) for col in row] for row in sys.stdin.read().splitlines()
 | 
			
		||||
]
 | 
			
		||||
class Solver(BaseSolver):
 | 
			
		||||
    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:
 | 
			
		||||
    print("\n".join(["".join("#" if col else "." for col in row) for row in beams]))
 | 
			
		||||
        # part 1
 | 
			
		||||
        yield sum(sum(map(bool, row)) for row in beams)
 | 
			
		||||
 | 
			
		||||
# part 1
 | 
			
		||||
answer_1 = sum(sum(map(bool, row)) for row in beams)
 | 
			
		||||
print(f"answer 1 is {answer_1}")
 | 
			
		||||
        # part 2
 | 
			
		||||
        n_rows, n_cols = len(layout), len(layout[0])
 | 
			
		||||
        cases: list[tuple[tuple[int, int], Direction]] = []
 | 
			
		||||
 | 
			
		||||
# part 2
 | 
			
		||||
n_rows, n_cols = len(layout), len(layout[0])
 | 
			
		||||
cases: list[tuple[tuple[int, int], Direction]] = []
 | 
			
		||||
        for row in range(n_rows):
 | 
			
		||||
            cases.append(((row, 0), "R"))
 | 
			
		||||
            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):
 | 
			
		||||
    cases.append(((row, 0), "R"))
 | 
			
		||||
    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"))
 | 
			
		||||
 | 
			
		||||
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}")
 | 
			
		||||
        yield max(
 | 
			
		||||
            sum(sum(map(bool, row)) for row in propagate(layout, start, direction))
 | 
			
		||||
            for start, direction in cases
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,11 @@
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import heapq
 | 
			
		||||
import os
 | 
			
		||||
import sys
 | 
			
		||||
from collections import defaultdict
 | 
			
		||||
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"]
 | 
			
		||||
 | 
			
		||||
@@ -32,202 +30,204 @@ MAPPINGS: dict[Direction, tuple[int, int, Direction]] = {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def print_shortest_path(
 | 
			
		||||
    grid: list[list[int]],
 | 
			
		||||
    target: tuple[int, int],
 | 
			
		||||
    per_cell: dict[tuple[int, int], list[tuple[Label, int]]],
 | 
			
		||||
):
 | 
			
		||||
    assert len(per_cell[target]) == 1
 | 
			
		||||
    label = per_cell[target][0][0]
 | 
			
		||||
class Solver(BaseSolver):
 | 
			
		||||
    def print_shortest_path(
 | 
			
		||||
        self,
 | 
			
		||||
        grid: list[list[int]],
 | 
			
		||||
        target: tuple[int, int],
 | 
			
		||||
        per_cell: dict[tuple[int, int], list[tuple[Label, int]]],
 | 
			
		||||
    ):
 | 
			
		||||
        assert len(per_cell[target]) == 1
 | 
			
		||||
        label = per_cell[target][0][0]
 | 
			
		||||
 | 
			
		||||
    path: list[Label] = []
 | 
			
		||||
    while True:
 | 
			
		||||
        path.insert(0, label)
 | 
			
		||||
        if label.parent is None:
 | 
			
		||||
            break
 | 
			
		||||
        label = label.parent
 | 
			
		||||
        path: list[Label] = []
 | 
			
		||||
        while True:
 | 
			
		||||
            path.insert(0, label)
 | 
			
		||||
            if label.parent is None:
 | 
			
		||||
                break
 | 
			
		||||
            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 j in range(len(grid[0])):
 | 
			
		||||
            if per_cell[i, j]:
 | 
			
		||||
                p_grid[i][j] = f"\033[94m{grid[i][j]}\033[0m"
 | 
			
		||||
        for i in range(len(grid)):
 | 
			
		||||
            for j in range(len(grid[0])):
 | 
			
		||||
                if per_cell[i, j]:
 | 
			
		||||
                    p_grid[i][j] = f"\033[94m{grid[i][j]}\033[0m"
 | 
			
		||||
 | 
			
		||||
    prev_label = path[0]
 | 
			
		||||
    for label in path[1:]:
 | 
			
		||||
        for r in range(
 | 
			
		||||
            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,
 | 
			
		||||
        prev_label = path[0]
 | 
			
		||||
        for label in path[1:]:
 | 
			
		||||
            for r in range(
 | 
			
		||||
                min(prev_label.row, label.row), max(prev_label.row, label.row) + 1
 | 
			
		||||
            ):
 | 
			
		||||
                if (r, c) != (prev_label.row, prev_label.col):
 | 
			
		||||
                    p_grid[r][c] = f"\033[93m{grid[r][c]}\033[0m"
 | 
			
		||||
                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):
 | 
			
		||||
                        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]:
 | 
			
		||||
    n_rows, n_cols = len(grid), len(grid[0])
 | 
			
		||||
        visited: dict[tuple[int, int], tuple[Label, int]] = {}
 | 
			
		||||
 | 
			
		||||
    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]] = [
 | 
			
		||||
        (0, Label(row=n_rows - 1, col=n_cols - 1, direction="^", count=0))
 | 
			
		||||
    ]
 | 
			
		||||
        while queue and len(visited) != n_rows * n_cols:
 | 
			
		||||
            distance, label = heapq.heappop(queue)
 | 
			
		||||
 | 
			
		||||
    while queue and len(visited) != n_rows * n_cols:
 | 
			
		||||
        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):
 | 
			
		||||
            if (label.row, label.col) in visited:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            heapq.heappush(
 | 
			
		||||
                queue,
 | 
			
		||||
                (
 | 
			
		||||
            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
 | 
			
		||||
 | 
			
		||||
                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
 | 
			
		||||
                    + 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,
 | 
			
		||||
                    - grid[label.row][label.col]
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                heapq.heappush(
 | 
			
		||||
                    queue,
 | 
			
		||||
                    (
 | 
			
		||||
                        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
 | 
			
		||||
            elif label.count == max_straight:
 | 
			
		||||
                continue
 | 
			
		||||
        if self.verbose:
 | 
			
		||||
            self.print_shortest_path(grid, target, per_cell)
 | 
			
		||||
 | 
			
		||||
            # 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
 | 
			
		||||
        return per_cell[target][0][1]
 | 
			
		||||
 | 
			
		||||
            distance_to = (
 | 
			
		||||
                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[label.row][label.col]
 | 
			
		||||
            )
 | 
			
		||||
    def solve(self, input: str) -> Iterator[Any]:
 | 
			
		||||
        data = [[int(c) for c in r] for r in input.splitlines()]
 | 
			
		||||
        estimates = self.shortest_many_paths(data)
 | 
			
		||||
 | 
			
		||||
            heapq.heappush(
 | 
			
		||||
                queue,
 | 
			
		||||
                (
 | 
			
		||||
                    distance_to + lower_bounds[row, col],
 | 
			
		||||
                    distance_to,
 | 
			
		||||
                    Label(
 | 
			
		||||
                        row=row,
 | 
			
		||||
                        col=col,
 | 
			
		||||
                        direction=direction,
 | 
			
		||||
                        count=count,
 | 
			
		||||
                        parent=label,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
        # part 1
 | 
			
		||||
        yield self.shortest_path(data, 1, 3, lower_bounds=estimates)
 | 
			
		||||
 | 
			
		||||
    if VERBOSE:
 | 
			
		||||
        print_shortest_path(grid, target, per_cell)
 | 
			
		||||
 | 
			
		||||
    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}")
 | 
			
		||||
        # part 2
 | 
			
		||||
        yield self.shortest_path(data, 4, 10, lower_bounds=estimates)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import sys
 | 
			
		||||
from typing import Literal, TypeAlias, cast
 | 
			
		||||
from typing import Any, Iterator, Literal, TypeAlias, cast
 | 
			
		||||
 | 
			
		||||
from ..base import BaseSolver
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
answer_1 = area(
 | 
			
		||||
    *polygon([(cast(Direction, (p := line.split())[0]), int(p[1])) for line in lines])
 | 
			
		||||
)
 | 
			
		||||
print(f"answer 1 is {answer_1}")
 | 
			
		||||
 | 
			
		||||
# 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}")
 | 
			
		||||
        # part 2
 | 
			
		||||
        yield area(
 | 
			
		||||
            *polygon(
 | 
			
		||||
                [
 | 
			
		||||
                    (DIRECTIONS[int((h := line.split()[-1])[-2])], int(h[2:-2], 16))
 | 
			
		||||
                    for line in lines
 | 
			
		||||
                ]
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,8 @@
 | 
			
		||||
import logging
 | 
			
		||||
import operator
 | 
			
		||||
import os
 | 
			
		||||
import sys
 | 
			
		||||
from math import prod
 | 
			
		||||
from typing import Literal, TypeAlias, cast
 | 
			
		||||
from typing import Any, Iterator, Literal, TypeAlias, cast
 | 
			
		||||
 | 
			
		||||
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
 | 
			
		||||
 | 
			
		||||
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
 | 
			
		||||
from ..base import BaseSolver
 | 
			
		||||
 | 
			
		||||
Category: TypeAlias = Literal["x", "m", "a", "s"]
 | 
			
		||||
Part: TypeAlias = dict[Category, int]
 | 
			
		||||
@@ -22,119 +17,118 @@ Check: TypeAlias = tuple[Category, Literal["<", ">"], int] | None
 | 
			
		||||
Workflow: TypeAlias = list[tuple[Check, str]]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def accept(workflows: dict[str, Workflow], part: Part) -> bool:
 | 
			
		||||
    workflow = "in"
 | 
			
		||||
    decision: bool | None = None
 | 
			
		||||
class Solver(BaseSolver):
 | 
			
		||||
    def accept(self, workflows: dict[str, Workflow], part: Part) -> bool:
 | 
			
		||||
        workflow = "in"
 | 
			
		||||
        decision: bool | None = None
 | 
			
		||||
 | 
			
		||||
    while decision is None:
 | 
			
		||||
        for check, target in workflows[workflow]:
 | 
			
		||||
            passed = check is None
 | 
			
		||||
            if check is not None:
 | 
			
		||||
                category, sense, value = check
 | 
			
		||||
                passed = OPERATORS[sense](part[category], value)
 | 
			
		||||
        while decision is None:
 | 
			
		||||
            for check, target in workflows[workflow]:
 | 
			
		||||
                passed = check is None
 | 
			
		||||
                if check is not None:
 | 
			
		||||
                    category, sense, value = check
 | 
			
		||||
                    passed = OPERATORS[sense](part[category], value)
 | 
			
		||||
 | 
			
		||||
            if passed:
 | 
			
		||||
                if target in workflows:
 | 
			
		||||
                    workflow = target
 | 
			
		||||
                else:
 | 
			
		||||
                    decision = target == "A"
 | 
			
		||||
                break
 | 
			
		||||
                if passed:
 | 
			
		||||
                    if target in workflows:
 | 
			
		||||
                        workflow = target
 | 
			
		||||
                    else:
 | 
			
		||||
                        decision = target == "A"
 | 
			
		||||
                    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 _fmt(meta: PartWithBounds) -> str:
 | 
			
		||||
        return "{" + ", ".join(f"{k}={v}" for k, v in meta.items()) + "}"
 | 
			
		||||
 | 
			
		||||
    def transfer_or_accept(
 | 
			
		||||
        target: str, meta: PartWithBounds, queue: list[tuple[PartWithBounds, str]]
 | 
			
		||||
    ) -> int:
 | 
			
		||||
        count = 0
 | 
			
		||||
        if target in workflows:
 | 
			
		||||
            logging.info(f"    transfer to {target}")
 | 
			
		||||
            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)
 | 
			
		||||
        def transfer_or_accept(
 | 
			
		||||
            target: str, meta: PartWithBounds, queue: list[tuple[PartWithBounds, str]]
 | 
			
		||||
        ) -> int:
 | 
			
		||||
            count = 0
 | 
			
		||||
            if target in workflows:
 | 
			
		||||
                self.logger.info(f"    transfer to {target}")
 | 
			
		||||
                queue.append((meta, target))
 | 
			
		||||
            elif target == "A":
 | 
			
		||||
                count = prod((high - low + 1) for low, high in meta.values())
 | 
			
		||||
                self.logger.info(f"    accepted ({count})")
 | 
			
		||||
            else:
 | 
			
		||||
                meta[category], meta2[category] = (low, value), (value + 1, high)
 | 
			
		||||
            logging.info(f"    split {_fmt(meta2)} ({target}), {_fmt(meta)}")
 | 
			
		||||
                self.logger.info("    rejected")
 | 
			
		||||
            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")
 | 
			
		||||
    return accepted
 | 
			
		||||
        n_iterations = 0
 | 
			
		||||
 | 
			
		||||
        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] = {}
 | 
			
		||||
for workflow_s in workflows_s.split("\n"):
 | 
			
		||||
    name, block_s = workflow_s.split("{")
 | 
			
		||||
    workflows[name] = []
 | 
			
		||||
                self.logger.info(
 | 
			
		||||
                    f"  checking {_fmt(meta)} against {category} {sense} {value}"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
    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))
 | 
			
		||||
                if not op(bounds[0], value) and not op(bounds[1], value):
 | 
			
		||||
                    self.logger.info("    reject, always false")
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
# 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")
 | 
			
		||||
]
 | 
			
		||||
answer_1 = sum(sum(part.values()) for part in parts if accept(workflows, part))
 | 
			
		||||
print(f"answer 1 is {answer_1}")
 | 
			
		||||
                if op(meta[category][0], value) and op(meta[category][1], value):
 | 
			
		||||
                    self.logger.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:
 | 
			
		||||
                    meta[category], meta2[category] = (low, value), (value + 1, high)
 | 
			
		||||
                self.logger.info(f"    split {_fmt(meta2)} ({target}), {_fmt(meta)}")
 | 
			
		||||
 | 
			
		||||
# part 2
 | 
			
		||||
answer_2 = propagate(
 | 
			
		||||
    workflows, {cast(Category, c): (1, 4000) for c in ["x", "m", "a", "s"]}
 | 
			
		||||
)
 | 
			
		||||
print(f"answer 2 is {answer_2}")
 | 
			
		||||
                accepted += transfer_or_accept(target, meta2, queue)
 | 
			
		||||
 | 
			
		||||
        self.logger.info(f"run took {n_iterations} iterations")
 | 
			
		||||
        return accepted
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
from collections import defaultdict
 | 
			
		||||
from math import lcm
 | 
			
		||||
from typing import Literal, TypeAlias
 | 
			
		||||
 | 
			
		||||
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
 | 
			
		||||
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
 | 
			
		||||
from typing import Any, Iterator, Literal, TypeAlias
 | 
			
		||||
 | 
			
		||||
from ..base import BaseSolver
 | 
			
		||||
 | 
			
		||||
ModuleType: TypeAlias = Literal["broadcaster", "conjunction", "flip-flop"]
 | 
			
		||||
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:
 | 
			
		||||
    name, outputs_s = line.split(" -> ")
 | 
			
		||||
    outputs = outputs_s.split(", ")
 | 
			
		||||
    if name == "broadcaster":
 | 
			
		||||
        modules["broadcaster"] = ("broadcaster", outputs)
 | 
			
		||||
    else:
 | 
			
		||||
        modules[name[1:]] = (
 | 
			
		||||
            "conjunction" if name.startswith("&") else "flip-flop",
 | 
			
		||||
            outputs,
 | 
			
		||||
    def _process(
 | 
			
		||||
        self,
 | 
			
		||||
        start: tuple[str, str, PulseType],
 | 
			
		||||
        flip_flop_states: dict[str, Literal["on", "off"]],
 | 
			
		||||
        conjunction_states: dict[str, dict[str, PulseType]],
 | 
			
		||||
    ) -> tuple[dict[PulseType, int], dict[str, dict[PulseType, int]]]:
 | 
			
		||||
        pulses: list[tuple[str, str, PulseType]] = [start]
 | 
			
		||||
        counts: dict[PulseType, int] = {"low": 0, "high": 0}
 | 
			
		||||
        inputs: dict[str, dict[PulseType, int]] = defaultdict(
 | 
			
		||||
            lambda: {"low": 0, "high": 0}
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.logger.info("starting process... ")
 | 
			
		||||
 | 
			
		||||
def process(
 | 
			
		||||
    start: tuple[str, str, PulseType],
 | 
			
		||||
    flip_flop_states: dict[str, Literal["on", "off"]],
 | 
			
		||||
    conjunction_states: dict[str, dict[str, PulseType]],
 | 
			
		||||
) -> tuple[dict[PulseType, int], dict[str, dict[PulseType, int]]]:
 | 
			
		||||
    pulses: list[tuple[str, str, PulseType]] = [start]
 | 
			
		||||
    counts: dict[PulseType, int] = {"low": 0, "high": 0}
 | 
			
		||||
    inputs: dict[str, dict[PulseType, int]] = defaultdict(lambda: {"low": 0, "high": 0})
 | 
			
		||||
        while pulses:
 | 
			
		||||
            input, name, pulse = pulses.pop(0)
 | 
			
		||||
            self.logger.info(f"{input} -{pulse}-> {name}")
 | 
			
		||||
            counts[pulse] += 1
 | 
			
		||||
 | 
			
		||||
    logging.info("starting process... ")
 | 
			
		||||
            inputs[name][pulse] += 1
 | 
			
		||||
 | 
			
		||||
    while pulses:
 | 
			
		||||
        input, name, pulse = pulses.pop(0)
 | 
			
		||||
        logging.info(f"{input} -{pulse}-> {name}")
 | 
			
		||||
        counts[pulse] += 1
 | 
			
		||||
 | 
			
		||||
        inputs[name][pulse] += 1
 | 
			
		||||
 | 
			
		||||
        if name not in modules:
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        type, outputs = modules[name]
 | 
			
		||||
 | 
			
		||||
        if type == "broadcaster":
 | 
			
		||||
            ...
 | 
			
		||||
 | 
			
		||||
        elif type == "flip-flop":
 | 
			
		||||
            if pulse == "high":
 | 
			
		||||
            if name not in self._modules:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            if flip_flop_states[name] == "off":
 | 
			
		||||
                flip_flop_states[name] = "on"
 | 
			
		||||
                pulse = "high"
 | 
			
		||||
            type, outputs = self._modules[name]
 | 
			
		||||
 | 
			
		||||
            if type == "broadcaster":
 | 
			
		||||
                ...
 | 
			
		||||
 | 
			
		||||
            elif type == "flip-flop":
 | 
			
		||||
                if pulse == "high":
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                if flip_flop_states[name] == "off":
 | 
			
		||||
                    flip_flop_states[name] = "on"
 | 
			
		||||
                    pulse = "high"
 | 
			
		||||
                else:
 | 
			
		||||
                    flip_flop_states[name] = "off"
 | 
			
		||||
                    pulse = "low"
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
                flip_flop_states[name] = "off"
 | 
			
		||||
                pulse = "low"
 | 
			
		||||
                conjunction_states[name][input] = pulse
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            conjunction_states[name][input] = pulse
 | 
			
		||||
                if all(state == "high" for state in conjunction_states[name].values()):
 | 
			
		||||
                    pulse = "low"
 | 
			
		||||
                else:
 | 
			
		||||
                    pulse = "high"
 | 
			
		||||
 | 
			
		||||
            if all(state == "high" for state in conjunction_states[name].values()):
 | 
			
		||||
                pulse = "low"
 | 
			
		||||
            pulses.extend((name, output, pulse) for output in outputs)
 | 
			
		||||
 | 
			
		||||
        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:
 | 
			
		||||
                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:
 | 
			
		||||
    fp.write("digraph G {\n")
 | 
			
		||||
    fp.write("rx [shape=circle, color=red, style=filled];\n")
 | 
			
		||||
    for name, (type, outputs) in modules.items():
 | 
			
		||||
        if type == "conjunction":
 | 
			
		||||
            shape = "diamond"
 | 
			
		||||
        elif type == "flip-flop":
 | 
			
		||||
            shape = "box"
 | 
			
		||||
        else:
 | 
			
		||||
            shape = "circle"
 | 
			
		||||
        fp.write(f"{name} [shape={shape}];\n")
 | 
			
		||||
    for name, (type, outputs) in modules.items():
 | 
			
		||||
        for output in outputs:
 | 
			
		||||
            fp.write(f"{name} -> {output};\n")
 | 
			
		||||
    fp.write("}\n")
 | 
			
		||||
        # reset states
 | 
			
		||||
        for name in flip_flop_states:
 | 
			
		||||
            flip_flop_states[name] = "off"
 | 
			
		||||
 | 
			
		||||
# part 1
 | 
			
		||||
flip_flop_states: dict[str, Literal["on", "off"]] = {
 | 
			
		||||
    name: "off" for name, (type, _) in modules.items() if type == "flip-flop"
 | 
			
		||||
}
 | 
			
		||||
conjunction_states: dict[str, dict[str, PulseType]] = {
 | 
			
		||||
    name: {input: "low" for input, (_, outputs) in modules.items() if name in outputs}
 | 
			
		||||
    for name, (type, _) in modules.items()
 | 
			
		||||
    if type == "conjunction"
 | 
			
		||||
}
 | 
			
		||||
counts: dict[PulseType, int] = {"low": 0, "high": 0}
 | 
			
		||||
for _ in range(1000):
 | 
			
		||||
    result, _ = process(
 | 
			
		||||
        ("button", "broadcaster", "low"), flip_flop_states, conjunction_states
 | 
			
		||||
    )
 | 
			
		||||
    for pulse in ("low", "high"):
 | 
			
		||||
        counts[pulse] += result[pulse]
 | 
			
		||||
answer_1 = counts["low"] * counts["high"]
 | 
			
		||||
print(f"answer 1 is {answer_1}")
 | 
			
		||||
        for name in conjunction_states:
 | 
			
		||||
            for input in conjunction_states[name]:
 | 
			
		||||
                conjunction_states[name][input] = "low"
 | 
			
		||||
 | 
			
		||||
# 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
 | 
			
		||||
for name in flip_flop_states:
 | 
			
		||||
    flip_flop_states[name] = "off"
 | 
			
		||||
        to_rx_inputs = [
 | 
			
		||||
            name for name, (_, outputs) in self._modules.items() if to_rx[0] in outputs
 | 
			
		||||
        ]
 | 
			
		||||
        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:
 | 
			
		||||
    for input in conjunction_states[name]:
 | 
			
		||||
        conjunction_states[name][input] = "low"
 | 
			
		||||
        count = 1
 | 
			
		||||
        cycles: dict[str, int] = {}
 | 
			
		||||
        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
 | 
			
		||||
to_rx = [name for name, (_, outputs) in modules.items() if "rx" in outputs]
 | 
			
		||||
assert len(to_rx) == 1, "cannot handle multiple module inputs for rx"
 | 
			
		||||
assert (
 | 
			
		||||
    modules[to_rx[0]][0] == "conjunction"
 | 
			
		||||
), "can only handle conjunction as input to rx"
 | 
			
		||||
            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
 | 
			
		||||
 | 
			
		||||
to_rx_inputs = [name for name, (_, outputs) in modules.items() if to_rx[0] in outputs]
 | 
			
		||||
assert all(
 | 
			
		||||
    modules[i][0] == "conjunction" and len(modules[i][1]) == 1 for i in to_rx_inputs
 | 
			
		||||
), "can only handle inversion as second-order inputs to rx"
 | 
			
		||||
            count += 1
 | 
			
		||||
 | 
			
		||||
        assert all(
 | 
			
		||||
            second[k] == cycles[k] * 2 for k in to_rx_inputs
 | 
			
		||||
        ), "cannot only handle cycles starting at the beginning"
 | 
			
		||||
 | 
			
		||||
count = 1
 | 
			
		||||
cycles: dict[str, int] = {}
 | 
			
		||||
second: dict[str, int] = {}
 | 
			
		||||
while len(second) != len(to_rx_inputs):
 | 
			
		||||
    _, inputs = process(
 | 
			
		||||
        ("button", "broadcaster", "low"), flip_flop_states, conjunction_states
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    for node in to_rx_inputs:
 | 
			
		||||
        if inputs[node]["low"] == 1:
 | 
			
		||||
            if node not in cycles:
 | 
			
		||||
                cycles[node] = count
 | 
			
		||||
            elif node not in second:
 | 
			
		||||
                second[node] = count
 | 
			
		||||
 | 
			
		||||
    count += 1
 | 
			
		||||
 | 
			
		||||
assert all(
 | 
			
		||||
    second[k] == cycles[k] * 2 for k in to_rx_inputs
 | 
			
		||||
), "cannot only handle cycles starting at the beginning"
 | 
			
		||||
 | 
			
		||||
answer_2 = lcm(*cycles.values())
 | 
			
		||||
print(f"answer 2 is {answer_2}")
 | 
			
		||||
        yield lcm(*cycles.values())
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,6 @@
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
import sys
 | 
			
		||||
from typing import Any, Iterator
 | 
			
		||||
 | 
			
		||||
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
 | 
			
		||||
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
 | 
			
		||||
from ..base import BaseSolver
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def reachable(
 | 
			
		||||
@@ -21,129 +18,133 @@ def reachable(
 | 
			
		||||
    return tiles
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
map = sys.stdin.read().splitlines()
 | 
			
		||||
start = next(
 | 
			
		||||
    (i, j) for i in range(len(map)) for j in range(len(map[i])) if map[i][j] == "S"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
# part 1
 | 
			
		||||
answer_1 = len(reachable(map, {start}, 6 if len(map) < 20 else 64))
 | 
			
		||||
print(f"answer 1 is {answer_1}")
 | 
			
		||||
 | 
			
		||||
# part 2
 | 
			
		||||
 | 
			
		||||
# the initial map is a square and contains an empty rhombus whose diameter is the size
 | 
			
		||||
# of the map, and has only empty cells around the middle row and column
 | 
			
		||||
#
 | 
			
		||||
# after ~n/2 steps, the first map is filled with a rhombus, after that we get a bigger
 | 
			
		||||
# rhombus every n steps
 | 
			
		||||
#
 | 
			
		||||
# we are going to find the number of cells reached for the initial rhombus, n steps
 | 
			
		||||
# after and n * 2 steps after
 | 
			
		||||
#
 | 
			
		||||
cycle = len(map)
 | 
			
		||||
rhombus = (len(map) - 3) // 2 + 1
 | 
			
		||||
 | 
			
		||||
values: list[int] = []
 | 
			
		||||
values.append(len(tiles := reachable(map, {start}, rhombus)))
 | 
			
		||||
values.append(len(tiles := reachable(map, tiles, cycle)))
 | 
			
		||||
values.append(len(tiles := reachable(map, tiles, cycle)))
 | 
			
		||||
 | 
			
		||||
if logging.root.getEffectiveLevel() == logging.INFO:
 | 
			
		||||
    n_rows, n_cols = len(map), len(map[0])
 | 
			
		||||
 | 
			
		||||
    rows = [
 | 
			
		||||
        [
 | 
			
		||||
            map[i % n_rows][j % n_cols] if (i, j) not in tiles else "O"
 | 
			
		||||
            for j in range(-2 * cycle, 3 * cycle)
 | 
			
		||||
        ]
 | 
			
		||||
        for i in range(-2 * cycle, 3 * cycle)
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    for i in range(len(rows)):
 | 
			
		||||
        for j in range(len(rows[i])):
 | 
			
		||||
            if (i // cycle) % 2 == (j // cycle) % 2:
 | 
			
		||||
                rows[i][j] = f"\033[91m{rows[i][j]}\033[0m"
 | 
			
		||||
 | 
			
		||||
    print("\n".join("".join(row) for row in rows))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
logging.info(f"values to fit: {values}")
 | 
			
		||||
 | 
			
		||||
# version 1:
 | 
			
		||||
#
 | 
			
		||||
# after 3 cycles, the figure looks like the following:
 | 
			
		||||
#
 | 
			
		||||
#   I M D
 | 
			
		||||
# I J A K D
 | 
			
		||||
# H A F A L
 | 
			
		||||
# C E A K B
 | 
			
		||||
#   C G B
 | 
			
		||||
#
 | 
			
		||||
# after 4 cycles, the figure looks like the following:
 | 
			
		||||
#
 | 
			
		||||
#     I M D
 | 
			
		||||
#   I J A K D
 | 
			
		||||
# I J A B A K D
 | 
			
		||||
# H A B A B A L
 | 
			
		||||
# C E A B A N F
 | 
			
		||||
#   C E A N F
 | 
			
		||||
#     C G F
 | 
			
		||||
#
 | 
			
		||||
# the 'radius' of the rhombus is the number of cycles minus 1
 | 
			
		||||
#
 | 
			
		||||
# the 4 'corner' (M, H, L, G) are counted once, the blocks with a corner triangle (D, I,
 | 
			
		||||
# C, B) are each counted radius times, the blocks with everything but one corner (J, K,
 | 
			
		||||
# E, N) are each counted radius - 1 times
 | 
			
		||||
#
 | 
			
		||||
# there are two versions of the whole block, A and B in the above (or odd and even),
 | 
			
		||||
# depending on the number of cycles, either A or B will be in the center
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
counts = [
 | 
			
		||||
    [
 | 
			
		||||
        sum(
 | 
			
		||||
            (i, j) in tiles
 | 
			
		||||
            for i in range(ci * cycle, (ci + 1) * cycle)
 | 
			
		||||
            for j in range(cj * cycle, (cj + 1) * cycle)
 | 
			
		||||
class Solver(BaseSolver):
 | 
			
		||||
    def solve(self, input: str) -> Iterator[Any]:
 | 
			
		||||
        map = input.splitlines()
 | 
			
		||||
        start = next(
 | 
			
		||||
            (i, j)
 | 
			
		||||
            for i in range(len(map))
 | 
			
		||||
            for j in range(len(map[i]))
 | 
			
		||||
            if map[i][j] == "S"
 | 
			
		||||
        )
 | 
			
		||||
        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}")
 | 
			
		||||
        # part 1
 | 
			
		||||
        yield len(reachable(map, {start}, 6 if len(map) < 20 else 64))
 | 
			
		||||
 | 
			
		||||
# 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
 | 
			
		||||
        # part 2
 | 
			
		||||
 | 
			
		||||
n = (26501365 - rhombus) // cycle
 | 
			
		||||
answer_2 = a * n * n + b * n + c
 | 
			
		||||
print(f"answer 2 (v2) is {answer_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 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 logging
 | 
			
		||||
import os
 | 
			
		||||
import string
 | 
			
		||||
import sys
 | 
			
		||||
from collections import defaultdict
 | 
			
		||||
from typing import Any, Iterator
 | 
			
		||||
 | 
			
		||||
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
 | 
			
		||||
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
 | 
			
		||||
from ..base import BaseSolver
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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:
 | 
			
		||||
    if len(lines) < 26:
 | 
			
		||||
        return string.ascii_uppercase[i]
 | 
			
		||||
    return f"B{i:04d}"
 | 
			
		||||
        def build_supports(
 | 
			
		||||
            bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]],
 | 
			
		||||
        ) -> tuple[dict[int, set[int]], dict[int, set[int]]]:
 | 
			
		||||
            # 1. compute locations where a brick of sand will land after falling by processing
 | 
			
		||||
            # them in sorted order of bottom z location
 | 
			
		||||
            levels: dict[tuple[int, int, int], int] = defaultdict(lambda: -1)
 | 
			
		||||
            for i_brick, ((sx, sy, sz), (ex, ey, ez)) in enumerate(bricks):
 | 
			
		||||
                assert sx <= ex and sy <= ey and sz <= ez
 | 
			
		||||
 | 
			
		||||
                xs, ys = range(sx, ex + 1), range(sy, ey + 1)
 | 
			
		||||
 | 
			
		||||
def build_supports(
 | 
			
		||||
    bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]],
 | 
			
		||||
) -> tuple[dict[int, set[int]], dict[int, set[int]]]:
 | 
			
		||||
    # 1. compute locations where a brick of sand will land after falling by processing
 | 
			
		||||
    # them in sorted order of bottom z location
 | 
			
		||||
    levels: dict[tuple[int, int, int], int] = defaultdict(lambda: -1)
 | 
			
		||||
    for i_brick, ((sx, sy, sz), (ex, ey, ez)) in enumerate(bricks):
 | 
			
		||||
        assert sx <= ex and sy <= ey and sz <= ez
 | 
			
		||||
                for z in range(sz - 1, 0, -1):
 | 
			
		||||
                    if any(levels[x, y, z] >= 0 for x, y in itertools.product(xs, ys)):
 | 
			
		||||
                        break
 | 
			
		||||
                    sz, ez = sz - 1, ez - 1
 | 
			
		||||
 | 
			
		||||
        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):
 | 
			
		||||
            if any(levels[x, y, z] >= 0 for x, y in itertools.product(xs, ys)):
 | 
			
		||||
                break
 | 
			
		||||
            sz, ez = sz - 1, ez - 1
 | 
			
		||||
                for x, y, z in itertools.product(xs, ys, zs):
 | 
			
		||||
                    levels[x, y, z] = i_brick
 | 
			
		||||
 | 
			
		||||
        bricks[i_brick] = ((sx, sy, sz), (ex, ey, ez))
 | 
			
		||||
        zs = range(sz, ez + 1)
 | 
			
		||||
            # 2. compute the bricks that supports any brick
 | 
			
		||||
            supported_by: dict[int, set[int]] = {}
 | 
			
		||||
            supports: dict[int, set[int]] = {
 | 
			
		||||
                i_brick: set() for i_brick in range(len(bricks))
 | 
			
		||||
            }
 | 
			
		||||
            for i_brick, ((sx, sy, sz), (ex, ey, ez)) in enumerate(bricks):
 | 
			
		||||
                name = _name(i_brick)
 | 
			
		||||
 | 
			
		||||
        for x, y, z in itertools.product(xs, ys, zs):
 | 
			
		||||
            levels[x, y, z] = i_brick
 | 
			
		||||
                supported_by[i_brick] = {
 | 
			
		||||
                    v
 | 
			
		||||
                    for x, y in itertools.product(range(sx, ex + 1), range(sy, ey + 1))
 | 
			
		||||
                    if (v := levels[x, y, sz - 1]) != -1
 | 
			
		||||
                }
 | 
			
		||||
                self.logger.info(
 | 
			
		||||
                    f"{name} supported by {', '.join(map(_name, supported_by[i_brick]))}"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
    # 2. compute the bricks that supports any brick
 | 
			
		||||
    supported_by: dict[int, set[int]] = {}
 | 
			
		||||
    supports: dict[int, set[int]] = {i_brick: set() for i_brick in range(len(bricks))}
 | 
			
		||||
    for i_brick, ((sx, sy, sz), (ex, ey, ez)) in enumerate(bricks):
 | 
			
		||||
        name = _name(i_brick)
 | 
			
		||||
                for support in supported_by[i_brick]:
 | 
			
		||||
                    supports[support].add(i_brick)
 | 
			
		||||
 | 
			
		||||
        supported_by[i_brick] = {
 | 
			
		||||
            v
 | 
			
		||||
            for x, y in itertools.product(range(sx, ex + 1), range(sy, ey + 1))
 | 
			
		||||
            if (v := levels[x, y, sz - 1]) != -1
 | 
			
		||||
        }
 | 
			
		||||
        logging.info(
 | 
			
		||||
            f"{name} supported by {', '.join(map(_name, supported_by[i_brick]))}"
 | 
			
		||||
            return supported_by, supports
 | 
			
		||||
 | 
			
		||||
        bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]] = []
 | 
			
		||||
        for line in lines:
 | 
			
		||||
            bricks.append(
 | 
			
		||||
                (
 | 
			
		||||
                    tuple(int(c) for c in line.split("~")[0].split(",")),  # type: ignore
 | 
			
		||||
                    tuple(int(c) for c in line.split("~")[1].split(",")),  # type: ignore
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        # sort bricks by bottom z position to compute supports
 | 
			
		||||
        bricks = sorted(bricks, key=lambda b: b[0][-1])
 | 
			
		||||
        supported_by, supports = build_supports(bricks)
 | 
			
		||||
 | 
			
		||||
        # part 1
 | 
			
		||||
        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]:
 | 
			
		||||
            supports[support].add(i_brick)
 | 
			
		||||
        # part 2
 | 
			
		||||
        falling_in_chain: dict[int, set[int]] = {}
 | 
			
		||||
        for i_brick in range(len(bricks)):
 | 
			
		||||
            to_disintegrate: set[int] = {
 | 
			
		||||
                supported
 | 
			
		||||
                for supported in supports[i_brick]
 | 
			
		||||
                if len(supported_by[supported]) == 1
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
    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]]] = []
 | 
			
		||||
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
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
                to_disintegrate_v: set[int] = set()
 | 
			
		||||
 | 
			
		||||
# sort bricks by bottom z position to compute supports
 | 
			
		||||
bricks = sorted(bricks, key=lambda b: b[0][-1])
 | 
			
		||||
supported_by, supports = build_supports(bricks)
 | 
			
		||||
                for d_brick in to_disintegrate:
 | 
			
		||||
                    for supported in supports[d_brick]:
 | 
			
		||||
                        supported_by_copy[supported] = supported_by_copy[supported] - {
 | 
			
		||||
                            d_brick
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
# part 1
 | 
			
		||||
answer_1 = len(bricks) - sum(
 | 
			
		||||
    any(len(supported_by[supported]) == 1 for supported in supports_to)
 | 
			
		||||
    for supports_to in supports.values()
 | 
			
		||||
)
 | 
			
		||||
print(f"answer 1 is {answer_1}")
 | 
			
		||||
                        if not supported_by_copy[supported]:
 | 
			
		||||
                            to_disintegrate_v.add(supported)
 | 
			
		||||
 | 
			
		||||
# part 2
 | 
			
		||||
falling_in_chain: dict[int, set[int]] = {}
 | 
			
		||||
for i_brick in range(len(bricks)):
 | 
			
		||||
    to_disintegrate: set[int] = {
 | 
			
		||||
        supported
 | 
			
		||||
        for supported in supports[i_brick]
 | 
			
		||||
        if len(supported_by[supported]) == 1
 | 
			
		||||
    }
 | 
			
		||||
                to_disintegrate = to_disintegrate_v
 | 
			
		||||
 | 
			
		||||
    supported_by_copy = dict(supported_by)
 | 
			
		||||
 | 
			
		||||
    falling_in_chain[i_brick] = set()
 | 
			
		||||
    while to_disintegrate:
 | 
			
		||||
        falling_in_chain[i_brick].update(to_disintegrate)
 | 
			
		||||
 | 
			
		||||
        to_disintegrate_v: set[int] = set()
 | 
			
		||||
 | 
			
		||||
        for d_brick in to_disintegrate:
 | 
			
		||||
            for supported in supports[d_brick]:
 | 
			
		||||
                supported_by_copy[supported] = supported_by_copy[supported] - {d_brick}
 | 
			
		||||
 | 
			
		||||
                if not supported_by_copy[supported]:
 | 
			
		||||
                    to_disintegrate_v.add(supported)
 | 
			
		||||
 | 
			
		||||
        to_disintegrate = to_disintegrate_v
 | 
			
		||||
 | 
			
		||||
answer_2 = sum(len(falling) for falling in falling_in_chain.values())
 | 
			
		||||
print(f"answer 2 is {answer_2}")
 | 
			
		||||
        yield sum(len(falling) for falling in falling_in_chain.values())
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,7 @@
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
import sys
 | 
			
		||||
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"
 | 
			
		||||
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
 | 
			
		||||
from ..base import BaseSolver
 | 
			
		||||
 | 
			
		||||
DirectionType: TypeAlias = Literal[">", "<", "^", "v", ".", "#"]
 | 
			
		||||
 | 
			
		||||
@@ -35,6 +31,7 @@ def neighbors(
 | 
			
		||||
    Compute neighbors of the given node, ignoring the given set of nodes and considering
 | 
			
		||||
    that you can go uphill on slopes.
 | 
			
		||||
    """
 | 
			
		||||
    n_rows, n_cols = len(grid), len(grid[0])
 | 
			
		||||
    i, j = node
 | 
			
		||||
 | 
			
		||||
    for di, dj in Neighbors[grid[i][j]]:
 | 
			
		||||
@@ -103,65 +100,66 @@ def compute_direct_links(
 | 
			
		||||
    return direct
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def longest_path_length(
 | 
			
		||||
    links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]],
 | 
			
		||||
    start: tuple[int, int],
 | 
			
		||||
    target: tuple[int, int],
 | 
			
		||||
) -> int:
 | 
			
		||||
    max_distance: int = -1
 | 
			
		||||
    queue: list[tuple[tuple[int, int], int, frozenset[tuple[int, int]]]] = [
 | 
			
		||||
        (start, 0, frozenset({start}))
 | 
			
		||||
    ]
 | 
			
		||||
class Solver(BaseSolver):
 | 
			
		||||
    def longest_path_length(
 | 
			
		||||
        self,
 | 
			
		||||
        links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]],
 | 
			
		||||
        start: tuple[int, int],
 | 
			
		||||
        target: tuple[int, int],
 | 
			
		||||
    ) -> int:
 | 
			
		||||
        max_distance: int = -1
 | 
			
		||||
        queue: list[tuple[tuple[int, int], int, frozenset[tuple[int, int]]]] = [
 | 
			
		||||
            (start, 0, frozenset({start}))
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    nodes = 0
 | 
			
		||||
    while queue:
 | 
			
		||||
        node, distance, path = queue.pop()
 | 
			
		||||
        nodes = 0
 | 
			
		||||
        while queue:
 | 
			
		||||
            node, distance, path = queue.pop()
 | 
			
		||||
 | 
			
		||||
        nodes += 1
 | 
			
		||||
            nodes += 1
 | 
			
		||||
 | 
			
		||||
        if node == target:
 | 
			
		||||
            max_distance = max(distance, max_distance)
 | 
			
		||||
            continue
 | 
			
		||||
            if node == target:
 | 
			
		||||
                max_distance = max(distance, max_distance)
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
        queue.extend(
 | 
			
		||||
            (reach, distance + length, path | {reach})
 | 
			
		||||
            for reach, length in links.get(node, [])
 | 
			
		||||
            if reach not in path
 | 
			
		||||
            queue.extend(
 | 
			
		||||
                (reach, distance + length, path | {reach})
 | 
			
		||||
                for reach, length in links.get(node, [])
 | 
			
		||||
                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())
 | 
			
		||||
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}")
 | 
			
		||||
        yield self.longest_path_length(links, start, target)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,63 +1,68 @@
 | 
			
		||||
import sys
 | 
			
		||||
from typing import Any, Iterator
 | 
			
		||||
 | 
			
		||||
import numpy as np
 | 
			
		||||
from sympy import solve, symbols
 | 
			
		||||
 | 
			
		||||
lines = sys.stdin.read().splitlines()
 | 
			
		||||
 | 
			
		||||
positions = np.array(
 | 
			
		||||
    [[int(c) for c in line.split("@")[0].strip().split(", ")] for line in lines]
 | 
			
		||||
)
 | 
			
		||||
velocities = np.array(
 | 
			
		||||
    [[int(c) for c in line.split("@")[1].strip().split(", ")] for line in lines]
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
# part 1
 | 
			
		||||
low, high = [7, 27] if len(positions) <= 10 else [200000000000000, 400000000000000]
 | 
			
		||||
 | 
			
		||||
count = 0
 | 
			
		||||
for i1, (p1, v1) in enumerate(zip(positions, velocities)):
 | 
			
		||||
    p, r = p1[:2], v1[:2]
 | 
			
		||||
 | 
			
		||||
    q, s = positions[i1 + 1 :, :2], velocities[i1 + 1 :, :2]
 | 
			
		||||
 | 
			
		||||
    rs = np.cross(r, s)
 | 
			
		||||
 | 
			
		||||
    q, s, rs = q[m := (rs != 0)], s[m], rs[m]
 | 
			
		||||
    t = np.cross((q - p), s) / rs
 | 
			
		||||
    u = np.cross((q - p), r) / rs
 | 
			
		||||
 | 
			
		||||
    t, u = t[m := ((t >= 0) & (u >= 0))], u[m]
 | 
			
		||||
    c = p + np.expand_dims(t, 1) * r
 | 
			
		||||
    count += np.all((low <= c) & (c <= high), axis=1).sum()
 | 
			
		||||
from ..base import BaseSolver
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
answer_1 = count
 | 
			
		||||
print(f"answer 1 is {answer_1}")
 | 
			
		||||
class Solver(BaseSolver):
 | 
			
		||||
    def solve(self, input: str) -> Iterator[Any]:
 | 
			
		||||
        lines = input.splitlines()
 | 
			
		||||
 | 
			
		||||
# 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
 | 
			
		||||
#
 | 
			
		||||
        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]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
# 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
 | 
			
		||||
        # part 1
 | 
			
		||||
        low, high = (
 | 
			
		||||
            [7, 27] if len(positions) <= 10 else [200000000000000, 400000000000000]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
        count = 0
 | 
			
		||||
        for i1, (p1, v1) in enumerate(zip(positions, velocities)):
 | 
			
		||||
            p, r = p1[:2], v1[:2]
 | 
			
		||||
 | 
			
		||||
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]
 | 
			
		||||
print(f"answer 2 is {answer_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()
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
components = {
 | 
			
		||||
    (p := line.split(": "))[0]: p[1].split() for line in sys.stdin.read().splitlines()
 | 
			
		||||
}
 | 
			
		||||
from ..base import BaseSolver
 | 
			
		||||
 | 
			
		||||
targets = {t for c in components for t in components[c] if t not in components}
 | 
			
		||||
 | 
			
		||||
graph = nx.Graph()
 | 
			
		||||
graph.add_edges_from((u, v) for u, vs in components.items() for v in vs)
 | 
			
		||||
class Solver(BaseSolver):
 | 
			
		||||
    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.remove_edges_from(cut)
 | 
			
		||||
        graph: "nx.Graph[str]" = nx.Graph()
 | 
			
		||||
        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
 | 
			
		||||
answer_1 = len(c1) * len(c2)
 | 
			
		||||
print(f"answer 1 is {answer_1}")
 | 
			
		||||
        c1, c2 = nx.connected_components(graph)
 | 
			
		||||
 | 
			
		||||
# part 2
 | 
			
		||||
answer_2 = ...
 | 
			
		||||
print(f"answer 2 is {answer_2}")
 | 
			
		||||
        # part 1
 | 
			
		||||
        yield len(c1) * len(c2)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
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]:
 | 
			
		||||
@@ -25,23 +27,23 @@ def extreme_times_to_beat(time: int, distance: int) -> tuple[int, int]:
 | 
			
		||||
    return t1, t2
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
lines = sys.stdin.read().splitlines()
 | 
			
		||||
class Solver(BaseSolver):
 | 
			
		||||
    def solve(self, input: str) -> Iterator[Any]:
 | 
			
		||||
        lines = input.splitlines()
 | 
			
		||||
 | 
			
		||||
# part 1
 | 
			
		||||
times = list(map(int, lines[0].split()[1:]))
 | 
			
		||||
distances = list(map(int, lines[1].split()[1:]))
 | 
			
		||||
answer_1 = math.prod(
 | 
			
		||||
    t2 - t1 + 1
 | 
			
		||||
    for t1, t2 in (
 | 
			
		||||
        extreme_times_to_beat(time, distance)
 | 
			
		||||
        for time, distance in zip(times, distances)
 | 
			
		||||
    )
 | 
			
		||||
)
 | 
			
		||||
print(f"answer 1 is {answer_1}")
 | 
			
		||||
        # part 1
 | 
			
		||||
        times = list(map(int, lines[0].split()[1:]))
 | 
			
		||||
        distances = list(map(int, lines[1].split()[1:]))
 | 
			
		||||
        yield math.prod(
 | 
			
		||||
            t2 - t1 + 1
 | 
			
		||||
            for t1, t2 in (
 | 
			
		||||
                extreme_times_to_beat(time, distance)
 | 
			
		||||
                for time, distance in zip(times, distances)
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
# part 2
 | 
			
		||||
time = int(lines[0].split(":")[1].strip().replace(" ", ""))
 | 
			
		||||
distance = int(lines[1].split(":")[1].strip().replace(" ", ""))
 | 
			
		||||
t1, t2 = extreme_times_to_beat(time, distance)
 | 
			
		||||
answer_2 = t2 - t1 + 1
 | 
			
		||||
print(f"answer 2 is {answer_2}")
 | 
			
		||||
        # part 2
 | 
			
		||||
        time = int(lines[0].split(":")[1].strip().replace(" ", ""))
 | 
			
		||||
        distance = int(lines[1].split(":")[1].strip().replace(" ", ""))
 | 
			
		||||
        t1, t2 = extreme_times_to_beat(time, distance)
 | 
			
		||||
        yield t2 - t1 + 1
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
import sys
 | 
			
		||||
from collections import Counter, defaultdict
 | 
			
		||||
from typing import Any, Iterator
 | 
			
		||||
 | 
			
		||||
from ..base import BaseSolver
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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()
 | 
			
		||||
cards = [(t[0], int(t[1])) for line in lines if (t := line.split())]
 | 
			
		||||
class Solver(BaseSolver):
 | 
			
		||||
    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
 | 
			
		||||
values = {card: value for value, card in enumerate("23456789TJQKA")}
 | 
			
		||||
cards.sort(key=lambda cv: extract_key(cv[0], values=values))
 | 
			
		||||
answer_1 = 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}")
 | 
			
		||||
        # part 2
 | 
			
		||||
        values = {card: value for value, card in enumerate("J23456789TQKA")}
 | 
			
		||||
        cards.sort(key=lambda cv: extract_key(cv[0], values=values, joker="J"))
 | 
			
		||||
        yield sum(rank * value for rank, (_, value) in enumerate(cards, start=1))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,29 +1,30 @@
 | 
			
		||||
import itertools
 | 
			
		||||
import math
 | 
			
		||||
import sys
 | 
			
		||||
from typing import Any, Iterator
 | 
			
		||||
 | 
			
		||||
lines = sys.stdin.read().splitlines()
 | 
			
		||||
 | 
			
		||||
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(" = "))
 | 
			
		||||
}
 | 
			
		||||
from ..base import BaseSolver
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def path(start: str):
 | 
			
		||||
    path = [start]
 | 
			
		||||
    it_seq = iter(itertools.cycle(sequence))
 | 
			
		||||
    while not path[-1].endswith("Z"):
 | 
			
		||||
        path.append(nodes[path[-1]][next(it_seq)])
 | 
			
		||||
    return path
 | 
			
		||||
class Solver(BaseSolver):
 | 
			
		||||
    def solve(self, input: str) -> Iterator[Any]:
 | 
			
		||||
        lines = input.splitlines()
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
answer_1 = len(path(next(node for node in nodes if node.endswith("A")))) - 1
 | 
			
		||||
print(f"answer 1 is {answer_1}")
 | 
			
		||||
        def path(start: str):
 | 
			
		||||
            path = [start]
 | 
			
		||||
            it_seq = iter(itertools.cycle(sequence))
 | 
			
		||||
            while not path[-1].endswith("Z"):
 | 
			
		||||
                path.append(nodes[path[-1]][next(it_seq)])
 | 
			
		||||
            return path
 | 
			
		||||
 | 
			
		||||
# part 2
 | 
			
		||||
answer_2 = math.lcm(*(len(path(node)) - 1 for node in nodes if node.endswith("A")))
 | 
			
		||||
print(f"answer 2 is {answer_2}")
 | 
			
		||||
        # part 1
 | 
			
		||||
        yield len(path(next(node for node in nodes if node.endswith("A")))) - 1
 | 
			
		||||
 | 
			
		||||
        # 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] = []
 | 
			
		||||
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:])])
 | 
			
		||||
class Solver(BaseSolver):
 | 
			
		||||
    def solve(self, input: str) -> Iterator[Any]:
 | 
			
		||||
        lines = input.splitlines()
 | 
			
		||||
 | 
			
		||||
    rhs: list[int] = [0]
 | 
			
		||||
    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])
 | 
			
		||||
        data = [[int(c) for c in line.split()] for line in lines]
 | 
			
		||||
 | 
			
		||||
    right_values.append(rhs[-1])
 | 
			
		||||
    left_values.append(lhs[-1])
 | 
			
		||||
        right_values: list[int] = []
 | 
			
		||||
        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
 | 
			
		||||
answer_1 = sum(right_values)
 | 
			
		||||
print(f"answer 1 is {answer_1}")
 | 
			
		||||
            rhs: list[int] = [0]
 | 
			
		||||
            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])
 | 
			
		||||
 | 
			
		||||
# part 2
 | 
			
		||||
answer_2 = sum(left_values)
 | 
			
		||||
print(f"answer 2 is {answer_2}")
 | 
			
		||||
            right_values.append(rhs[-1])
 | 
			
		||||
            left_values.append(lhs[-1])
 | 
			
		||||
 | 
			
		||||
        # part 1
 | 
			
		||||
        yield sum(right_values)
 | 
			
		||||
 | 
			
		||||
        # part 2
 | 
			
		||||
        yield sum(left_values)
 | 
			
		||||
 
 | 
			
		||||
@@ -54,7 +54,7 @@ def main():
 | 
			
		||||
        f".{year}.day{day}", __package__
 | 
			
		||||
    ).Solver
 | 
			
		||||
 | 
			
		||||
    solver = solver_class(logging.getLogger("AOC"), year, day)
 | 
			
		||||
    solver = solver_class(logging.getLogger("AOC"), verbose, year, day)
 | 
			
		||||
 | 
			
		||||
    data: str
 | 
			
		||||
    if stdin:
 | 
			
		||||
 
 | 
			
		||||
@@ -4,10 +4,14 @@ from typing import Any, Final, Iterator
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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.verbose: Final = verbose
 | 
			
		||||
        self.year: Final = year
 | 
			
		||||
        self.day: Final = day
 | 
			
		||||
        self.outputs = outputs
 | 
			
		||||
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def solve(self, input: str) -> Iterator[Any]: ...
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user