from functools import lru_cache @lru_cache(maxsize=None) def find_min_dist(valid, a, b, DX=[1, -1, 1j, -1j]): dist = 0 state = {a} seen = state.copy() while state: dist += 1 state = {x + dx for x in state for dx in DX} & valid - seen seen.update(state) if b in state: return dist def evolve(old, pinned): valid = frozenset({p for p, c in old if c == '.'}) for x, k in old: if x not in pinned and k.isalpha(): xi = 'ABCD'.index(k) * 2 + 3 cost = 10 ** 'ABCD'.index(k) # rule: if you're in the hall, you can only end up in your room if x.imag == 1: ys = {complex(xi, y) for y in slots} # rule: if you're in a wrong spot, you can only end up in the hall else: ys = {complex(x, 1) for x in (1, 2, 4, 6, 8, 10, 11)} for y in ys & valid: # heuristic: assume only A can waste time with alcove if y.real in {1, 2} and k != 'A': continue # rule: you only get one move new_pinned = pinned if y.imag > 1: new_pinned |= {y} if dist := find_min_dist(valid, x, y): new = old - {(x, k), (y, '.')} | {(x, '.'), (y, k)} options = {scores.get(new), scores[old] + cost * dist} scores[new] = min(options - {None}) yield new, new_pinned text = open(0).read() expansion = ''' #D#C#B#A# #D#B#A#C# ''' expanded_text = text.replace('#\n ', '#' + expansion + ' ', 1) for text in [text, expanded_text]: slots = list(range(2, len(text.splitlines()) - 1)) init = frozenset( (complex(x, y), ch) for y, ln in enumerate(text.splitlines()) for x, ch in enumerate(ln) ) goal = { (complex(i * 2 + 3, y), ch) for i, ch in enumerate('ABCD') for y in slots } scores = {init: 0} states = {(init, frozenset())} wins = set() seen = set() i = 0 while states: i += 1; print(i, len(states)) states = {new for old in states for new in evolve(*old)} - seen seen |= states if wins := {old[0] for old in states if goal < old[0]}: print('end', scores[min(wins, key=scores.get)]) break