2025: d09: ex2: add solution
This commit is contained in:
parent
53bd9bac44
commit
913c012114
1 changed files with 149 additions and 0 deletions
149
2025/d09/ex2/ex2.py
Executable file
149
2025/d09/ex2/ex2.py
Executable 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue