Skip to content

Commit 7046de1

Browse files
committed
Added day 2019-18
1 parent dd568b5 commit 7046de1

File tree

1 file changed

+332
-0
lines changed

1 file changed

+332
-0
lines changed
Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
# -------------------------------- Input data ---------------------------------------- #
2+
import os, pathfinding, heapq
3+
4+
from complex_utils import *
5+
6+
test_data = {}
7+
8+
test = 1
9+
test_data[test] = {
10+
"input": """#########
11+
#b.A.@.a#
12+
#########""",
13+
"expected": ["8", "Unknown"],
14+
}
15+
16+
test += 1
17+
test_data[test] = {
18+
"input": """########################
19+
#f.D.E.e.C.b.A.@.a.B.c.#
20+
######################.#
21+
#d.....................#
22+
########################""",
23+
"expected": ["86", "Unknown"],
24+
}
25+
26+
test += 1
27+
test_data[test] = {
28+
"input": """########################
29+
#...............b.C.D.f#
30+
#.######################
31+
#.....@.a.B.c.d.A.e.F.g#
32+
########################""",
33+
"expected": ["132", "Unknown"],
34+
}
35+
36+
test += 1
37+
test_data[test] = {
38+
"input": """#################
39+
#i.G..c...e..H.p#
40+
########.########
41+
#j.A..b...f..D.o#
42+
########@########
43+
#k.E..a...g..B.n#
44+
########.########
45+
#l.F..d...h..C.m#
46+
#################""",
47+
"expected": ["136", "Unknown"],
48+
}
49+
50+
test += 1
51+
test_data[test] = {
52+
"input": """########################
53+
#@..............ac.GI.b#
54+
###d#e#f################
55+
###A#B#C################
56+
###g#h#i################
57+
########################""",
58+
"expected": ["81", "Unknown"],
59+
}
60+
61+
test += 1
62+
test_data[test] = {
63+
"input": """#######
64+
#a.#Cd#
65+
##...##
66+
##.@.##
67+
##...##
68+
#cB#Ab#
69+
#######""",
70+
"expected": ["Unknown", "8"],
71+
}
72+
73+
test += 1
74+
test_data[test] = {
75+
"input": """#############
76+
#DcBa.#.GhKl#
77+
#.###...#I###
78+
#e#d#.@.#j#k#
79+
###C#...###J#
80+
#fEbA.#.FgHi#
81+
#############""",
82+
"expected": ["Unknown", "32"],
83+
}
84+
85+
test += 1
86+
test_data[test] = {
87+
"input": """#############
88+
#g#f.D#..h#l#
89+
#F###e#E###.#
90+
#dCba...BcIJ#
91+
#####.@.#####
92+
#nK.L...G...#
93+
#M###N#H###.#
94+
#o#m..#i#jk.#
95+
#############""",
96+
"expected": ["Unknown", "72"],
97+
}
98+
99+
test = "real"
100+
input_file = os.path.join(
101+
os.path.dirname(__file__),
102+
"Inputs",
103+
os.path.basename(__file__).replace(".py", ".txt"),
104+
)
105+
test_data[test] = {
106+
"input": open(input_file, "r+").read().strip(),
107+
"expected": ["4844", "Unknown"],
108+
}
109+
110+
# -------------------------------- Control program execution ------------------------- #
111+
112+
case_to_test = "real"
113+
part_to_test = 2
114+
115+
# -------------------------------- Initialize some variables ------------------------- #
116+
117+
puzzle_input = test_data[case_to_test]["input"]
118+
puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1]
119+
puzzle_actual_result = "Unknown"
120+
121+
122+
# -------------------------------- Actual code execution ----------------------------- #
123+
def grid_to_vertices(self, grid, diagonals_allowed=False, wall="#"):
124+
self.vertices = {}
125+
y = 0
126+
127+
for line in grid.splitlines():
128+
for x in range(len(line)):
129+
if line[x] != wall:
130+
self.vertices[x - y * j] = line[x]
131+
y += 1
132+
133+
for source in self.vertices:
134+
for direction in directions_straight:
135+
target = source + direction
136+
if target in self.vertices:
137+
if source in self.edges:
138+
self.edges[source].append(target)
139+
else:
140+
self.edges[source] = [target]
141+
142+
return True
143+
144+
145+
pathfinding.Graph.grid_to_vertices = grid_to_vertices
146+
147+
148+
def breadth_first_search(self, start, end=None):
149+
current_distance = 0
150+
frontier = [(start, 0)]
151+
self.distance_from_start = {start: 0}
152+
self.came_from = {start: None}
153+
154+
while frontier:
155+
vertex, current_distance = frontier.pop(0)
156+
current_distance += 1
157+
neighbors = self.neighbors(vertex)
158+
if not neighbors:
159+
continue
160+
161+
# Stop search when reaching another object
162+
if self.vertices[vertex] not in (".", "@") and vertex != start:
163+
continue
164+
165+
for neighbor in neighbors:
166+
if neighbor in self.distance_from_start:
167+
continue
168+
# Adding for future examination
169+
frontier.append((neighbor, current_distance))
170+
171+
# Adding for final search
172+
self.distance_from_start[neighbor] = current_distance
173+
self.came_from[neighbor] = vertex
174+
175+
if neighbor == end:
176+
return True
177+
178+
if end:
179+
return True
180+
return False
181+
182+
183+
pathfinding.Graph.breadth_first_search = breadth_first_search
184+
185+
186+
def neighbors_part1(self, vertex):
187+
neighbors = {}
188+
for target_item in edges[vertex[0]]:
189+
if target_item == "@":
190+
neighbors[(target_item, vertex[1])] = edges[vertex[0]][target_item]
191+
elif target_item == target_item.lower():
192+
if target_item in vertex[1]:
193+
neighbors[(target_item, vertex[1])] = edges[vertex[0]][target_item]
194+
else:
195+
keys = "".join(sorted([x for x in vertex[1]] + [target_item]))
196+
neighbors[(target_item, keys)] = edges[vertex[0]][target_item]
197+
else:
198+
if target_item.lower() in vertex[1]:
199+
neighbors[(target_item, vertex[1])] = edges[vertex[0]][target_item]
200+
else:
201+
continue
202+
203+
return neighbors
204+
205+
206+
def neighbors_part2(self, vertex):
207+
neighbors = {}
208+
for robot in vertex[0]:
209+
for target_item in edges[robot]:
210+
new_position = vertex[0].replace(robot, target_item)
211+
distance = edges[robot][target_item]
212+
if target_item in "1234":
213+
neighbors[(new_position, vertex[1])] = distance
214+
elif target_item.islower():
215+
if target_item in vertex[1]:
216+
neighbors[(new_position, vertex[1])] = distance
217+
else:
218+
keys = "".join(sorted([x for x in vertex[1]] + [target_item]))
219+
neighbors[(new_position, keys)] = distance
220+
else:
221+
if target_item.lower() in vertex[1]:
222+
neighbors[(new_position, vertex[1])] = distance
223+
224+
return neighbors
225+
226+
227+
# Only the WeightedGraph method is replaced, so that it doesn't impact the first search
228+
if part_to_test == 1:
229+
pathfinding.WeightedGraph.neighbors = neighbors_part1
230+
else:
231+
pathfinding.WeightedGraph.neighbors = neighbors_part2
232+
233+
234+
def dijkstra(self, start, end=None):
235+
current_distance = 0
236+
frontier = [(0, start)]
237+
heapq.heapify(frontier)
238+
self.distance_from_start = {start: 0}
239+
self.came_from = {start: None}
240+
min_distance = float("inf")
241+
242+
while frontier:
243+
current_distance, vertex = heapq.heappop(frontier)
244+
245+
if current_distance > min_distance:
246+
continue
247+
248+
neighbors = self.neighbors(vertex)
249+
if not neighbors:
250+
continue
251+
252+
# print (vertex, min_distance, len(self.distance_from_start))
253+
254+
for neighbor, weight in neighbors.items():
255+
# We've already checked that node, and it's not better now
256+
if neighbor in self.distance_from_start and self.distance_from_start[
257+
neighbor
258+
] <= (current_distance + weight):
259+
continue
260+
261+
# Adding for future examination
262+
heapq.heappush(frontier, (current_distance + weight, neighbor))
263+
264+
# Adding for final search
265+
self.distance_from_start[neighbor] = current_distance + weight
266+
self.came_from[neighbor] = vertex
267+
268+
if len(neighbor[1]) == nb_keys:
269+
min_distance = min(min_distance, current_distance + weight)
270+
271+
return end is None or end in self.distance_from_start
272+
273+
274+
pathfinding.WeightedGraph.dijkstra = dijkstra
275+
276+
277+
maze = pathfinding.Graph()
278+
maze.grid_to_vertices(puzzle_input)
279+
280+
# First, simplify the maze to have only the important items (@, keys, doors)
281+
items = "abcdefghijklmnopqrstuvwxyz" + "abcdefghijklmnopqrstuvwxyz".upper() + "@"
282+
items = maze.grid_search(puzzle_input, items)
283+
nb_keys = len([x for x in items if x in "abcdefghijklmnopqrstuvwxyz"])
284+
285+
if part_to_test == 2:
286+
# Separate the start point
287+
start = items["@"][0]
288+
del items["@"]
289+
items["1"] = [start + northwest]
290+
items["2"] = [start + northeast]
291+
items["3"] = [start + southwest]
292+
items["4"] = [start + southeast]
293+
294+
for dir in directions_straight + [0]:
295+
maze.add_walls([start + dir])
296+
297+
298+
edges = {}
299+
for item in items:
300+
maze.reset_search()
301+
302+
maze.breadth_first_search(items[item][0])
303+
edges[item] = {}
304+
for other_item in items:
305+
if other_item == item:
306+
continue
307+
if items[other_item][0] in maze.distance_from_start:
308+
edges[item][other_item] = maze.distance_from_start[items[other_item][0]]
309+
310+
311+
# Then, perform Dijkstra on the simplified graph
312+
graph = pathfinding.WeightedGraph()
313+
graph.edges = edges
314+
graph.reset_search()
315+
if part_to_test == 1:
316+
graph.dijkstra(("@", ""))
317+
else:
318+
graph.dijkstra(("1234", ""))
319+
320+
puzzle_actual_result = min(
321+
[
322+
graph.distance_from_start[x]
323+
for x in graph.distance_from_start
324+
if len(x[1]) == nb_keys
325+
]
326+
)
327+
328+
329+
# -------------------------------- Outputs / results --------------------------------- #
330+
331+
print("Expected result : " + str(puzzle_expected_result))
332+
print("Actual result : " + str(puzzle_actual_result))

0 commit comments

Comments
 (0)