advent-of-code/2023/day10/day10.py

159 lines
4.7 KiB
Python
Raw Permalink Normal View History

2024-12-03 14:29:02 +00:00
from typing import Dict, List, Set, Tuple
from dataclasses import dataclass
from collections import defaultdict
from enum import Enum
@dataclass(frozen=True)
class Vec2d:
x: int
y: int
def __add__(self, other):
return Vec2d(self.x + other.x, self.y + other.y)
class Direction(Enum):
NORTH = Vec2d(0, -1) # [0, 0] is the top-left corner, so y increases going downwards
SOUTH = Vec2d(0, 1)
EAST = Vec2d(1, 0)
WEST = Vec2d(-1, 0)
"""
| is a vertical pipe connecting north and south.
- is a horizontal pipe connecting east and west.
L is a 90-degree bend connecting north and east.
J is a 90-degree bend connecting north and west.
7 is a 90-degree bend connecting south and west.
F is a 90-degree bend connecting south and east.
. is ground; there is no pipe in this tile.
S is the starting position of the animal; there is a pipe on this tile, but your sketch doesn't show what shape the pipe has.
"""
PIPES = {
"|": (Direction.SOUTH, Direction.NORTH),
"-": (Direction.EAST, Direction.WEST),
"L": (Direction.NORTH, Direction.EAST),
"J": (Direction.NORTH, Direction.WEST),
"7": (Direction.SOUTH, Direction.WEST),
"F": (Direction.SOUTH,Direction.EAST),
}
PIPES_REVERSE_LOOKUP = {v: k for k,v in PIPES.items()}
def find_start_position(grid: List[str]) -> Vec2d:
for y, row in enumerate(grid):
for x, c in enumerate(row):
if c == "S":
return Vec2d(x, y)
raise RuntimeError("The start position was not found")
def update_start_symbol(grid: List[str], start_pos: Vec2d):
"""
Updates the map by replacing the start symbol "S" with its actual corresponding pipe
"""
# check which neighbors are connected to the start position
connections = []
north = start_pos + Direction.NORTH.value
south = start_pos + Direction.SOUTH.value
east = start_pos + Direction.EAST.value
west = start_pos + Direction.WEST.value
if grid[north.y][north.x] in "|7F":
connections.append(Direction.NORTH)
if grid[south.y][south.x] in "|LJ":
connections.append(Direction.SOUTH)
if grid[east.y][east.x] in "-7J":
connections.append(Direction.EAST)
if grid[west.y][west.x] in "-LF":
connections.append(Direction.WEST)
print("Start symbol has the following connections: ", connections)
assert len(connections) == 2, "start symbol has invalid connections"
pipe = PIPES_REVERSE_LOOKUP[tuple(connections)]
print(f"Start symbol is a {pipe} pipe")
# replace it in the grid accordingly
grid[start_pos.y] = grid[start_pos.y].replace("S", pipe)
def parse_graph(grid: List[str]) -> Dict[Vec2d, List[Vec2d]]:
graph = defaultdict(list)
for y, row in enumerate(grid):
for x, pipe in enumerate(row):
pos = Vec2d(x, y)
if pipe in PIPES:
for direction in PIPES[pipe]:
next_pos = pos + direction.value
graph[pos].append(next_pos)
return graph
def traverse_graph(graph, start_pos) -> Tuple[int, Set[Vec2d]]:
"""
traverse the graph using BFS, return the path and the
find the length of the longest path in the graph
"""
queue = [(start_pos, 0)] # (pos, distance from start)
max_dist = 0
visited = {start_pos}
while queue != []:
cur, dist = queue.pop(0)
max_dist = max(max_dist, dist)
for next_pos in graph[cur]:
if next_pos not in visited:
visited.add(next_pos)
queue.append((next_pos, dist+1))
return max_dist, visited
def count_enclosed_tiles(grid, edges):
"""
count the number of enclosed tiles in the loop by casting a ray on each row
and counting the number of intersections with the edges of the loop
"""
enclosed_count = 0
for y, row in enumerate(grid):
crossings = 0
for x, pipe in enumerate(row):
pos = Vec2d(x, y)
if pos in edges:
if pipe in "L|J":
crossings += 1
elif crossings % 2 == 1:
enclosed_count += 1
return enclosed_count
def main(grid):
rows, cols = len(grid), len(grid[0])
start_pos = find_start_position(grid)
print("Start pos ", start_pos)
update_start_symbol(grid, start_pos)
graph = parse_graph(grid)
max_dist, visited = traverse_graph(graph, start_pos)
print("Part 1: ", max_dist)
# visited edges are the ones that are part of the loop
inside_count = count_enclosed_tiles(grid, visited)
print("Part 2: ", inside_count)
if __name__ == "__main__":
import sys
infile = sys.argv[1]
with open(infile) as f:
lines = [l.rstrip() for l in f.readlines()]
main(lines)