Day 18 - Part 2

This commit is contained in:
Stefan Harmuth 2024-01-01 21:35:18 +01:00
parent b053e244e4
commit 1640348c40
4 changed files with 74 additions and 201 deletions

211
day18.py
View File

@ -1,214 +1,24 @@
from __future__ import annotations from __future__ import annotations
from tools.aoc import AOCDay from tools.aoc import AOCDay
from tools.coordinate import Coordinate from tools.coordinate import Coordinate, Polygon
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}
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"""
minX = min(c.from_coord.x for c in coords)
maxX = max(c.to_coord.x for c in coords)
minY = min(c.from_coord.y for c in coords)
maxY = max(c.to_coord.y for c in coords)
return minX, maxX, minY, maxY
def split_top_rect(coords: set[Edge]) -> int:
"""
try to find the "highest" rectangle and split it from the rest
return the inner area of the cut-off rectangular
"""
# TODO: handle some other split_edge being somewhere in the middle
# TODO: handle found rectangle being disconnected from the rest
# ################################################################
# # # # #
# # # # #
# # # # ########### #
# ##### # # ### #
# # # # #
# # ##### ######
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()
split_y = max_y
split_edge = None
for c in coords:
if (c.connects_to(side_edges[0]) or c.connects_to(side_edges[1])) and min_y < c.from_coord.y < split_y:
split_y = c.from_coord.y
split_edge = c
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:
left_edge = other_split_edge
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 = [
[ [
(62, "input18_test"), (62, "input18_test"),
(237, "input18_test2"),
(227, "input18_test3"),
(54, "input18_test4"),
(50603, "input18"), (50603, "input18"),
], ],
[ [
(952408144115, "input18_test"), (952408144115, "input18_test"),
(None, "input18"), (96556251590677, "input18"),
], ],
] ]
@ -222,23 +32,22 @@ class Day(AOCDay):
def get_lagoon_area(self, part2: bool = False) -> int: def get_lagoon_area(self, part2: bool = False) -> int:
start = Coordinate(0, 0) start = Coordinate(0, 0)
edges = set() points = []
perimeter = 0 perimeter = 0
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(Edge(start, end)) points.append(end)
start = end start = end
print_debug(edges) p = Polygon(points)
return get_inside_area(edges) + perimeter a = abs(p.get_area())
return int(p.get_area()) + perimeter // 2 + 1
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:
if not self.is_test():
return ""
return self.get_lagoon_area(part2=True) return self.get_lagoon_area(part2=True)

32
inputs/input18_test2 Normal file
View File

@ -0,0 +1,32 @@
R 10 (#70c710)
D 2 (#0dc571)
L 4 (#0dc571)
D 3 (#0dc571)
R 7 (#0dc571)
U 2 (#0dc571)
R 3 (#0dc571)
D 5 (#0dc571)
L 6 (#0dc571)
D 3 (#0dc571)
L 2 (#5713f0)
U 4 (#d2c081)
L 2 (#59c680)
D 6 (#411b91)
L 4 (#8ceee2)
D 3 (#caa173)
L 5 (#caa173)
U 5 (#caa173)
R 3 (#caa173)
U 2 (#caa173)
L 5 (#caa173)
U 3 (#caa173)
R 1 (#caa173)
U 2 (#caa173)
L 2 (#caa173)
U 4 (#caa173)
R 2 (#caa173)
D 2 (#caa173)
R 2 (#caa173)
D 4 (#caa173)
R 2 (#caa173)
U 6 (#caa173)

14
inputs/input18_test3 Normal file
View File

@ -0,0 +1,14 @@
R 7 (#000000)
D 2 (#000000)
R 9 (#000000)
D 9 (#000000)
R 5 (#000000)
D 3 (#000000)
L 9 (#000000)
U 9 (#000000)
L 3 (#000000)
D 9 (#000000)
L 7 (#000000)
U 6 (#000000)
L 2 (#000000)
U 8 (#000000)

18
inputs/input18_test4 Normal file
View File

@ -0,0 +1,18 @@
L 6 (#000000)
D 2 (#000000)
L 3 (#000000)
D 2 (#000000)
R 1 (#000000)
D 2 (#000000)
R 2 (#000000)
U 1 (#000000)
R 2 (#000000)
U 1 (#000000)
R 2 (#000000)
U 1 (#000000)
L 2 (#000000)
U 1 (#000000)
R 3 (#000000)
D 3 (#000000)
R 1 (#000000)
U 5 (#000000)