aoc2024/day16.py

155 lines
4 KiB
Python
Raw Permalink Normal View History

2024-12-16 21:37:12 +00:00
#!/usr/bin/env python3
import math
from enum import Enum, auto
from typing import Tuple, Dict, Set
from copy import copy
class Tile(Enum):
WALL = auto()
PATH = auto()
class Direction(Enum):
NORTH = auto()
EAST = auto()
SOUTH = auto()
WEST = auto()
type Pos = Tuple[int,int]
maze = []
end: Pos = (0,0)
player: Pos = (0,0)
with open("day16") as f:
for y,l in enumerate(f):
temp = []
for x,c in enumerate(l):
if c == '#':
temp.append(Tile.WALL)
else:
temp.append(Tile.PATH)
if c == 'E':
end = (y,x)
if c == 'S':
player = (y,x)
maze.append(temp)
def next_moves(pos, direction, cost):
moves = []
for d in Direction:
if((d == Direction.NORTH and direction == Direction.SOUTH)
or (d == Direction.SOUTH and direction == Direction.NORTH)
or (d == Direction.WEST and direction == Direction.EAST)
or (d == Direction.EAST and direction == Direction.WEST)):
continue # only 90°
p = pos
if d == Direction.NORTH:
p = (p[0]-1, p[1])
if d == Direction.EAST:
p = (p[0], p[1]+1)
if d == Direction.SOUTH:
p = (p[0]+1, p[1])
if d == Direction.WEST:
p = (p[0], p[1]-1)
# can't move into wall
if maze[p[0]][p[1]] == Tile.WALL: continue
# because I'm a good, defensive programmer lol
if p[0] < 0 or p[1] < 0: raise RuntimeError("Crossed a wall")
if p[0] >= len(maze) or p[1] >= len(maze[0]): raise RuntimeError("Crossed a wall")
c = cost
if d == direction: c += 1
else: c += 1000+1
moves.append((p,d,c))
return moves
visited: Dict[Pos, Set[Tuple[int, Direction]]] = {}
queue = [(player, Direction.EAST, 0)]
hits = []
cheap_map = [[math.inf for _ in range(len(maze[0]))] for _ in range(len(maze))]
while queue:
w = queue.pop()
if w[0] in visited:
v = visited[w[0]]
v0 = next(iter(visited[w[0]])) # just get any element, for cost doesn't matter
# this is not the cheapest path, prune search here
if v0[0] < w[2]: continue
# this is the cheapest path so far, replace all others found so far
if v0[0] > w[2]: visited[w[0]] = {(w[2], w[1])}
# another possibility, equally good as the others, but potentially different direction
else: visited[w[0]].add((w[2], w[1]))
else:
visited[w[0]] = {(w[2], w[1])}
if w[2] < cheap_map[w[0][0]][w[0][1]]:
cheap_map[w[0][0]][w[0][1]] = w[2]
if w[0] == end:
hits.append(w[2])
queue.extend(next_moves(w[0], w[1], w[2]))
print(min(hits))
tiles = set()
queue = [(player, Direction.EAST, 0, [player])]
while True:
if not queue: break
w = queue.pop()
if w[0] == end and w[2] == min(hits):
tiles.update(w[3])
continue
tainted = False
# we might cross a path that is cheaper at this point
# but which has to take a turn while we don't
if cheap_map[w[0][0]][w[0][1]] < w[2]:
tainted = True
nms = next_moves(w[0], w[1], w[2])
for nm in nms:
# if we're tainted we are either back
# to cheapest path here or we won't come back
if tainted and cheap_map[nm[0][0]][nm[0][1]] < nm[2]:
continue
l = copy(w[3]); l.append(nm[0]);
queue.append((nm[0], nm[1], nm[2], l))
print(len(tiles))
def pprint():
for i,y in enumerate(maze):
for j,x in enumerate(y):
if (i,j) in tiles:
print("O", sep=" ", end="")
elif x == Tile.WALL:
print("#", sep=" ", end = "")
elif x == Tile.PATH:
print(".", sep=" ", end = "")
print()
def pprint_cheap():
for y in cheap_map:
for x in y:
if x == math.inf:
print("#"*6, sep=" ", end = "")
else:
print(f" {x:<5}", sep=" ", end="")
print()