123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119 |
- #!/usr/bin/python3
-
- """Solution for day 11 of Advent of Code 2016.
-
- Performs a breadth-first search of possible moves. Because of the "elevator stops at each floor to charge" mechanic,
- moves are treated as up or down a single floor.
-
- To avoid backtracking, a set of previously visited states is maintained. Before being recorded, each state is
- 'serialised' by stripping away the element names in a deterministic manner. This is because a puzzle with a
- Foo-generator and Bar-chip on floor 1 and a Bar-generator and Foo-chip on floor 2, has an identical solution to
- 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
- elements don't matter, just the relative positions of the pairs. This dramatically reduces the number of states
- that have to be checked.
- """
-
- import itertools, re
-
- # Marker used to show the position of the lift
- lift = '*YOU ARE HERE*'
-
- # Read the input
- with open('data/11.txt', 'r') as file:
- lines = list(map(str.strip, file.readlines()))
- floors = [re.findall(r'\b(\S+ (?:generator|microchip))\b', line) for line in lines]
- floors[0].append(lift)
-
- # Return the elements of the chips or generators in the given lists
- chips = lambda items: set([item.split('-')[0] for item in items if item.endswith('microchip')])
- genrs = lambda items: set([item.split(' ')[0] for item in items if item.endswith('generator')])
-
- # Verify that if there are generators, then all microchips present are paired
- valid_floor = lambda floor: not len(genrs(floor)) or not len(chips(floor) - genrs(floor))
- valid_layout = lambda layout: False not in [valid_floor(floor) for floor in layout]
-
- # We win when everything is on the last floor (i.e., nothing is on the other floors)
- target = lambda layout: sum(len(floor) for floor in layout[:-1]) == 0
-
- # Returns the floor/floor index the lift is currently on
- my_floor = lambda layout: next(floor for floor in layout if lift in floor)
- my_floor_index = lambda layout: next(i for i, floor in enumerate(layout) if lift in floor)
-
- # Returns just the items on a floor (not the lift)
- items = lambda floor: set(floor) - {lift}
-
- # Returns an enumeration of sets of items that could potentially be picked up (any combo of 1 or 2 items)
- pickups = lambda items: map(set, itertools.chain(itertools.combinations(items, 2), itertools.combinations(items, 1)))
-
- # Returns an enumeration of possible destinations for the lift (up or down one floor)
- dests = lambda layout: filter(lambda i: 0 <= i < len(floors), [my_floor_index(layout) + 1, my_floor_index(layout) - 1])
-
- # Returns an enumeration of possible moves that could be made from the given state
- moves = lambda layout: itertools.product(pickups(items(my_floor(layout))), dests(layout))
-
- # Finds a floor that contains the given item
- find = lambda item, layout: next(i for i, floor in enumerate(layout) if item in floor)
-
-
- # Performs a breadth-first search over all moves for the given layout in order to find the number
- # of steps needed to get to a winning state.
- def run(floors):
-
- # The available types depends on the input (and thus differs between calls to the run function),
- # so we have to calculate it here, and make the serialise() and domoves() functions closures
- # over this list.
- types = [chip.split(' ')[0] for chip in itertools.chain.from_iterable(chips(items(floor)) for floor in floors)]
-
- # Serialises a layout into a string, for easy storage.
- # Items are replaced with numeric identifiers, determined based on position of the generator and
- # chip of that type. This means that layouts that are identical except for the elements being
- # swapped around serialise to the same string (as the process for moving them to the end will be
- # the) same.
- def serialise(layout):
- keys = sorted(types, key=lambda t: find(t + ' generator', layout) * len(layout)
- + find(t + '-compatible microchip', layout))
- mappings = {lift: '*'}
- for i, key in enumerate(keys):
- mappings['%s generator' % key] = '%iG' % i
- mappings['%s-compatible microchip' % key] = '%iM' % i
- return '|'.join(''.join(sorted(mappings[item] for item in floor)) for floor in layout)
-
- # Evaluates each possible move for the given layout.
- # Moves are checked to ensure they're valid and serialised to ensure they haven't been visited
- # Returns a list of new layouts for the next step, or False if a solution was encountered
- def domoves(layout, steps):
- queued = []
- for items, to in moves(layout):
- items = set(items).union({lift})
- new_layout = [set(floor) - items for floor in layout]
- new_layout[to] |= items
- if valid_layout(new_layout):
- serialised = serialise(new_layout)
- if serialised not in distances:
- distances.add(serialised)
- queued.append(new_layout)
- if target(new_layout):
- return False
- return queued
-
- # Run repeated iterations until we hit a winning result, then immediately returns the step
- # count.
- distances = {serialise(floors)}
- step = 1
- queued = [floors]
- while True:
- next_queue = []
- for layout in queued:
- res = domoves(layout, step)
- if res == False:
- return step
- next_queue.extend(res)
- queued = next_queue
- step += 1
-
- print("Part 1: %s" % run(floors))
-
- floors[0].extend(['elerium generator', 'elerium-compatible microchip',
- 'dilithium generator', 'dilithium-compatible microchip'])
-
- print("Part 2: %s" % run(floors))
|