import itertools import logging import os import string import sys from collections import defaultdict VERBOSE = os.getenv("AOC_VERBOSE") == "True" logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING) lines = sys.stdin.read().splitlines() def _name(i: int) -> str: if len(lines) < 26: return string.ascii_uppercase[i] return f"B{i:04d}" def build_supports( bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]], ) -> tuple[dict[int, set[int]], dict[int, set[int]]]: # 1. compute locations where a brick of sand will land after falling by processing # them in sorted order of bottom z location levels: dict[tuple[int, int, int], int] = defaultdict(lambda: -1) for i_brick, ((sx, sy, sz), (ex, ey, ez)) in enumerate(bricks): assert sx <= ex and sy <= ey and sz <= ez xs, ys = range(sx, ex + 1), range(sy, ey + 1) for z in range(sz - 1, 0, -1): if any(levels[x, y, z] >= 0 for x, y in itertools.product(xs, ys)): break sz, ez = sz - 1, ez - 1 bricks[i_brick] = ((sx, sy, sz), (ex, ey, ez)) zs = range(sz, ez + 1) for x, y, z in itertools.product(xs, ys, zs): levels[x, y, z] = i_brick # 2. compute the bricks that supports any brick supported_by: dict[int, set[int]] = {} supports: dict[int, set[int]] = {i_brick: set() for i_brick in range(len(bricks))} for i_brick, ((sx, sy, sz), (ex, ey, ez)) in enumerate(bricks): name = _name(i_brick) supported_by[i_brick] = { v for x, y in itertools.product(range(sx, ex + 1), range(sy, ey + 1)) if (v := levels[x, y, sz - 1]) != -1 } logging.info( f"{name} supported by {', '.join(map(_name, supported_by[i_brick]))}" ) for support in supported_by[i_brick]: supports[support].add(i_brick) return supported_by, supports bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]] = [] for line in lines: bricks.append( ( tuple(int(c) for c in line.split("~")[0].split(",")), # type: ignore tuple(int(c) for c in line.split("~")[1].split(",")), # type: ignore ) ) # sort bricks by bottom z position to compute supports bricks = sorted(bricks, key=lambda b: b[0][-1]) supported_by, supports = build_supports(bricks) # part 1 answer_1 = len(bricks) - sum( any(len(supported_by[supported]) == 1 for supported in supports_to) for supports_to in supports.values() ) print(f"answer 1 is {answer_1}") # part 2 falling_in_chain: dict[int, set[int]] = {} for i_brick in range(len(bricks)): to_disintegrate: set[int] = { supported for supported in supports[i_brick] if len(supported_by[supported]) == 1 } supported_by_copy = dict(supported_by) falling_in_chain[i_brick] = set() while to_disintegrate: falling_in_chain[i_brick].update(to_disintegrate) to_disintegrate_v: set[int] = set() for d_brick in to_disintegrate: for supported in supports[d_brick]: supported_by_copy[supported] = supported_by_copy[supported] - {d_brick} if not supported_by_copy[supported]: to_disintegrate_v.add(supported) to_disintegrate = to_disintegrate_v answer_2 = sum(len(falling) for falling in falling_in_chain.values()) print(f"answer 2 is {answer_2}")