|
@@ -0,0 +1,87 @@
|
|
1
|
+#!/usr/bin/python3
|
|
2
|
+
|
|
3
|
+"""Solution for day 24 of Advent of Code 2016.
|
|
4
|
+
|
|
5
|
+Another day, another breadth-first search! I split this problem into two stages: first, computing the distance
|
|
6
|
+between every combination of points in the maze, and, second, finding the shortest route using those paths.
|
|
7
|
+
|
|
8
|
+Finding the distances is just a simple BFS. As we only care about distance, each pair of nodes is only calculated
|
|
9
|
+once (i.e., it stores a distance for, e.g., (0 -> 2) but not (2 -> 0)). Pairs are stored sorted.
|
|
10
|
+
|
|
11
|
+To find all the possible routes, we just need a call to itertools.permutations for the middle section (ignoring '0').
|
|
12
|
+The '0' stop(s) are then added around the permutation, and the total distance calculated by chunking them into pairs
|
|
13
|
+and looking them up in the distances table. This is done in the route_length method.
|
|
14
|
+
|
|
15
|
+For example, with the example maze with 5 points we compute and store 10 distances:
|
|
16
|
+ 0 -> 1, 0 -> 2, 0 -> 3, 0 -> 4, 1 -> 2, 1 -> 3, 1 -> 4, 2 -> 3, 2-> 4, 3-> 4
|
|
17
|
+
|
|
18
|
+Which become tuple keys in our distances dictionary:
|
|
19
|
+ (0, 1): a, (0, 2): b, ... (3, 4): z
|
|
20
|
+
|
|
21
|
+Then to find the route we start with all 24 possible routes:
|
|
22
|
+ 01234, 01243, 01324, 01342, 01423, 01432, 02134, etc...
|
|
23
|
+
|
|
24
|
+Each route is chunked into pairs:
|
|
25
|
+ 01234 ==> (0, 1), (1, 2), (2, 3), (3, 4)
|
|
26
|
+
|
|
27
|
+And the pairs are then looked up in the dictionary and summed.
|
|
28
|
+
|
|
29
|
+The problem could in theory be solved using one large BFS but it would be fairly complicated as you'd need to prevent
|
|
30
|
+backtracking generally, while still allowing it after reaching a new numbered location. You'd also end up recalculating
|
|
31
|
+the distances between many places unless you did some clever optimisations. Pre-computing the distances also makes part
|
|
32
|
+2 very easy.
|
|
33
|
+"""
|
|
34
|
+
|
|
35
|
+import functools
|
|
36
|
+import itertools
|
|
37
|
+import operator
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+def find(maze, num):
|
|
41
|
+ for y, line in enumerate(maze):
|
|
42
|
+ if num in line:
|
|
43
|
+ return line.index(num), y
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+def navigable(maze, location):
|
|
47
|
+ return maze[location[1]][location[0]] != '#'
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+def directions(location):
|
|
51
|
+ yield (location[0], location[1] + 1)
|
|
52
|
+ yield (location[0], location[1] - 1)
|
|
53
|
+ yield (location[0] + 1, location[1])
|
|
54
|
+ yield (location[0] - 1, location[1])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+def moves(maze, start):
|
|
58
|
+ return set(filter(lambda loc: navigable(maze, loc), directions(start)))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+def distance(maze, start_num, end_num):
|
|
62
|
+ start = find(maze, start_num)
|
|
63
|
+ end = find(maze, end_num)
|
|
64
|
+ visited = set()
|
|
65
|
+ queue = [start]
|
|
66
|
+ steps = 0
|
|
67
|
+ while len(queue):
|
|
68
|
+ new_queue = functools.reduce(operator.or_, [moves(maze, target) for target in queue])
|
|
69
|
+ queue = new_queue - visited
|
|
70
|
+ visited |= queue
|
|
71
|
+ steps += 1
|
|
72
|
+ if end in queue:
|
|
73
|
+ return steps
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+def route_length(distances, route):
|
|
77
|
+ return sum(map(lambda p: distances[tuple(sorted(p))], (route[i:i +2] for i in range(0, len(route)-1))))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+with open('data/24.txt', 'r') as file:
|
|
81
|
+ my_maze = list(map(str.strip, file.readlines()))
|
|
82
|
+ points = sorted(list(x for x in itertools.chain(*my_maze) if x.isdigit()))
|
|
83
|
+ distances = dict(((pair, distance(my_maze, *pair)) for pair in itertools.combinations(points, 2)))
|
|
84
|
+ routes = set(itertools.permutations(points[1:]))
|
|
85
|
+
|
|
86
|
+ print('Part one: %s' % min(route_length(distances, ['0'] + [*route]) for route in routes))
|
|
87
|
+ print('Part two: %s' % min(route_length(distances, ['0'] + [*route] + ['0']) for route in routes))
|