Advertisement
JonathanGupton

Advent of Code 2024 - Day 12 - Python

Dec 12th, 2024 (edited)
155
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 6.38 KB | None | 0 0
  1. from collections import deque
  2. from itertools import product
  3. from typing import Generator
  4. from typing import Sequence
  5.  
  6. DIRECTIONS = (complex(1, 0), complex(0, 1), complex(-1, 0), complex(0, -1))
  7. HORIZONTAL_DIRECTIONS = [complex(1, 0), complex(-1, 0)]
  8. VERTICAL_DIRECTIONS = [complex(0, -1), complex(0, 1)]
  9. DIRECTIONS_PERPENDICULAR = {
  10.     HORIZONTAL_DIRECTIONS[0]: VERTICAL_DIRECTIONS,
  11.     HORIZONTAL_DIRECTIONS[1]: VERTICAL_DIRECTIONS,
  12.     VERTICAL_DIRECTIONS[0]: HORIZONTAL_DIRECTIONS,
  13.     VERTICAL_DIRECTIONS[1]: HORIZONTAL_DIRECTIONS,
  14. }
  15.  
  16.  
  17. def parse_data(fp: str) -> tuple[dict[complex, str], tuple[int, int]]:
  18.     output = {}
  19.     with open(fp, "r") as f:
  20.         for y, row in enumerate(f.readlines()):
  21.             for x, val in enumerate(row.strip()):
  22.                 output[complex(x, y)] = val
  23.     bounds = (x + 1, y + 1)
  24.     return output, bounds
  25.  
  26.  
  27. def next_directions(
  28.     start: complex, directions: tuple[complex, ...] = DIRECTIONS
  29. ) -> Generator[complex, None, None]:
  30.     for direction in directions:
  31.         yield start + direction
  32.  
  33.  
  34. def find_adjacent_plots(
  35.     start: complex, garden_plots: dict[complex, str]
  36. ) -> set[complex]:
  37.     seen: set[complex] = set()
  38.     adjacent: set[complex] = set()
  39.     q = deque([start])
  40.     plot_type = garden_plots[start]
  41.     seen.add(start)
  42.     adjacent.add(start)
  43.     while q:
  44.         current = q.popleft()
  45.         seen.add(current)
  46.         for direction in next_directions(current):
  47.             if direction not in seen and garden_plots.get(direction) == plot_type:
  48.                 adjacent.add(direction)
  49.                 q.appendleft(direction)
  50.     return adjacent
  51.  
  52.  
  53. def get_plot_areas(
  54.     garden_plots: dict[complex, str], bounds: tuple[int, int]
  55. ) -> deque[set[complex]]:
  56.     seen = set()
  57.     plot_collections = deque()
  58.     x_max, y_max = bounds
  59.     for x, y in product(range(x_max), range(y_max)):
  60.         if complex(x, y) in seen:
  61.             continue
  62.         else:
  63.             plots = find_adjacent_plots(complex(x, y), garden_plots)
  64.             plot_collections.append(plots)
  65.             seen.update(plots)
  66.     return plot_collections
  67.  
  68.  
  69. def compute_plot_perimeter(plot: set[complex]) -> int:
  70.     perimeter = 0
  71.     for single_plot in plot:
  72.         for direction in next_directions(single_plot):
  73.             if direction not in plot:
  74.                 perimeter += 1
  75.     return perimeter
  76.  
  77.  
  78. def get_full_plot_perimeters(plot_areas: Sequence[set[complex]]) -> list[int]:
  79.     perimeters = []
  80.     for area in plot_areas:
  81.         perimeters.append(compute_plot_perimeter(area))
  82.     return perimeters
  83.  
  84.  
  85. def traverse_fence_edge(
  86.     start: complex, area: set[complex], perpendicular_direction: complex
  87. ) -> frozenset[tuple[complex, complex]]:
  88.     seen = set()
  89.     for parallel in DIRECTIONS_PERPENDICULAR[perpendicular_direction]:
  90.         q = deque()
  91.         q.append(start)
  92.         while q:
  93.             current = q.popleft()
  94.             seen.add((current, current + perpendicular_direction))
  95.             if (
  96.                 current + parallel in area
  97.                 and (current + parallel + perpendicular_direction) not in area
  98.             ):
  99.                 current = current + parallel
  100.                 q.append(current)
  101.     return frozenset(seen)
  102.  
  103.  
  104. def count_continuous_plot_edges(area: set[complex]) -> int:
  105.     """Part B to find continuous perimeters"""
  106.     edges: set[frozenset[tuple[complex, complex]]] = set()
  107.     for plot in area:
  108.         for direction in DIRECTIONS:
  109.             if plot + direction not in area:  # it's an edge
  110.                 edges.add(traverse_fence_edge(plot, area, direction))
  111.     return len(edges)
  112.  
  113.  
  114. def get_continuous_plot_perimeters(plot_areas: Sequence[set[complex]]) -> list[int]:
  115.     perimeters = []
  116.     for area in plot_areas:
  117.         perimeters.append(count_continuous_plot_edges(area))
  118.     return perimeters
  119.  
  120.  
  121. def calculate_fence_price(
  122.     garden_plots: dict[complex, str], bounds: tuple[int, int]
  123. ) -> int:
  124.     adjacent_plots = get_plot_areas(garden_plots, bounds)
  125.     perimeters = get_full_plot_perimeters(adjacent_plots)
  126.     total = 0
  127.     for adjacent_plot, perimeter in zip(adjacent_plots, perimeters):
  128.         total += len(adjacent_plot) * perimeter
  129.     return total
  130.  
  131.  
  132. def calculate_bulk_fence_price(
  133.     garden_plots: dict[complex, str], bounds: tuple[int, int]
  134. ) -> int:
  135.     adjacent_plots = get_plot_areas(garden_plots, bounds)
  136.     perimeters = get_continuous_plot_perimeters(adjacent_plots)
  137.     total = 0
  138.     for adjacent_plot, perimeter in zip(adjacent_plots, perimeters):
  139.         total += len(adjacent_plot) * perimeter
  140.     return total
  141.  
  142.  
  143. def part_a_examples():
  144.     fp = r"./example/day12-example01.txt"
  145.     data, bounds = parse_data(fp)
  146.     fence_price = calculate_fence_price(data, bounds)
  147.     print(fence_price, "= 140")
  148.  
  149.     fp = r"./example/day12-example02.txt"
  150.     data, bounds = parse_data(fp)
  151.     fence_price = calculate_fence_price(data, bounds)
  152.     print(fence_price, "= 772")
  153.  
  154.     fp = r"./example/day12-example03.txt"
  155.     data, bounds = parse_data(fp)
  156.     fence_price = calculate_fence_price(data, bounds)
  157.     print(fence_price, "= 1930")
  158.  
  159.  
  160. def part_a():
  161.     fp = r"./data/day12.txt"
  162.     data, bounds = parse_data(fp)
  163.     fence_price = calculate_fence_price(data, bounds)
  164.     print(fence_price)
  165.  
  166.  
  167. def part_b_examples():
  168.     fp = r"./example/day12-example01.txt"
  169.     data, bounds = parse_data(fp)
  170.     fence_price = calculate_bulk_fence_price(data, bounds)
  171.     print(fence_price, "= 80")
  172.  
  173.     fp = r"./example/day12-example02.txt"
  174.     data, bounds = parse_data(fp)
  175.     fence_price = calculate_bulk_fence_price(data, bounds)
  176.     print(fence_price, "= 436")
  177.  
  178.     fp = r"./example/day12-example04.txt"
  179.     data, bounds = parse_data(fp)
  180.     fence_price = calculate_bulk_fence_price(data, bounds)
  181.     print(fence_price, "= 236")
  182.  
  183.     fp = r"./example/day12-example05.txt"
  184.     data, bounds = parse_data(fp)
  185.     fence_price = calculate_bulk_fence_price(data, bounds)
  186.     print(fence_price, "= 368")
  187.  
  188.     fp = r"./example/day12-example03.txt"
  189.     data, bounds = parse_data(fp)
  190.     fence_price = calculate_bulk_fence_price(data, bounds)
  191.     print(fence_price, "= 1206")
  192.  
  193.  
  194. def part_b():
  195.     fp = r"./data/day12.txt"
  196.     data, bounds = parse_data(fp)
  197.     fence_price = calculate_bulk_fence_price(data, bounds)
  198.     print(fence_price)
  199.  
  200.  
  201. if __name__ == "__main__":
  202.     part_a_examples()
  203.     part_a()
  204.     part_b_examples()
  205.     part_b()
  206.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement