156 lines
4.7 KiB
Python
156 lines
4.7 KiB
Python
|
#!/usr/bin/env python
|
||
|
|
||
|
import sys
|
||
|
from collections.abc import Iterator
|
||
|
from enum import Enum, StrEnum
|
||
|
from typing import NamedTuple, Optional
|
||
|
|
||
|
|
||
|
class Point(NamedTuple):
|
||
|
x: int
|
||
|
y: int
|
||
|
|
||
|
|
||
|
class Direction(Enum):
|
||
|
NORTH = Point(-1, 0)
|
||
|
SOUTH = Point(1, 0)
|
||
|
WEST = Point(0, -1)
|
||
|
EAST = Point(0, 1)
|
||
|
|
||
|
def apply(self, pos: "Point") -> "Point":
|
||
|
dx, dy = self.value
|
||
|
return Point(pos.x + dx, pos.y + dy)
|
||
|
|
||
|
|
||
|
class Pipe(StrEnum):
|
||
|
VERTICAL = "|"
|
||
|
HORIZONTAL = "-"
|
||
|
NE_BEND = "L"
|
||
|
NW_BEND = "J"
|
||
|
SW_BEND = "7"
|
||
|
SE_BEND = "F"
|
||
|
|
||
|
@classmethod
|
||
|
def from_connection(cls, connections: dict[Direction, bool]) -> "Pipe":
|
||
|
assert sum(connections.values()) == 2 # Sanity check
|
||
|
if connections.get(Direction.NORTH, False):
|
||
|
if connections.get(Direction.SOUTH, False):
|
||
|
return cls.VERTICAL
|
||
|
if connections.get(Direction.EAST, False):
|
||
|
return cls.NE_BEND
|
||
|
if connections.get(Direction.WEST, False):
|
||
|
return cls.NW_BEND
|
||
|
if connections.get(Direction.SOUTH, False):
|
||
|
if connections.get(Direction.WEST, False):
|
||
|
return cls.SW_BEND
|
||
|
if connections.get(Direction.EAST, False):
|
||
|
return cls.SE_BEND
|
||
|
assert connections[Direction.WEST] and connections[Direction.EAST]
|
||
|
return cls.HORIZONTAL
|
||
|
|
||
|
def neighbours(self, pos: Point) -> Iterator[Point]:
|
||
|
deltas: tuple[Direction, Direction]
|
||
|
|
||
|
match self:
|
||
|
case self.VERTICAL:
|
||
|
deltas = (Direction.NORTH, Direction.SOUTH)
|
||
|
case self.HORIZONTAL:
|
||
|
deltas = (Direction.WEST, Direction.EAST)
|
||
|
case self.NE_BEND:
|
||
|
deltas = (Direction.NORTH, Direction.EAST)
|
||
|
case self.NW_BEND:
|
||
|
deltas = (Direction.NORTH, Direction.WEST)
|
||
|
case self.SW_BEND:
|
||
|
deltas = (Direction.SOUTH, Direction.WEST)
|
||
|
case self.SE_BEND:
|
||
|
deltas = (Direction.SOUTH, Direction.EAST)
|
||
|
|
||
|
for dir in deltas:
|
||
|
yield dir.apply(pos)
|
||
|
|
||
|
def go_through(self, pos: Point, prev: Point) -> Point:
|
||
|
for dest in self.neighbours(pos):
|
||
|
if dest == prev:
|
||
|
continue
|
||
|
return dest
|
||
|
assert False # Sanity check
|
||
|
|
||
|
|
||
|
Pipes = dict[Point, Pipe]
|
||
|
|
||
|
|
||
|
def solve(input: list[str]) -> int:
|
||
|
def parse(input: list[str]) -> tuple[Point, Pipes]:
|
||
|
start: Optional[Point] = None
|
||
|
pipes: Pipes = {}
|
||
|
|
||
|
for x, line in enumerate(input):
|
||
|
for y, c in enumerate(line):
|
||
|
if c == ".":
|
||
|
continue
|
||
|
if c == "S":
|
||
|
start = Point(x, y)
|
||
|
continue
|
||
|
pipes[Point(x, y)] = Pipe(c)
|
||
|
|
||
|
assert start is not None # Sanity check
|
||
|
return start, pipes
|
||
|
|
||
|
def iter_pipe(cur: Point, start: Point, pipes: Pipes) -> Iterator[Point]:
|
||
|
prev = start
|
||
|
while True:
|
||
|
if cur == start:
|
||
|
break
|
||
|
if cur not in pipes:
|
||
|
break
|
||
|
# Check that the receiving pipe is connected to previous one
|
||
|
if prev not in pipes[cur].neighbours(cur):
|
||
|
return
|
||
|
yield cur
|
||
|
cur, prev = pipes[cur].go_through(cur, prev), cur
|
||
|
# Yield the loop element
|
||
|
if cur == start:
|
||
|
yield cur
|
||
|
|
||
|
# Return the possible pipes that start can be
|
||
|
def resolve_start(start: Point, pipes: Pipes) -> Pipe:
|
||
|
# Returns the direction which closes the loop, if there is one
|
||
|
def explore(dir: Direction) -> Optional[Direction]:
|
||
|
points = list(iter_pipe(dir.apply(start), start, pipes))
|
||
|
if len(points) == 0:
|
||
|
return None
|
||
|
penultimate, last = points[-2], points[-1]
|
||
|
if last != start:
|
||
|
return None
|
||
|
return Direction(Point(penultimate.x - last.x, penultimate.y - last.y))
|
||
|
|
||
|
res: set[Pipe] = set()
|
||
|
for dir in Direction:
|
||
|
if (resulting_dir := explore(dir)) is None:
|
||
|
continue
|
||
|
res.add(Pipe.from_connection({dir: True, resulting_dir: True}))
|
||
|
assert len(res) == 1 # Instructions say there is exactly one loop
|
||
|
return res.pop()
|
||
|
|
||
|
def compute_dist(start: Point, pipes: Pipes) -> dict[Point, int]:
|
||
|
res = {start: 0}
|
||
|
|
||
|
for n in pipes[start].neighbours(start):
|
||
|
for d, p in enumerate(iter_pipe(n, start, pipes), start=1):
|
||
|
res[p] = min(d, res.get(p, d))
|
||
|
|
||
|
return res
|
||
|
|
||
|
start, pipes = parse(input)
|
||
|
pipes[start] = resolve_start(start, pipes)
|
||
|
return max(compute_dist(start, pipes).values())
|
||
|
|
||
|
|
||
|
def main() -> None:
|
||
|
input = sys.stdin.read().splitlines()
|
||
|
print(solve(input))
|
||
|
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
main()
|