Advent of Code 2023 Day 22

Advent of Code
Published

February 4, 2024

Time for another 3-D coordinate puzzle. You are given the coordinates of a number of blocks hanging in three-dimensional space. Part 1 asks you to simulate the position of the blocks once they fall to the floor, then compute how many you can “safely” vaporize. A block can be “safely” vaporized so long as any block that rests on it also rests on at least one other block.

As is often the case, this task had some finicky implementation challenges, but nothing profoundly difficult.

Part 2 reveals that it’s time for Tetris! You’re asked to sum, for each block, how many blocks would fall if that block were vaporized. Since there are only a few hundred blocks in total, nothing stops me from just calling the fall function used in part 1 on each block. For once, part 2 ends up easier.

```{python}
from functools import reduce
from operator import itemgetter
from itertools import chain

from utils.utils import split_lines

class Coord(tuple):
    def update(self, key, value):
        return __class__(chain(self[:key], (value,), self[key + 1 :]))

    def __sub__(self, value):
        return __class__(self[:2] + (self[2] - value,))

    def __isub__(self, value):
        return self - value


def coord_range(start, end):
    start, second = sorted([start, end])
    if start == second:
        return {start}
    for field in range(len(start)):
        if start[field] != end[field]:
            axis = field
            break
    else:
        raise ValueError
    # Singleton cube

    offset = start[axis]
    assert start[axis] <= end[axis]
    return {start.update(axis, val) for val in range(offset, end[axis] + 1)}

def get_bricks(bricks):
    return reduce( set.union, bricks)


def parse(line):
    parts = line.split("~")
    return Coord(map(int, parts[0].split(","))), Coord(map(int, parts[1].split(",")))

def lowest_z(coords):
    return min(coords, key = itemgetter(-1))[-1]


def fall(bricks, contained):
    result = {}

    bricks = list(sorted(bricks.items(), key = lambda x: lowest_z(x[1])))
    # Make each brick fall in ascending order
    fallen = 0

    for pair in bricks:
        id, brick = pair
        current= set(brick)
        contained -= current
        z_level = lowest_z(brick)
        done = started = False

        while z_level > 1 and not done:
            new = set()
            for coord in current:
                new_coord = coord - 1
                if new_coord in contained:
                    done = True
                    break
                new.add(new_coord)
            else:
                started = True
                z_level -= 1
                current = new

        fallen += started
        result[id] = current
        contained |= current
    return result, fallen

def count_not_supporting(bricks):
    mapping = {}
    alone = set()
    for id, brick in bricks.items():
        for coord in brick:
            mapping[coord] = id
    for id, brick in bricks.items():
        below = set()
        for coord in brick:
            if coord[2] == 1:
                break
            new = coord - 1
            supporting = mapping.get(new)
            if supporting is not None and supporting != id:
                below.add(supporting)
        if len(below) == 1:
            alone.update(below)
    return len(bricks) - len(alone)


raw_input = split_lines("inputs/day22.txt")
parsed = list(map(parse, raw_input))
contained = set()
bricks = {i: coord_range(*line) for i, line in enumerate(parsed)}
contained = get_bricks(bricks.values())
bricks, _ = fall(bricks, contained)
part1 = count_not_supporting(bricks)
print(part1)

part2 = 0
contained = get_bricks(bricks.values())

for id in list(bricks.keys()):
    removed = bricks[id]
    bricks.pop(id)
    part2 += fall(bricks, contained - removed)[1]
    bricks[id] = removed
print(part2)
```