|
@@ -0,0 +1,62 @@
|
|
1
|
+#!/usr/bin/python3
|
|
2
|
+
|
|
3
|
+import functools
|
|
4
|
+import hashlib
|
|
5
|
+import heapq
|
|
6
|
+import itertools
|
|
7
|
+
|
|
8
|
+salt = 'njfxhljp'
|
|
9
|
+moves = {'U': (0, -1), 'D': (0, +1), 'L': (-1, 0), 'R': (+1, 0)}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+def open_doors(path):
|
|
13
|
+ """Gives a list of open doors around the current square, given the path used to reach it.
|
|
14
|
+
|
|
15
|
+ Does not take into account whether doors actually exist or not.
|
|
16
|
+
|
|
17
|
+ :param path: The path taken to reach the square (as a string, one char per move; e.g. 'UDUDRLRLLL')
|
|
18
|
+ :return: Iterator of directions which have open doors
|
|
19
|
+ """
|
|
20
|
+ return itertools.compress('UDLR', [x > 'a' for x in hashlib.md5((salt + path).encode('UTF-8')).hexdigest()[:4]])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+def next_moves(coords, path):
|
|
24
|
+ """Gets a list of all possible moves from the given position tuple.
|
|
25
|
+
|
|
26
|
+ Takes in to account the maze bounds and door states.
|
|
27
|
+
|
|
28
|
+ A move's "priority" is the length of the path taken so far, plus the Manhattan distance to the end goal.
|
|
29
|
+ This allows us to use a best-first searching algorithm like A* to efficiently find paths to the end.
|
|
30
|
+
|
|
31
|
+ :param coords: The current co-ordinates, as a tuple of (x, y).
|
|
32
|
+ :param path: The path taken to reach the square (as a string, one char per move; e.g. 'UDUDRLRLLL')
|
|
33
|
+ :return: An (unsorted) iterator of new potential positions, as tuples of (priority, co-ordinates, path)
|
|
34
|
+ """
|
|
35
|
+ for direction in open_doors(path):
|
|
36
|
+ new_coords = tuple(map(sum, zip(coords, moves[direction])))
|
|
37
|
+ if 0 <= new_coords[0] <= 3 and 0 <= new_coords[1] <= 3:
|
|
38
|
+ yield ((len(path) + 7 - coords[0] - coords[1], new_coords, path + direction))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+def find_paths():
|
|
42
|
+ """Finds all paths from the starting point of (0, 0) to the end point of (3, 3).
|
|
43
|
+
|
|
44
|
+ Paths are searched in priority order, meaning the shortest path is returned first and the longest path returned
|
|
45
|
+ last.
|
|
46
|
+
|
|
47
|
+ :return: A generator which emits paths (as strings of directions, e.g. 'UDDDRRR...') from shortest to longest
|
|
48
|
+ """
|
|
49
|
+ queue = []
|
|
50
|
+ heapq.heappush(queue, (6, (0, 0), ''))
|
|
51
|
+ while len(queue):
|
|
52
|
+ _, coords, path = heapq.heappop(queue)
|
|
53
|
+ if coords == (3, 3):
|
|
54
|
+ yield path
|
|
55
|
+ else:
|
|
56
|
+ for move in next_moves(coords, path):
|
|
57
|
+ heapq.heappush(queue, move)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+paths = find_paths()
|
|
61
|
+print('Step 1: %s' % next(paths))
|
|
62
|
+print('Step 2: %s' % len(functools.reduce(lambda x, y: y, paths)))
|