From 00000190d0e07b5756b366f44f568641d18fcaf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Jane=C5=BEi=C4=8D?= Date: Tue, 9 Dec 2025 22:05:33 +0100 Subject: [PATCH] feat: add solution 2025/09 --- data/example/2025/09.txt | 8 ++ src/solution/year_2025/day_09.py | 162 +++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 data/example/2025/09.txt create mode 100644 src/solution/year_2025/day_09.py diff --git a/data/example/2025/09.txt b/data/example/2025/09.txt new file mode 100644 index 000000000..c8563ea --- /dev/null +++ b/data/example/2025/09.txt @@ -0,0 +1,8 @@ +7,1 +11,1 +11,7 +9,7 +9,5 +2,5 +2,3 +7,3 diff --git a/src/solution/year_2025/day_09.py b/src/solution/year_2025/day_09.py new file mode 100644 index 000000000..2a77100 --- /dev/null +++ b/src/solution/year_2025/day_09.py @@ -0,0 +1,162 @@ +from collections.abc import Generator +from typing import Any + +Point = tuple[int, int] + + +def _parse_input(input_data) -> list[Point]: + points = [] + for line in input_data.splitlines(): + a, b = line.split(",") + points.append((int(a), int(b))) + + return points + + +def _rectangle_size(p1: Point, p2: Point) -> int: + x1, y1 = p1 + x2, y2 = p2 + + return (abs(x1 - x2) + 1) * (abs(y1 - y2) + 1) + + +def part_1(input_data: str) -> Any: + lines = _parse_input(input_data) + + m = None + for idx, p1 in enumerate(lines): + for p2 in lines[idx + 1 :]: + size = _rectangle_size(p1, p2) + m = max(size, m or size) + + return m + + +def _get_dir(p1: Point, p2: Point) -> Point: + x1, y1 = p1 + x2, y2 = p2 + + dx = x2 - x1 + dy = y2 - y1 + + if dx: + dx = dx // abs(dx) + if dy: + dy = dy // abs(dy) + + return dx, dy + + +def _get_segment(p1: Point, p2: Point) -> Generator[Point]: + dx, dy = _get_dir(p1, p2) + + points = [] + + sx, sy = p1 + while (sx, sy) != p2: + yield sx, sy + points.append((sx, sy)) + sx, sy = sx + dx, sy + dy + + yield p2 + + +def _compress_coordinates(points: list[Point], y: bool) -> dict[int, int]: + return {v: k for k, v in enumerate(sorted({p[y] for p in points}))} + + +def _find_inner( + points: list[Point], xc: dict[int, int], yc: dict[int, int] +) -> set[Point]: + def c(p: Point) -> Point: + return xc[p[0]], yc[p[1]] + + inner = set() + inside = set() + + for idx in range(len(points)): + p1 = points[idx - 1] + p2 = points[idx] + + c1, c2 = c(p1), c(p2) + + rng = _get_segment(c1, c2) + + dx, dy = _get_dir(c1, c2) + # rotate 90 positive as inside is in the positive direction + dx, dy = -dy, dx + + for x, y in rng: + inner.add((x, y)) + inside.add((x + dx, y + dy)) + + stack = list(inside - inner) + + dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)] + while stack: + px, py = stack.pop() + inner.add((px, py)) + + for dx, dy in dirs: + nx, ny = px + dx, py + dy + if (nx, ny) in inner: + continue + stack.append((nx, ny)) + + return inner + + +def _get_corner_points(p1: Point, p2: Point) -> tuple[Point, ...]: + if p1 == p2: + return (p1,) + + px1, py1 = p1 + px2, py2 = p2 + p3 = px1, py2 + p4 = px2, py1 + + return p1, p3, p2, p4 + + +def part_2(input_data: str) -> Any: + points = _parse_input(input_data) + + x_compression = _compress_coordinates(points, False) + y_compression = _compress_coordinates(points, True) + + def c(p: Point) -> Point: + return x_compression[p[0]], y_compression[p[1]] + + inner = _find_inner(points, x_compression, y_compression) + + m = None + + for idx, p1 in enumerate(points): + c1 = c(p1) + for p2 in points[idx + 1 :]: + c2 = c(p2) + + c1, c2, c3, c4 = _get_corner_points(c1, c2) + + is_inside = ( + all(x in inner for x in _get_segment(c1, c2)) + and all(x in inner for x in _get_segment(c2, c3)) + and all(x in inner for x in _get_segment(c3, c4)) + and all(x in inner for x in _get_segment(c4, c1)) + ) + + if not is_inside: + continue + + size = _rectangle_size(p1, p2) + m = max(m or size, size) + + return m + + +def test_part_1(example_data): + assert part_1(example_data) == 50 + + +def test_part_2(example_data): + assert part_2(example_data) == 24