From 33154a92ffe75b76e73134180c6db83c066ead17 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Mon, 18 Dec 2023 16:37:08 +0100 Subject: [PATCH] Day 18 - a maybe solution, with some "edge"cases still to handle --- day18.py | 242 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 194 insertions(+), 48 deletions(-) diff --git a/day18.py b/day18.py index 5b62e3b..c096f6f 100644 --- a/day18.py +++ b/day18.py @@ -1,59 +1,204 @@ +from __future__ import annotations from tools.aoc import AOCDay from tools.coordinate import Coordinate +from tools.grid import Grid from typing import Any, Iterable - 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""" - minX = list(sorted(coords))[0][0][0] - minY = list(sorted(coords, key=lambda d: (d[0][1], d[0][0])))[0][0][1] - maxX = list(sorted(coords, reverse=True))[0][0][0] - maxY = list(sorted(coords, key=lambda d: (d[0][1], d[0][0]), reverse=True))[0][0][1] + 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_polygon(coords: set[tuple[Coordinate, Coordinate]], cut_idx: int, horizontal: bool) -> (set[Coordinate], set[Coordinate]): - """split the polygon described by coords at cut_idx horizontally or vertically and return both resulting polygons""" - if horizontal: - vertical_edges = {x for x in coords if x[0].x == x[1].x} - 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() - below = set() - 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 +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() -def get_inside_area(coords: set[tuple[Coordinate, Coordinate]]) -> int: - min_x, max_x, min_y, max_y = get_borders(coords) - if len(coords) == 4: # rectangle, hopefully - return (max_x - min_x - 1) * (max_y - min_y - 1) - else: - if max_x - min_x > max_y - min_y: - half_1, half_2 = split_polygon(coords, (max_x - min_x) // 2, horizontal=False) + 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: - half_1, half_2 = split_polygon(coords, (max_y - min_y) // 2, horizontal=True) - return get_inside_area(half_1) + get_inside_area(half_2) + 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): inputs = [ @@ -64,7 +209,7 @@ class Day(AOCDay): [ (952408144115, "input18_test"), (None, "input18"), - ] + ], ] 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): perimeter += l end = start + DIRECTIONS[d] * l - edges.add((start, end)) + edges.add(Edge(start, end)) start = end - print(split_polygon(edges, 4, True)) - - #return get_inside_area(edges) + perimeter + print_debug(edges) + return get_inside_area(edges) + perimeter def part1(self) -> Any: return self.get_lagoon_area() 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.run(verbose=True)