#!/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()