2025: d09: ex2: add solution

This commit is contained in:
Bruno BELANYI 2025-12-09 13:14:16 +00:00
parent 53bd9bac44
commit 913c012114

149
2025/d09/ex2/ex2.py Executable file
View file

@ -0,0 +1,149 @@
#!/usr/bin/env python
import itertools
import sys
from collections.abc import Iterator
from typing import NamedTuple
class Point(NamedTuple):
x: int
y: int
def solve(input: list[str]) -> int:
def parse(input: list[str]) -> list[Point]:
return [Point(*map(int, line.split(","))) for line in input]
def compression_mapping(points: list[Point]) -> dict[Point, Point]:
def compress_1d(values: set[int]) -> dict[int, int]:
return {val: i for i, val in enumerate(sorted(values))}
xs = compress_1d({p.x for p in points})
ys = compress_1d({p.y for p in points})
compress = lambda p: Point(xs[p.x], ys[p.y])
return {p: compress(p) for p in points}
def line(p1: Point, p2: Point) -> Iterator[Point]:
def inclusive_range_any_order(a: int, b: int) -> Iterator[int]:
if a < b:
yield from range(a, b + 1)
else:
yield from range(a, b - 1, -1)
# Hack-ish work-around to avoid infinite loops
if p1 == p2:
yield p1
return
xs = inclusive_range_any_order(p1.x, p2.x)
ys = inclusive_range_any_order(p1.y, p2.y)
if p1.x == p2.x:
xs = itertools.repeat(p1.x)
if p1.y == p2.y:
ys = itertools.repeat(p1.y)
yield from map(Point._make, zip(xs, ys))
def draw_edges(tiles: list[Point]) -> set[Point]:
# Close the loop by repeating the first vertex at the end
vertices = tiles + [tiles[0]]
edges = {p for a, b in itertools.pairwise(vertices) for p in line(a, b)}
return edges
def flood_fill(start: Point, points: set[Point]) -> set[Point]:
assert start in points # Sanity check
visited: set[Point] = set()
stack = [start]
while stack:
p = stack.pop()
visited.add(p)
for dx, dy in (
(-1, 0),
(1, 0),
(0, -1),
(0, 1),
):
n = Point(p.x + dx, p.y + dy)
if n in visited:
continue
if n not in points:
continue
stack.append(n)
return visited
def fill_tiles(tiles: list[Point]) -> set[Point]:
# I'm too lazy to find an interior point to flood-fill to the edges from,
# instead fill the exterior and invert the set
min_x, max_x = min(p.x for p in tiles), max(p.x for p in tiles)
min_y, max_y = min(p.y for p in tiles), max(p.y for p in tiles)
# Keep a border all around to make sure the flood-fill can reach everywhere
all_points = {
Point(x, y)
for x in range(min_x - 1, max_x + 1 + 1)
for y in range(min_y - 1, max_y + 1 + 1)
}
edges = draw_edges(tiles)
# Pick a corner we know for sure won't be in the polygon
start = Point(min_x - 1, min_y - 1)
# Sever the points inside and outside the polygon, then flood-fill exterior
exterior = flood_fill(start, all_points - edges)
# Return interior/edge points by removing everything that is on the exterior
return all_points - exterior
def rectangle_edges(p: Point, other: Point) -> Iterator[Point]:
min_x, max_x = min(p.x, other.x), max(p.x, other.x)
min_y, max_y = min(p.y, other.y), max(p.y, other.y)
c1 = Point(min_x, min_y)
c2 = Point(max_x, min_y)
c3 = Point(max_x, max_y)
c4 = Point(min_x, max_y)
yield from line(c1, c2)
yield from line(c2, c3)
yield from line(c3, c4)
yield from line(c4, c1)
def rectangle_area(p: Point, other: Point) -> int:
dx = abs(p.x - other.x)
dy = abs(p.y - other.y)
return (dx + 1) * (dy + 1)
def inscribed_rectangles(tiles: list[Point]) -> Iterator[tuple[Point, Point]]:
inside_tiles = fill_tiles(tiles)
yield from (
(a, b)
for a, b in itertools.combinations(tiles, 2)
if all(p in inside_tiles for p in rectangle_edges(a, b))
)
def largest_rectangle_area(tiles: list[Point]) -> int:
to_compressed = compression_mapping(tiles)
decompress = {v: k for k, v in to_compressed.items()}.__getitem__
compressed_tiles = [to_compressed[p] for p in tiles]
compressed_rectangles = inscribed_rectangles(compressed_tiles)
return max(
rectangle_area(decompress(a), decompress(b))
for a, b in compressed_rectangles
)
tiles = parse(input)
return largest_rectangle_area(tiles)
def main() -> None:
input = sys.stdin.read().splitlines()
print(solve(input))
if __name__ == "__main__":
main()