118 lines
4 KiB
Python
118 lines
4 KiB
Python
from collections import defaultdict
|
|
from string import ascii_uppercase
|
|
|
|
from grid import Grid
|
|
|
|
|
|
def main():
|
|
with open("day20.txt") as input_file:
|
|
lines = [l.rstrip() for l in input_file.readlines()]
|
|
|
|
# Part 1
|
|
maze = Maze(lines)
|
|
maze.find_portals()
|
|
min_steps = maze.count_min_steps(maze.entry, maze.exit)
|
|
print("Min steps:", min_steps)
|
|
|
|
# Part 2
|
|
maze = Maze(lines, recurse=True)
|
|
maze.find_portals()
|
|
min_steps = maze.count_min_steps(maze.entry, maze.exit)
|
|
print("Min steps in recursion:", min_steps)
|
|
|
|
|
|
class Maze(Grid):
|
|
|
|
TILE_PATH = "."
|
|
TILE_WALL = "#"
|
|
|
|
ENTRY_DG = "AA"
|
|
EXIT_DG = "ZZ"
|
|
|
|
def __init__(self, lines, recurse=False):
|
|
super().__init__(value_factory=str, lines=lines)
|
|
self.entry = None
|
|
self.exit = None
|
|
self.portals = {}
|
|
self.recurse = recurse
|
|
self.portal_is_outer = {}
|
|
self.height = len(lines)
|
|
self.width = max(len(line) for line in lines)
|
|
|
|
def find_portals(self):
|
|
portals = defaultdict(list)
|
|
for x, y, v in list(self.values_gen()):
|
|
if v in ascii_uppercase:
|
|
rv = self.getv(x + 1, y)
|
|
if rv and rv in ascii_uppercase:
|
|
rrv = self.getv(x + 2, y)
|
|
portal_pos = (x + 2, y) if rrv == Maze.TILE_PATH else (x - 1, y)
|
|
portals[v + rv].append(portal_pos)
|
|
self.portal_is_outer[portal_pos] = x == 0 or x == self.width - 2
|
|
continue
|
|
dv = self.getv(x, y + 1)
|
|
if dv and dv in ascii_uppercase:
|
|
ddv = self.getv(x, y + 2)
|
|
portal_pos = (x, y + 2) if ddv == Maze.TILE_PATH else (x, y - 1)
|
|
portals[v + dv].append(portal_pos)
|
|
self.portal_is_outer[portal_pos] = y == 0 or y == self.height - 2
|
|
self.entry = portals["AA"][0]
|
|
self.exit = portals["ZZ"][0]
|
|
for p in portals.values():
|
|
if len(p) == 2:
|
|
self.portals[p[0]] = p[1]
|
|
self.portals[p[1]] = p[0]
|
|
|
|
def dumb_print(self, rpos=None, level=0):
|
|
for y, row in self.g.items():
|
|
for x, v in row.items():
|
|
pos = (x, y)
|
|
if pos == rpos:
|
|
v = "@"
|
|
elif pos in (self.entry, self.exit):
|
|
v = "▒" if not self.recurse or level == 0 else "#"
|
|
elif pos in self.portals:
|
|
v = "▒" if not self.recurse or level > 0 or not self.portal_is_outer[pos] else "#"
|
|
print(v, end="")
|
|
print()
|
|
|
|
def bfs(self, start, end):
|
|
discovered = defaultdict(lambda: Grid(value_factory=bool))
|
|
discovered[0].setv(start[0], end[1], True)
|
|
parents = {}
|
|
q = [(start, 0)]
|
|
while q:
|
|
pos, level = q.pop(0)
|
|
if pos == end and (not self.recurse or level == 0):
|
|
return parents
|
|
nears = self.near_objects(pos)
|
|
is_portal = pos in self.portals
|
|
if is_portal:
|
|
nears[self.portals[pos]] = Maze.TILE_PATH
|
|
for near_pos, near_tile in nears.items():
|
|
if near_tile != Maze.TILE_PATH:
|
|
continue
|
|
near_level = level
|
|
if self.recurse and is_portal and near_pos == self.portals[pos]:
|
|
near_level += -1 if self.portal_is_outer[pos] else 1
|
|
if near_level < 0 or discovered[near_level].getv(near_pos[0], near_pos[1]):
|
|
continue
|
|
discovered[near_level].setv(near_pos[0], near_pos[1], True)
|
|
parents[(near_pos, near_level)] = (pos, level)
|
|
q.append((near_pos, near_level))
|
|
return None
|
|
|
|
def count_min_steps(self, start, end):
|
|
parents = self.bfs(start, end)
|
|
count = 0
|
|
pos = (end, 0)
|
|
while pos != (start, 0):
|
|
count += 1
|
|
parent = parents[pos]
|
|
pos = parent
|
|
return count
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|