Browse Source

Add module doc for some puzzles, tidy up.

master
Chris Smith 7 years ago
parent
commit
5d0f137bcf
7 changed files with 94 additions and 36 deletions
  1. 16
    5
      11.py
  2. 29
    19
      12.py
  3. 11
    11
      13.py
  4. 13
    0
      14.py
  5. 12
    1
      15.py
  6. 6
    0
      16.py
  7. 7
    0
      17.py

+ 16
- 5
11.py View File

@@ -1,5 +1,18 @@
1 1
 #!/usr/bin/python3
2 2
 
3
+"""Solution for day 11 of Advent of Code 2016.
4
+
5
+Performs a breadth-first search of possible moves. Because of the "elevator stops at each floor to charge" mechanic,
6
+moves are treated as up or down a single floor.
7
+
8
+To avoid backtracking, a set of previously visited states is maintained. Before being recorded, each state is
9
+'serialised' by stripping away the element names in a deterministic manner. This is because a puzzle with a
10
+Foo-generator and Bar-chip on floor 1 and a Bar-generator and Foo-chip on floor 2, has an identical solution to
11
+a puzzle with a Bar-Generator and Foo-chip on floor 1, and a Foo-generator and Bar-chip on floor 2. That is, the
12
+elements don't matter, just the relative positions of the pairs. This dramatically reduces the number of states
13
+that have to be checked.
14
+"""
15
+
3 16
 import itertools, re
4 17
 
5 18
 # Marker used to show the position of the lift
@@ -33,8 +46,7 @@ items = lambda floor: set(floor) - {lift}
33 46
 pickups = lambda items: map(set, itertools.chain(itertools.combinations(items, 2), itertools.combinations(items, 1)))
34 47
 
35 48
 # Returns an enumeration of possible destinations for the lift (up or down one floor)
36
-dests = lambda layout: filter(is_floor, [my_floor_index(layout) + 1, my_floor_index(layout) - 1])
37
-is_floor = lambda i: 0 <= i < len(floors)
49
+dests = lambda layout: filter(lambda i: 0 <= i < len(floors), [my_floor_index(layout) + 1, my_floor_index(layout) - 1])
38 50
 
39 51
 # Returns an enumeration of possible moves that could be made from the given state
40 52
 moves = lambda layout: itertools.product(pickups(items(my_floor(layout))), dests(layout))
@@ -78,16 +90,15 @@ def run(floors):
78 90
             if valid_layout(new_layout):
79 91
                 serialised = serialise(new_layout)
80 92
                 if serialised not in distances:
81
-                    distances[serialised] = steps
93
+                    distances.add(serialised)
82 94
                     queued.append(new_layout)
83 95
                     if target(new_layout):
84
-
85 96
                         return False
86 97
         return queued
87 98
 
88 99
     # Run repeated iterations until we hit a winning result, then immediately returns the step
89 100
     # count.
90
-    distances = {serialise(floors): 0}
101
+    distances = {serialise(floors)}
91 102
     step = 1
92 103
     queued = [floors]
93 104
     while True:

+ 29
- 19
12.py View File

@@ -1,30 +1,40 @@
1 1
 #!/usr/bin/python3
2 2
 
3
+"""Solution for day 12 of Advent of Code 2016.
4
+
5
+Models a simple virtual machine. Each of the four operations is defined as a method, and called using reflection.
6
+The current line number is kept in a special 'pc' (for 'program counter') register, which means that each operation is
7
+a simple mutation of a register.
8
+
9
+All values are handled by first trying to parse the input as an integer; if that fails then it is treated as a register
10
+name and the value of the register is returned. This allows any argument to be numerical or a register, not just those
11
+shown int he question example.
12
+"""
13
+
14
+
3 15
 with open('data/12.txt', 'r') as file:
4 16
     instr = list(map(str.split, map(str.strip, file.readlines())))
5 17
 
6
-def value(x):
7
-    try:
8
-        return int(x)
9
-    except ValueError:
10
-        return registers[x]
18
+def run(values):
19
+    registers = values
20
+    registers['pc'] = 0
11 21
 
12
-def cpy(args): registers[args[1]] = value(args[0])
13
-def inc(args): registers[args[0]] += 1
14
-def dec(args): registers[args[0]] -= 1
15
-def jnz(args): registers['pc'] += 0 if value(args[0]) == 0 else value(args[1]) - 1
22
+    def value(x):
23
+        try:
24
+            return int(x)
25
+        except ValueError:
26
+            return registers[x]
27
+
28
+    def cpy(args): registers[args[1]] = value(args[0])
29
+    def inc(args): registers[args[0]] += 1
30
+    def dec(args): registers[args[0]] -= 1
31
+    def jnz(args): registers['pc'] += 0 if value(args[0]) == 0 else value(args[1]) - 1
16 32
 
17
-def run():
18 33
     while registers['pc'] < len(instr):
19
-        globals()[instr[registers['pc']][0]](instr[registers['pc']][1:])
34
+        locals()[instr[registers['pc']][0]](instr[registers['pc']][1:])
20 35
         registers['pc'] += 1
21 36
 
22
-registers = {'a': 0, 'b': 0, 'c': 0, 'd': 0, 'pc': 0}
23
-run()
24
-
25
-print("Stage 1: %s" % registers['a'])
26
-
27
-registers = {'a': 0, 'b': 0, 'c': 1, 'd': 0, 'pc': 0}
28
-run()
37
+    return registers['a']
29 38
 
30
-print("Stage 2: %s" % registers['a'])
39
+print("Stage 1: %s" % run({'a': 0, 'b': 0, 'c': 0, 'd': 0}))
40
+print("Stage 2: %s" % run({'a': 0, 'b': 0, 'c': 1, 'd': 0}))

+ 11
- 11
13.py View File

@@ -1,5 +1,11 @@
1 1
 #!/usr/bin/python3
2 2
 
3
+"""Solution for day 13 of Advent of Code 2016.
4
+
5
+Performs a breadth-first search of the maze, for up to 100 steps. Distances of each square are kept in a dictionary,
6
+which is also used to prevent back-tracking.
7
+"""
8
+
3 9
 import itertools
4 10
 
5 11
 input = 1364
@@ -9,19 +15,13 @@ wall = lambda x, y: high(x*x + 3*x + 2*x*y + y + y*y + input) % 2 == 1
9 15
 all_moves = lambda x, y: [(x-1, y), (x+1, y), (x, y-1), (x, y+1)]
10 16
 moves = lambda x, y: (m for m in all_moves(x, y) if m[0] >= 0 and m[1] >= 0 and not wall(*m))
11 17
 queue = {(1, 1)}
12
-visited = set()
13 18
 distance = {}
14
-steps = 0
15
-
16
-while steps < 100:
17
-	for c in queue:
18
-		if c not in distance:
19
-			distance[c] = steps
20 19
 
21
-	visited = visited.union(queue)
22
-	next_queue = set(itertools.chain.from_iterable(moves(*c) for c in queue)) - visited
23
-	queue = next_queue
24
-	steps += 1
20
+for step in range(100):
21
+    # Merge in all the new distances from the queue, if they're not present
22
+    distance = dict(list(distance.items()) + list(dict.fromkeys(queue, step).items()))
23
+    # Calculate all moves from all places in the queue, and skip any places we've already visited
24
+    queue = set(itertools.chain.from_iterable(moves(*c) for c in queue)) - set(distance.keys())
25 25
 
26 26
 print("Part 1: %s" % distance[(31, 39)])
27 27
 print("Part 2: %s" % sum(1 for s in distance.values() if s <= 50))

+ 13
- 0
14.py View File

@@ -1,5 +1,18 @@
1 1
 #!/usr/bin/python3
2 2
 
3
+"""Solution for day 14 of Advent of Code 2016.
4
+
5
+The actual logic for this solution is contained in the matches lambda, which returns an infinite-length generator
6
+containing all of the matching hashes. The implementation is very basic, calling the hash functions and triple-digit
7
+matcher multiple times for the same inputs. This avoids having to manually keep a second list of hashes to check for
8
+five letter matches.
9
+
10
+To avoid the massive slowness that would happen if we recomputed up to 1,000 hashes each step, the hash methods (and
11
+triple-finding method) themselves are cached using functools.lru_cache. The caches can hold 1024 entries, so
12
+comfortably hold the results of the 1,000 hash look-forward.
13
+"""
14
+
15
+
3 16
 import functools
4 17
 import hashlib
5 18
 import itertools

+ 12
- 1
15.py View File

@@ -1,5 +1,16 @@
1 1
 #!/usr/bin/python3
2 2
 
3
+"""Solution for day 15 of Advent of Code 2016.
4
+
5
+To simplify a lot of the logic in this solution, disc positions are offset according to their position. So if we have
6
+discs that start at positions [0, 1, 2] at t=0, when calculating positions the entry for t=0 will read [1, 3, 5]
7
+because the first disc will have moved one position before the capsule reaches it, the second disc two positions,
8
+and so on.
9
+
10
+Offsetting the positions means that a successful solution is represented as [0, 0, 0] instead of [n, n-1, n-2] etc.
11
+The latter case becomes particularly ugly when you consider that the discs may wrap around at different places.
12
+"""
13
+
3 14
 import itertools
4 15
 import re
5 16
 
@@ -21,7 +32,7 @@ def run(lines):
21 32
     combos = zip(*map(positions, discs))
22 33
 
23 34
     # Find a time when all discs will be at position 0 for the arrival of the capsule.
24
-    times = (i for i, c in enumerate(combos) if all(p == 0 for p in c))
35
+    times = (i for i, c in enumerate(combos) if not(sum(c)))
25 36
     return next(times)
26 37
 
27 38
 with open('data/15.txt', 'r') as file:

+ 6
- 0
16.py View File

@@ -1,5 +1,11 @@
1 1
 #!/usr/bin/python3
2 2
 
3
+"""Solution for day 16 of Advent of Code 2016.
4
+
5
+This is a straightforward, brute-searching solution. It expands the input as defined in the puzzle, and then
6
+computes checksums until it finds an odd-length one.
7
+"""
8
+
3 9
 import itertools
4 10
 
5 11
 

+ 7
- 0
17.py View File

@@ -1,5 +1,12 @@
1 1
 #!/usr/bin/python3
2 2
 
3
+"""Solution for day 17 of Advent of Code 2016.
4
+
5
+Implements an A* search algorithm for finding the shortest path (which wasn't really necessary given part two!)
6
+The heapq module is used to keep the queue sorted in priority order, with the priorities calculated as the
7
+current distance travelled plus the Manhattan distance to the end (so the first path found will be the shortest).
8
+"""
9
+
3 10
 import functools
4 11
 import hashlib
5 12
 import heapq

Loading…
Cancel
Save