-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathday18.py
166 lines (130 loc) · 5.08 KB
/
day18.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
from heapq import heappush, heappop
from typing import List, Tuple
from collections import namedtuple, deque
from alive_progress import alive_bar
def parse_input(filename: str) -> List[str]:
return [line.strip() for line in open(filename).readlines()]
def get_start_and_keys(maze: List[str]) -> ((int, int), set):
width, height = len(maze[0]), len(maze)
keys = set()
start = (0, 0)
for y in range(height):
for x in range(width):
if maze[y][x] == '@':
start = (x, y)
if 'a' <= maze[y][x] <= 'z':
keys.add(maze[y][x])
return start, keys
def print_maze(maze: List[str]):
print('-' * len(maze[0]))
for row in maze:
print(row)
print('-' * len(maze[0]))
def get_neighbors(x, y) -> List[Tuple[int, int]]:
return [(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)]
def part1(maze: List[str]) -> int:
# print_maze(maze)
(x, y), all_keys = get_start_and_keys(maze)
State = namedtuple('State', ['x', 'y', 'keys', 'distance'])
queue = deque()
seen = set()
# print(f'Starting at ({x}, {y}), looking for keys: {list(all_keys)}')
queue.append(State(x, y, set(), 0))
with alive_bar(7400000) as bar:
while queue:
x, y, keys, distance = queue.popleft()
key = (x, y, tuple(sorted(keys)))
if key in seen:
# been here, done that
continue
seen.add(key)
if len(seen) % 100000 == 0:
bar(len(seen))
ch_at_pos = maze[y][x]
if ch_at_pos == '#':
# wall
continue
if 'A' <= ch_at_pos <= 'Z' and ch_at_pos.lower() not in keys:
# locked door
continue
new_keys = set(keys)
if 'a' <= ch_at_pos <= 'z':
# new key - who diz
new_keys.add(ch_at_pos)
if new_keys == all_keys:
return distance
# check out the neighbors
for nx, ny in get_neighbors(x, y):
queue.append(State(nx, ny, new_keys, distance + 1))
return 0
def get_key_locations(maze: List[List[str]]) -> (dict, int):
key_locations = {}
opening = ord('0')
for y in range(len(maze)):
for x in range(len(maze[y])):
if maze[y][x].islower():
key_locations[maze[y][x]] = (y, x)
elif maze[y][x] == '@':
key_locations[chr(opening)] = (y, x)
opening += 1
num_keys = len(key_locations.keys()) - (opening - ord('0'))
return key_locations, num_keys
def bfs(position, maze):
"""
returns all reachable keys (even if there are doors in between)
returns a dictionary like this:
{'a': (7, ('b', 'c')), 'b': (4, ('d'))}
-> can reach key a in 7 steps but door B and C are blocking
-> can reach key b in 4 steps but door D is blocking
"""
dy, dx = [-1, 0, 1, 0], [0, 1, 0, -1]
queue = [(*position, 0, ())]
seen, reachable_keys = set(), dict()
while queue:
y, x, distance, doors_between = queue.pop(0)
if (y, x) in seen:
continue
seen.add((y, x))
if maze[y][x].islower() and position != (y, x):
reachable_keys[maze[y][x]] = (distance, frozenset(doors_between))
for i in range(4):
ny, nx = y + dy[i], x + dx[i]
if maze[ny][nx] != '#':
queue.append((ny, nx, distance + 1, doors_between + (maze[ny][nx].lower(),) if maze[ny][nx].isupper() else doors_between))
return reachable_keys
def part2(maze: List[str]) -> int:
maze = [list(row) for row in maze]
# get all key location
key_locations, num_keys = get_key_locations(maze)
# get a graph between all starting points and keys and all other points
# - and doors in between
graph = {key: dict() for key in key_locations.keys()}
for key in key_locations.keys():
for key_reached, info in bfs(key_locations[key], maze).items():
graph[key][key_reached] = info
# start with four openings (0, 1, 2, 3) and an empty bag of keys
# and go to a full bag of keys
queue = [(0, (('0', '1', '2', '3'), frozenset()))]
seen = dict()
with alive_bar(1836) as bar:
while queue:
distance, node = heappop(queue)
bar(distance)
if node in seen:
continue
seen[node] = distance
bots, keys_in_bag = node
if len(keys_in_bag) == num_keys:
return distance
for i in range(len(bots)):
for key, (distance_to_key, doors) in graph[bots[i]].items():
if len(doors - keys_in_bag) == 0 and key not in keys_in_bag:
heappush(queue, ((distance + distance_to_key), (bots[:i] + (key,) + bots[i + 1:], keys_in_bag | frozenset(key))))
return 0
def main():
maze = parse_input('2019/input/day18.txt')
print(f'Part 1: {part1(maze)}')
maze = parse_input('2019/input/day18_pt2.txt')
print(f'Part 2: {part2(maze)}')
if __name__ == "__main__":
main()