|
@@ -0,0 +1,110 @@
|
|
1
|
+#!/usr/bin/python3
|
|
2
|
+
|
|
3
|
+import itertools, re
|
|
4
|
+
|
|
5
|
+# Marker used to show the position of the lift
|
|
6
|
+lift = '*YOU ARE HERE*'
|
|
7
|
+
|
|
8
|
+# Read the input
|
|
9
|
+with open('11.txt', 'r') as file:
|
|
10
|
+ lines = list(map(str.strip, file.readlines()))
|
|
11
|
+ floors = [re.findall(r'\b(\S+ (?:generator|microchip))\b', line) for line in lines]
|
|
12
|
+ floors[0].append(lift)
|
|
13
|
+
|
|
14
|
+# Return the elements of the chips or generators in the given lists
|
|
15
|
+chips = lambda items: set([item.split('-')[0] for item in items if item.endswith('microchip')])
|
|
16
|
+genrs = lambda items: set([item.split(' ')[0] for item in items if item.endswith('generator')])
|
|
17
|
+
|
|
18
|
+# Verify that if there are generators, then all microchips present are paired
|
|
19
|
+valid_floor = lambda floor: not len(genrs(floor)) or not len(chips(floor) - genrs(floor))
|
|
20
|
+valid_layout = lambda layout: False not in [valid_floor(floor) for floor in layout]
|
|
21
|
+
|
|
22
|
+# We win when everything is on the last floor (i.e., nothing is on the other floors)
|
|
23
|
+target = lambda layout: sum(len(floor) for floor in layout[:-1]) == 0
|
|
24
|
+
|
|
25
|
+# Returns the floor/floor index the lift is currently on
|
|
26
|
+my_floor = lambda layout: next(floor for floor in layout if lift in floor)
|
|
27
|
+my_floor_index = lambda layout: next(i for i, floor in enumerate(layout) if lift in floor)
|
|
28
|
+
|
|
29
|
+# Returns just the items on a floor (not the lift)
|
|
30
|
+items = lambda floor: set(floor) - set([lift])
|
|
31
|
+
|
|
32
|
+# Returns an enumeration of sets of items that could potentially be picked up (any combo of 1 or 2 items)
|
|
33
|
+pickups = lambda items: map(set, itertools.chain(itertools.combinations(items, 2), itertools.combinations(items, 1)))
|
|
34
|
+
|
|
35
|
+# 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: i >= 0 and i < len(floors)
|
|
38
|
+
|
|
39
|
+# Returns an enumeration of possible moves that could be made from the given state
|
|
40
|
+moves = lambda layout: itertools.product(pickups(items(my_floor(layout))), dests(layout))
|
|
41
|
+
|
|
42
|
+# Finds a floor that contains the given item
|
|
43
|
+find = lambda item, layout: next(i for i, floor in enumerate(layout) if item in floor)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+# Performs a breadth-first search over all moves for the given layout in order to find the number
|
|
47
|
+# of steps needed to get to a winning state.
|
|
48
|
+def run(floors):
|
|
49
|
+
|
|
50
|
+ # The available types depends on the input (and thus differs between calls to the run function),
|
|
51
|
+ # so we have to calculate it here, and make the serialise() and domoves() functions closures
|
|
52
|
+ # over this list.
|
|
53
|
+ types = [chip.split(' ')[0] for chip in itertools.chain.from_iterable(chips(items(floor)) for floor in floors)]
|
|
54
|
+
|
|
55
|
+ # Serialises a layout into a string, for easy storage.
|
|
56
|
+ # Items are replaced with numeric identifiers, determined based on position of the generator and
|
|
57
|
+ # chip of that type. This means that layouts that are identical except for the elements being
|
|
58
|
+ # swapped around serialise to the same string (as the process for moving them to the end will be
|
|
59
|
+ # the) same.
|
|
60
|
+ def serialise(layout):
|
|
61
|
+ keys = sorted(types, key=lambda t: find(t + ' generator', layout) * len(layout)
|
|
62
|
+ + find(t + '-compatible microchip', layout))
|
|
63
|
+ mappings = {lift: '*'}
|
|
64
|
+ for i, key in enumerate(keys):
|
|
65
|
+ mappings['%s generator' % key] = '%iG' % i
|
|
66
|
+ mappings['%s-compatible microchip' % key] = '%iM' % i
|
|
67
|
+ return '|'.join(''.join(sorted(mappings[item] for item in floor)) for floor in layout)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+ # Evaluates each possible move for the given layout.
|
|
71
|
+ # Moves are checked to ensure they're valid and serialised to ensure they haven't been visited
|
|
72
|
+ # Returns a list of new layouts for the next step, or False if a solution was encountered
|
|
73
|
+ def domoves(layout, steps):
|
|
74
|
+ queued = []
|
|
75
|
+ for items, to in moves(layout):
|
|
76
|
+ items = set(items).union(set([lift]))
|
|
77
|
+ new_layout = [set(floor) - items for floor in layout]
|
|
78
|
+ new_layout[to] |= (items)
|
|
79
|
+ if valid_layout(new_layout):
|
|
80
|
+ serialised = serialise(new_layout)
|
|
81
|
+ if serialised not in distances:
|
|
82
|
+ distances[serialised] = steps
|
|
83
|
+ queued.append(new_layout)
|
|
84
|
+ if target(new_layout):
|
|
85
|
+
|
|
86
|
+ return False
|
|
87
|
+ return queued
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+ # Run repeated iterations until we hit a winning result, then immediately returns the step
|
|
91
|
+ # count.
|
|
92
|
+ distances = {serialise(floors): 0}
|
|
93
|
+ step = 1
|
|
94
|
+ queued = [floors]
|
|
95
|
+ while True:
|
|
96
|
+ next_queue = []
|
|
97
|
+ for layout in queued:
|
|
98
|
+ res = domoves(layout, step)
|
|
99
|
+ if res == False:
|
|
100
|
+ return step
|
|
101
|
+ next_queue.extend(res)
|
|
102
|
+ queued = next_queue
|
|
103
|
+ step += 1
|
|
104
|
+
|
|
105
|
+print("Part 1: %s" % run(floors))
|
|
106
|
+
|
|
107
|
+floors[0].extend(['elerium generator', 'elerium-compatible microchip',
|
|
108
|
+ 'dilithium generator', 'dilithium-compatible microchip'])
|
|
109
|
+
|
|
110
|
+print("Part 2: %s" % run(floors))
|