Advertisement
MHorikx

AoC 2023 day 22

Jan 3rd, 2024
74
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 5.78 KB | None | 0 0
  1. import numpy as np
  2. from re import findall
  3. from collections import deque
  4.  
  5.  
  6. def find_all_ints(line):
  7.     return list(map(int, findall(r'\d+', line)))
  8.  
  9.  
  10. def read_input():
  11.     with open('../Inputs/day_22_input.txt') as f:
  12.         blocks = [find_all_ints(line) for line in f.read().strip().splitlines()]
  13.         blocks.sort(key=lambda u: u[2])
  14.         # Assume lower bounds are given before upper bounds.
  15.         max_x = max(block[3] for block in blocks)
  16.         max_y = max(block[4] for block in blocks)
  17.         max_z = max(block[5] for block in blocks)
  18.         block_places = np.zeros((max_x + 1, max_y + 1, max_z + 1), dtype=bool)
  19.         for block in blocks:
  20.             block_places[block[0]:block[3] + 1, block[1]:block[4] + 1, block[2]:block[5] + 1] = True
  21.         block_places[:, :, 0] = True
  22.     return blocks, block_places
  23.  
  24.  
  25. def drop_blocks(blocks, block_places):
  26.     for block in blocks:
  27.         x1, y1, z1, x2, y2, z2 = block
  28.         section = block_places[x1:x2 + 1, y1:y2 + 1, :z1]
  29.         flattened_section = section.sum(axis=0).sum(axis=0)
  30.         support_height = np.nonzero(flattened_section)[0].max()
  31.         fall_height = z1 - (support_height + 1)
  32.         block_places[x1:x2 + 1, y1:y2 + 1, z1:z2 + 1] = False
  33.         block_places[x1:x2 + 1, y1:y2 + 1, z1 - fall_height:z2 - fall_height + 1] = True
  34.         block[2] -= fall_height
  35.         block[5] -= fall_height
  36.     blocks.sort(key=lambda u: u[2])
  37.     return blocks, block_places
  38.  
  39.  
  40. def get_blocks_by_z(blocks):
  41.     max_z = max(block[5] for block in blocks)
  42.     # By making these one longer than necessary, we can
  43.     # always safely ask for the element with index block[5] + 1.
  44.     blocks_by_lowest_z = [[] for _ in range(max_z + 2)]
  45.     blocks_by_highest_z = [[] for _ in range(max_z + 2)]
  46.     for block in blocks:
  47.         blocks_by_lowest_z[block[2]].append(block)
  48.         blocks_by_highest_z[block[5]].append(block)
  49.     return blocks_by_lowest_z, blocks_by_highest_z
  50.  
  51.  
  52. def scan_below(blocks, blocks_by_highest_z, block_indices):
  53.     # Save the blocks as their index, since this will reduce hash overhead compared to
  54.     # tuple or string of the six numbers that represent a block.
  55.     # Since blocks is sorted by the lowest z coordinate of the blocks, is_supported_by will have the same ordering.
  56.     is_supported_by = [set() for i, _ in enumerate(blocks)]
  57.     for i, block_1 in enumerate(blocks):
  58.         x1, y1, z1, x2, y2, z2 = block_1
  59.         # The lowest z coordinate of block_1 needs to be 1 greater than the highest z coordinate of block_2
  60.         # to have the latter support the former.
  61.         for block_2 in blocks_by_highest_z[z1 - 1]:
  62.             # Closed intervals [a, b] and [c, d] overlap if and only if a <= d and c <= b.
  63.             if x1 <= block_2[3] and block_2[0] <= x2 and y1 <= block_2[4] and block_2[1] <= y2:
  64.                 is_supported_by[i].add(block_indices[tuple(block_2)])
  65.     return is_supported_by
  66.  
  67.  
  68. def scan_above(blocks, blocks_by_lowest_z, block_indices):
  69.     # Save the blocks as their index, since this will reduce hash overhead compared to
  70.     # tuple or string of the six numbers that represent a block.
  71.     # Since blocks is sorted by the lowest z coordinate of the blocks, is_supporting will have the same ordering.
  72.     is_supporting = [set() for i, _ in enumerate(blocks)]
  73.     for i, block_1 in enumerate(blocks):
  74.         x1, y1, z1, x2, y2, z2 = block_1
  75.         # The lowest z coordinate of block_2 needs to be 1 greater than the highest z coordinate of block_1
  76.         # to have the latter support the former.
  77.         for block_2 in blocks_by_lowest_z[z2 + 1]:
  78.             # Closed intervals [a, b] and [c, d] overlap if and only if a <= d and c <= b.
  79.             if x1 <= block_2[3] and block_2[0] <= x2 and y1 <= block_2[4] and block_2[1] <= y2:
  80.                 is_supporting[i].add(block_indices[tuple(block_2)])
  81.     return is_supporting
  82.  
  83.  
  84. def find_destroyable_blocks(is_supported_by):
  85.     # Blocks can be safely destroyed if they are not the sole support of some other block.
  86.     not_destroyable = set()
  87.     for supports in is_supported_by:
  88.         if len(supports) == 1:
  89.             support, = supports
  90.             not_destroyable.add(support)
  91.     # is_supported_by has an element for every block, so this is the number of destroyable blocks.
  92.     return len(is_supported_by) - len(not_destroyable)
  93.  
  94.  
  95. def find_falling_num(block, is_supporting, is_supported_by):
  96.     # A block does not really support itself but this makes the checks cleaner.
  97.     indirectly_supporting = {block}
  98.     # is_supporting is sorted by lowest z coordinate. This means the blocks added onto the queue
  99.     # are ordered as well and that gives the correct order for the support check.
  100.     queue = deque(is_supporting[block])
  101.     while queue:
  102.         on_top = queue.popleft()
  103.         if is_supported_by[on_top].issubset(indirectly_supporting):
  104.             indirectly_supporting.add(on_top)
  105.         queue.extend(is_supporting[on_top])
  106.     # Take 1 away to counteract the block itself being added.
  107.     return len(indirectly_supporting) - 1
  108.  
  109.  
  110. def main():
  111.     blocks, block_places = read_input()
  112.     blocks, block_places = drop_blocks(blocks, block_places)
  113.     block_indices = {tuple(block): i for i, block in enumerate(blocks)}
  114.     blocks_by_lowest_z, block_by_highest_z = get_blocks_by_z(blocks)
  115.     is_supported_by = scan_below(blocks, block_by_highest_z, block_indices)
  116.     destroyable = find_destroyable_blocks(is_supported_by)
  117.     print(f'Part one: {destroyable}')
  118.     is_supporting = scan_above(blocks, blocks_by_lowest_z, block_indices)
  119.     total_falling = sum(find_falling_num(i, is_supporting, is_supported_by) for i, _ in enumerate(blocks))
  120.     print(f'Part two: {total_falling}')
  121.  
  122.  
  123. if __name__ == '__main__':
  124.     from time import perf_counter as pf
  125.     t0 = pf()
  126.     main()
  127.     print(pf() - t0)
  128.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement