Advent of Code 2016 solutions https://adventofcode.com/2016/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

11.py 4.8KB

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