Advertisement
alexandrajay2002

Advent of Code 2024 day 20 part 2

Dec 20th, 2024
107
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 4.67 KB | Source Code | 0 0
  1. from enum import Enum
  2. from functools import cache, cached_property
  3. from collections import defaultdict
  4. from math import inf
  5. from argparse import ArgumentParser, FileType
  6.  
  7.  
  8. class Direction(Enum):
  9.     '''A cardinal direction.'''
  10.     UP = 0
  11.     LEFT = 1
  12.     DOWN = 2
  13.     RIGHT = 3
  14.  
  15.     @property
  16.     def horisontal(self):
  17.         return self in (Direction.LEFT, Direction.RIGHT)
  18.  
  19.     @cached_property
  20.     def delta(self):
  21.         return ((0, -1), (1, 0), (0, 1), (-1, 0))[self.value]
  22.  
  23.     def move(self, x, y):
  24.         '''Translate the coordinates a distance of 1 in this direction.'''
  25.         dx, dy = self.delta
  26.         return x + dx, y + dy
  27.  
  28.  
  29. def parse(src):
  30.     return [line for line in src.splitlines() if line != '']
  31.  
  32.  
  33. def find_tile(grid, tile):
  34.     for y, line in enumerate(grid):
  35.         for x, char in enumerate(line):
  36.             if char == tile:
  37.                 return x, y
  38.  
  39.     raise ValueError(f'Could not find "{tile}" in grid')
  40.  
  41.  
  42. def ScoreMap(mapping):
  43.     return defaultdict(lambda: inf, mapping)
  44.  
  45.  
  46. def on_grid(x, y, grid):
  47.     return 0 <= y < len(grid) and 0 <= x < len(grid[0])
  48.  
  49.  
  50. def at(grid, x, y):
  51.     return grid[y][x]
  52.  
  53.  
  54. @cache
  55. def taxicab_circle(x, y, r):
  56.     for offset in range(r):
  57.         inv_offset = r - offset
  58.         yield x + offset, y + inv_offset
  59.         yield x + inv_offset, y - offset
  60.         yield x - offset, y - inv_offset
  61.         yield x - inv_offset, y + offset
  62.  
  63.  
  64. def open_space(grid):
  65.     def check(coord):
  66.         return on_grid(*coord, grid) and at(grid, *coord) != '#'
  67.     return check
  68.  
  69.  
  70. def get_cheats(x, y, grid, cheat_time):
  71.     for radius in range(2, cheat_time + 1):
  72.         valid_cheats = filter(open_space(grid), taxicab_circle(x, y, radius))
  73.         yield from ((cheat, radius) for cheat in valid_cheats)
  74.  
  75.  
  76. def make_graph(grid, cheat_time):
  77.     valid_neighbour = open_space(grid)
  78.  
  79.     start = find_tile(grid, 'S')
  80.     end = find_tile(grid, 'E')
  81.  
  82.     seen = {start, }
  83.     edges = defaultdict(set)
  84.     cheats = dict()  # a cheat is effectively a bonus edge
  85.  
  86.     todo = {start, }
  87.     while len(todo) > 0:
  88.         node = todo.pop()
  89.  
  90.         # find this node's neighbours
  91.         for direction in Direction:
  92.             neighbour = direction.move(*node)
  93.             if valid_neighbour(neighbour):
  94.                 # valid neighbour
  95.                 edges[node].add(neighbour)
  96.  
  97.                 if neighbour not in seen:
  98.                     todo.add(neighbour)
  99.                     seen.add(neighbour)
  100.  
  101.         # find the cheats for this node
  102.         cheats[node] = set(get_cheats(*node, grid, cheat_time))
  103.  
  104.     return start, end, edges, cheats
  105.  
  106.  
  107. def dijkstra(start, edges):
  108.     discovered = {start, }
  109.     distance = ScoreMap({start: 0})
  110.  
  111.     while len(discovered) > 0:
  112.         current_node = min(discovered, key=lambda x: distance[x])
  113.         discovered.remove(current_node)
  114.  
  115.         for neighbour in edges[current_node]:
  116.             candidate_score = distance[current_node] + 1
  117.             if candidate_score < distance[neighbour]:
  118.                 distance[neighbour] = candidate_score
  119.                 discovered.add(neighbour)
  120.  
  121.     return distance
  122.  
  123.  
  124. def multi_items(mapping):
  125.     for key, items in mapping.items():
  126.         for item in items:
  127.             yield key, *item
  128.  
  129.  
  130. def main(grid, time_save, cheat_time, verbose):
  131.     start, end, edges, cheats = make_graph(grid, cheat_time)
  132.     distance_from_start = dijkstra(start, edges)
  133.     distance_from_end = dijkstra(end, edges)
  134.     target_distance = distance_from_start[end] - time_save
  135.  
  136.     cheat_count = defaultdict(int)
  137.     total = 0
  138.     for cheat_start, cheat_end, cheat_cost in multi_items(cheats):
  139.         dist = distance_from_start[cheat_start] + cheat_cost \
  140.              + distance_from_end[cheat_end]
  141.         if dist <= target_distance:
  142.             cheat_count[distance_from_start[end] - dist] += 1
  143.             total += 1
  144.  
  145.     if verbose:
  146.         for save, number in sorted(cheat_count.items(), key=lambda x: x[0]):
  147.             if number == 1:
  148.                 print(f'There is one cheat that saves {save} picoseconds')
  149.             else:
  150.                 print(
  151.                     f'There are {number} cheats that save {save} picoseconds')
  152.  
  153.     return total
  154.  
  155.  
  156. arg_parser = ArgumentParser()
  157. arg_parser.add_argument('src', type=FileType('r'))
  158. arg_parser.add_argument('time_save', nargs='?', type=int, default=100)
  159. arg_parser.add_argument('cheat_time', nargs='?', type=int, default=20)
  160. arg_parser.add_argument('-v', '--verbose', action='store_true')
  161.  
  162. if __name__ == '__main__':
  163.     args = arg_parser.parse_args()
  164.     grid = parse(args.src.read())
  165.     print(main(grid, args.time_save, args.cheat_time, args.verbose))
  166.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement