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} 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): inputs = [ [ (62, "input18_test"), (50603, "input18"), ], [ (952408144115, "input18_test"), (None, "input18"), ], ] def parse_input(self, part2: bool = False) -> Iterable[tuple[int, int]]: for line in self.getInput(): d, s, c = line.split() if not part2: yield DIR_TRANS[d], int(s) else: yield int(c[-2]), int(c[2:-2], 16) def get_lagoon_area(self, part2: bool = False) -> int: start = Coordinate(0, 0) edges = set() perimeter = 0 for d, l in self.parse_input(part2): perimeter += l end = start + DIRECTIONS[d] * l edges.add(Edge(start, end)) start = end print_debug(edges) return get_inside_area(edges) + perimeter def part1(self) -> Any: return self.get_lagoon_area() def part2(self) -> Any: if not self.is_test(): return "" return self.get_lagoon_area(part2=True) if __name__ == "__main__": day = Day(2023, 18) day.run(verbose=True)