···11+import fileinput
22+from string import ascii_uppercase, ascii_lowercase
33+from collections import defaultdict, deque
44+from heapq import heappush, heappop
55+66+from utils import Point
77+88+99+BOARD = defaultdict(lambda: '#')
1010+KEYS = {}
1111+DOORS = {}
1212+1313+ent = None
1414+1515+for i, line in enumerate(fileinput.input()):
1616+ for x, c in enumerate(line.strip()):
1717+ BOARD[Point(x, i)] = c
1818+ if c == '@':
1919+ ent = Point(x, i)
2020+2121+ if c in ascii_lowercase:
2222+ KEYS[c] = Point(x, i)
2323+2424+ if c in ascii_uppercase:
2525+ DOORS[c] = Point(x, i)
2626+2727+NUM_KEYS = len(KEYS)
2828+2929+3030+def part_1(start):
3131+ horizon = [(start, frozenset())]
3232+ seen = set()
3333+3434+ level = 0
3535+ while horizon:
3636+ new_horizon = []
3737+3838+ for p, k in horizon:
3939+ for np in p.neighbours_4():
4040+ if (np, k) in seen:
4141+ continue
4242+4343+ if BOARD[np] != '#':
4444+ c = BOARD[np]
4545+4646+ if c in DOORS and c.lower() not in k:
4747+ continue
4848+4949+ if c in KEYS:
5050+ if c in k:
5151+ state = (np, k)
5252+ else:
5353+ state = (np, k | set([c]))
5454+ # print state
5555+5656+ if len(state[1]) == NUM_KEYS:
5757+ return level + 1
5858+5959+ new_horizon.append(state)
6060+ seen.add(state)
6161+ else:
6262+ new_horizon.append((np, k))
6363+ seen.add((np, k))
6464+6565+ # print "yes"
6666+6767+ horizon = new_horizon
6868+ level += 1
6969+7070+print "Minimal steps for Part 1:", part_1(ent)
7171+7272+# Manual maze replacement for Part 2
7373+ents = []
7474+7575+for np in ent.neighbours_8():
7676+ if np.dist_manhattan(ent) == 2:
7777+ ents.append(np)
7878+ BOARD[np] = '@'
7979+ else:
8080+ BOARD[np] = '#'
8181+8282+BOARD[ent] = '#'
8383+8484+points = KEYS
8585+for i, ent in enumerate(ents):
8686+ points[str(i)] = ent
8787+# print points
8888+8989+# Preprocess graph to determine distances from each key to each other key
9090+# Edges are represented as start_pos: (steps, new_key, doors_passed).
9191+graph = defaultdict(list)
9292+9393+for i, node in enumerate(points):
9494+ # BFS from start pos to each key in quadrant
9595+ # state is (pos, doors_passed)
9696+ level = 1
9797+ horizon = [(points[node], frozenset())]
9898+ seen = set()
9999+ while horizon:
100100+ new_horizon = []
101101+ for pos, doors in horizon:
102102+ for np in pos.neighbours_4():
103103+ if (np, doors) in seen:
104104+ continue
105105+106106+ if BOARD[np] == '#':
107107+ continue
108108+109109+ tile = BOARD[np]
110110+ ndoors = doors
111111+ if tile in ascii_uppercase:
112112+ ndoors |= set([tile.lower()])
113113+114114+ if tile in ascii_lowercase and tile != node:
115115+ graph[node].append((level, tile, ndoors))
116116+ seen.add((np, ndoors))
117117+ continue
118118+119119+ new_horizon.append((np, ndoors))
120120+ seen.add((np, ndoors))
121121+122122+ horizon = new_horizon
123123+ level += 1
124124+125125+heap = [(0, '0', '1', '2', '3', frozenset())]
126126+seen = set()
127127+128128+while heap:
129129+ steps, a, b, c, d, keys = heappop(heap)
130130+131131+ if len(keys) == NUM_KEYS:
132132+ print "Minimal steps for Part 2:", steps
133133+ break
134134+135135+ for i, p in enumerate((a, b, c, d), start=1):
136136+ for ns, np, nd in graph[p]:
137137+ if not keys.issuperset(nd):
138138+ continue
139139+140140+ next_state = [steps + ns, a, b, c, d, keys | set([np])]
141141+142142+ next_state[i] = np
143143+ next_state = tuple(next_state)
144144+ if next_state[1:] in seen:
145145+ continue
146146+147147+ seen.add(next_state[1:])
148148+ heappush(heap, next_state)