Advertisement
alexandrajay2002

Advent of Code 2024 day 20 part 1

Dec 20th, 2024
101
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 4.03 KB | Source Code | 0 0
  1. from enum import Enum
  2. from functools import 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. def make_graph(grid):
  55.     start = find_tile(grid, 'S')
  56.     end = find_tile(grid, 'E')
  57.  
  58.     seen = {start, }
  59.     edges = defaultdict(set)
  60.     cheats = defaultdict(set)  # a cheat is effectively a bonus edge
  61.  
  62.     todo = {start, }
  63.     while len(todo) > 0:
  64.         node = todo.pop()
  65.         for direction in Direction:
  66.             neighbour = direction.move(*node)
  67.  
  68.             if not on_grid(*neighbour, grid):
  69.                 continue
  70.  
  71.             if at(grid, *neighbour) != '#':
  72.                 # valid neighbour
  73.                 edges[node].add(neighbour)
  74.  
  75.                 if neighbour not in seen:
  76.                     todo.add(neighbour)
  77.                     seen.add(neighbour)
  78.  
  79.                 continue
  80.  
  81.             after = direction.move(*neighbour)
  82.             if on_grid(*after, grid) and at(grid, *after) != '#':
  83.                 # if neighbour is a 1 tile thick wall, add it to cheats
  84.                 cheats[node].add(after)
  85.  
  86.     return start, end, edges, cheats
  87.  
  88.  
  89. def dijkstra(start, edges):
  90.     discovered = {start, }
  91.     distance = ScoreMap({start: 0})
  92.  
  93.     while len(discovered) > 0:
  94.         current_node = min(discovered, key=lambda x: distance[x])
  95.         discovered.remove(current_node)
  96.  
  97.         for neighbour in edges[current_node]:
  98.             candidate_score = distance[current_node] + 1
  99.             if candidate_score < distance[neighbour]:
  100.                 distance[neighbour] = candidate_score
  101.                 discovered.add(neighbour)
  102.  
  103.     return distance
  104.  
  105.  
  106. def multi_items(mapping):
  107.     for key, items in mapping.items():
  108.         for item in items:
  109.             yield key, item
  110.  
  111.  
  112. def main(grid, time_save, verbose):
  113.     start, end, edges, cheats = make_graph(grid)
  114.     distance_from_start = dijkstra(start, edges)
  115.     distance_from_end = dijkstra(end, edges)
  116.     target_distance = distance_from_start[end] - time_save
  117.  
  118.     cheat_count = defaultdict(int)
  119.     total = 0
  120.     for cheat_start, cheat_end in multi_items(cheats):
  121.         dist = distance_from_start[cheat_start] + 2 \
  122.              + distance_from_end[cheat_end]
  123.         if dist <= target_distance:
  124.             cheat_count[distance_from_start[end] - dist] += 1
  125.             total += 1
  126.  
  127.     if verbose:
  128.         for save, number in sorted(cheat_count.items(), key=lambda x: x[0]):
  129.             if number == 1:
  130.                 print(f'There is one cheat that saves {save} picoseconds')
  131.             else:
  132.                 print(
  133.                     f'There are {number} cheats that save {save} picoseconds')
  134.  
  135.     return total
  136.  
  137.  
  138. arg_parser = ArgumentParser()
  139. arg_parser.add_argument('src', type=FileType('r'))
  140. arg_parser.add_argument('time_save', nargs='?', type=int, default=100)
  141. arg_parser.add_argument('-v', '--verbose', action='store_true')
  142.  
  143. if __name__ == '__main__':
  144.     args = arg_parser.parse_args()
  145.     print(main(parse(args.src.read()), args.time_save, args.verbose))
  146.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement