Day 18 - a maybe solution, with some "edge"cases still to handle

This commit is contained in:
Stefan Harmuth 2023-12-18 16:37:08 +01:00
parent 8b888890b5
commit 33154a92ff

242
day18.py
View File

@ -1,59 +1,204 @@
from __future__ import annotations
from tools.aoc import AOCDay from tools.aoc import AOCDay
from tools.coordinate import Coordinate from tools.coordinate import Coordinate
from tools.grid import Grid
from typing import Any, Iterable from typing import Any, Iterable
DIRECTIONS = [Coordinate(1, 0), Coordinate(0, 1), Coordinate(-1, 0), Coordinate(0, -1)] DIRECTIONS = [Coordinate(1, 0), Coordinate(0, 1), Coordinate(-1, 0), Coordinate(0, -1)]
DIR_TRANS = {'R': 0, 'D': 1, 'L': 2, 'U': 3} DIR_TRANS = {"R": 0, "D": 1, "L": 2, "U": 3}
def get_borders(coords: set[tuple[Coordinate, Coordinate]]) -> (int, int, int, int): class Edge:
from_coord: Coordinate = None
to_coord: Coordinate = None
def __init__(self, start: Coordinate, end: Coordinate):
assert start.x == end.x or start.y == end.y, "Invalid Edge"
self.from_coord = min(start, end)
self.to_coord = max(start, end)
def is_horizontal(self) -> bool:
return self.from_coord.y == self.to_coord.y
def connects_to(self, other: Edge) -> bool:
return (
self.from_coord == other.from_coord
or self.from_coord == other.to_coord
or self.to_coord == other.from_coord
or self.to_coord == other.to_coord
)
def __hash__(self) -> int:
return hash((self.from_coord, self.to_coord))
def __lt__(self, other: Edge) -> bool:
return self.from_coord < other.from_coord
def __str__(self) -> str:
return f"{self.from_coord} -> {self.to_coord}"
def __repr__(self) -> str:
return str(self)
def get_borders(coords: set[Edge]) -> (int, int, int, int):
"""min_x, max_x, min_y, max_y""" """min_x, max_x, min_y, max_y"""
minX = list(sorted(coords))[0][0][0] minX = min(c.from_coord.x for c in coords)
minY = list(sorted(coords, key=lambda d: (d[0][1], d[0][0])))[0][0][1] maxX = max(c.to_coord.x for c in coords)
maxX = list(sorted(coords, reverse=True))[0][0][0] minY = min(c.from_coord.y for c in coords)
maxY = list(sorted(coords, key=lambda d: (d[0][1], d[0][0]), reverse=True))[0][0][1] maxY = max(c.to_coord.y for c in coords)
return minX, maxX, minY, maxY return minX, maxX, minY, maxY
def split_polygon(coords: set[tuple[Coordinate, Coordinate]], cut_idx: int, horizontal: bool) -> (set[Coordinate], set[Coordinate]): def split_top_rect(coords: set[Edge]) -> int:
"""split the polygon described by coords at cut_idx horizontally or vertically and return both resulting polygons""" """
if horizontal: try to find the "highest" rectangle and split it from the rest
vertical_edges = {x for x in coords if x[0].x == x[1].x} return the inner area of the cut-off rectangular
intersecting_edges = {x for x in vertical_edges if min(x[0].y, x[1].y) <= cut_idx <= max(x[0].y, x[1].y)} """
above = set() # TODO: handle some other split_edge being somewhere in the middle
below = set() # TODO: handle found rectangle being disconnected from the rest
for c in coords: # ################################################################
if c in intersecting_edges: # # # # #
# split into two # # # # #
# c = ((from_x, from_y), (to_x, to_y)) # # # # ########### #
c_1 = (c[0], Coordinate(c[1].x, cut_idx)) # ##### # # ### #
c_2 = (Coordinate(c[0].x, cut_idx), c[1]) # # # # #
if c[0].y <= cut_idx: # # ##### ######
above.add(c_1)
below.add(c_2)
else:
above.add(c_2)
below.add(c_1)
else:
if c[0].y <= cut_idx:
above.add(c)
if c[0].y >= cut_idx:
below.add(c)
return above, below
min_y = min(c.from_coord.y for c in coords)
max_y = max(c.to_coord.y for c in coords)
top_edge = [x for x in coords if x.is_horizontal() and x.from_coord.y == min_y][0]
side_edges = [x for x in coords if not x.is_horizontal() and x.connects_to(top_edge)]
try:
assert len(side_edges) == 2, f"found != 2 side edges: {side_edges} from {top_edge}"
except AssertionError:
print_debug(coords)
exit()
def get_inside_area(coords: set[tuple[Coordinate, Coordinate]]) -> int: split_y = max_y
min_x, max_x, min_y, max_y = get_borders(coords) split_edge = None
if len(coords) == 4: # rectangle, hopefully for c in coords:
return (max_x - min_x - 1) * (max_y - min_y - 1) if (c.connects_to(side_edges[0]) or c.connects_to(side_edges[1])) and min_y < c.from_coord.y < split_y:
else: split_y = c.from_coord.y
if max_x - min_x > max_y - min_y: split_edge = c
half_1, half_2 = split_polygon(coords, (max_x - min_x) // 2, horizontal=False)
if split_y == max_y:
# last rect
min_x, max_x, min_y, max_y = get_borders(coords)
for x in coords.copy():
coords.remove(x) # empty coords in-place
return abs(max_x - min_x - 1) * abs(max_y - min_y - 1)
assert split_edge is not None, f"could not find split_edge {top_edge=}, {side_edges=}"
# print(f"{top_edge=}, {split_edge=}")
# print(f"{side_edges=}")
top_min_x, top_max_x = top_edge.from_coord.x, top_edge.to_coord.x
split_min_x, split_max_x = split_edge.from_coord.x, split_edge.to_coord.x
side_edge_max_y_1 = side_edges[0].to_coord.y
side_edge_max_y_2 = side_edges[1].to_coord.y
# here we should know enough to remove all edges involved
coords.remove(top_edge)
coords.remove(split_edge)
for s in side_edges:
coords.remove(s)
# now to connect the open ends
# create new side edge
if side_edge_max_y_1 > side_edge_max_y_2:
old_edge = side_edges[0]
coords.add(
Edge(Coordinate(old_edge.from_coord.x, split_y), Coordinate(old_edge.from_coord.x, side_edge_max_y_1))
)
elif side_edge_max_y_1 < side_edge_max_y_2:
old_edge = side_edges[1]
coords.add(
Edge(Coordinate(old_edge.from_coord.x, split_y), Coordinate(old_edge.from_coord.x, side_edge_max_y_2))
)
area = abs(top_max_x - top_min_x - 1) * abs(split_y - min_y)
if side_edge_max_y_1 == side_edge_max_y_2:
print("edges to both sides")
# TODO: find the other corresponding horizontal edge, remove both edges and connect with one covering both ends
split_edges = {x for x in coords if x.is_horizontal() and x.from_coord.y == split_y}
other_split_edge = None
for edge in split_edges:
if edge != split_edge and (edge.connects_to(side_edges[0]) or edge.connects_to(side_edges[1])):
other_split_edge = edge
break
print(f"found split edge: {split_edge}")
print(f"other split edge: {other_split_edge}")
assert other_split_edge is not None, "could not find corresponding split edge"
coords.remove(other_split_edge)
if split_edge.from_coord.x < other_split_edge.from_coord.x:
left_edge = split_edge
right_edge = other_split_edge
else: else:
half_1, half_2 = split_polygon(coords, (max_y - min_y) // 2, horizontal=True) left_edge = other_split_edge
return get_inside_area(half_1) + get_inside_area(half_2) right_edge = split_edge
if left_edge.from_coord.x < top_edge.from_coord.x: # left_edge goes to the left
left_x = left_edge.from_coord.x
else: # left edge goes inside
left_x = left_edge.to_coord.x
area -= abs(left_edge.to_coord.x - top_edge.from_coord.x)
if right_edge.to_coord.x > top_edge.to_coord.x:
right_x = right_edge.to_coord.x
else: # right edge goes inside
right_x = right_edge.from_coord.x
area -= abs(top_edge.to_coord.x - right_edge.from_coord.x)
# TODO: create new connecting edge
coords.add(Edge(Coordinate(left_x, split_y), Coordinate(right_x, split_y)))
elif top_min_x == split_max_x: # edge goes to the left
print("edges goes to the left")
coords.add(Edge(Coordinate(split_min_x, split_y), Coordinate(top_max_x, split_y)))
elif top_max_x == split_min_x: # edge goes to the right
print("edges goes to the right")
coords.add(Edge(Coordinate(top_min_x, split_y), Coordinate(split_max_x, split_y)))
elif top_min_x == split_min_x: # edge goes inside from the left
print("edges goes inside from the left")
area -= abs(split_max_x - split_min_x)
coords.add(Edge(Coordinate(split_max_x, split_y), Coordinate(top_max_x, split_y)))
elif top_max_x == split_max_x: # edge goes inside from the right
print("edges goes inside from the right")
area -= abs(split_max_x - split_min_x)
coords.add(Edge(Coordinate(top_min_x, split_y), Coordinate(split_min_x, split_y)))
else:
assert False, "where does my split_edge go?!"
return area
def print_debug(coords: set[Edge]):
grid = Grid()
for coord in coords:
try:
for c in coord.from_coord.getLineTo(coord.to_coord):
grid.set(c)
except ZeroDivisionError:
print("ZDE:", coords)
exit()
grid.print()
def get_inside_area(coords: set[Edge]) -> int:
area = 0
while coords:
print_debug(coords)
f = split_top_rect(coords)
area += f
print(f"area removed: {f}, new {area=}")
print("=" * 50)
return area
class Day(AOCDay): class Day(AOCDay):
inputs = [ inputs = [
@ -64,7 +209,7 @@ class Day(AOCDay):
[ [
(952408144115, "input18_test"), (952408144115, "input18_test"),
(None, "input18"), (None, "input18"),
] ],
] ]
def parse_input(self, part2: bool = False) -> Iterable[tuple[int, int]]: def parse_input(self, part2: bool = False) -> Iterable[tuple[int, int]]:
@ -82,20 +227,21 @@ class Day(AOCDay):
for d, l in self.parse_input(part2): for d, l in self.parse_input(part2):
perimeter += l perimeter += l
end = start + DIRECTIONS[d] * l end = start + DIRECTIONS[d] * l
edges.add((start, end)) edges.add(Edge(start, end))
start = end start = end
print(split_polygon(edges, 4, True)) print_debug(edges)
return get_inside_area(edges) + perimeter
#return get_inside_area(edges) + perimeter
def part1(self) -> Any: def part1(self) -> Any:
return self.get_lagoon_area() return self.get_lagoon_area()
def part2(self) -> Any: def part2(self) -> Any:
return "" if not self.is_test():
return ""
return self.get_lagoon_area(part2=True)
if __name__ == '__main__': if __name__ == "__main__":
day = Day(2023, 18) day = Day(2023, 18)
day.run(verbose=True) day.run(verbose=True)