Juha-Matti Santala
Community Builder. Dreamer. Adventurer.

Advent of Code - 2022

This is a solution to Day 18 of Advent of Code 2022.

Day 18 - Boiling Boulders

You and the elephants finally reach fresh air. You've emerged near the base of a large volcano that seems to be actively erupting! Fortunately, the lava seems to be flowing away from you and toward the ocean.

Bits of lava are still being ejected toward you, so you're sheltering in the cavern exit a little longer. Outside the cave, you can see the lava landing in a pond and hear it loudly hissing as it solidifies.

Depending on the specific compounds in the lava and speed at which it cools, it might be forming obsidian! The cooling rate should be based on the surface area of the lava droplets, so you take a quick scan of a droplet as it flies past you (your puzzle input).

Because of how quickly the lava is moving, the scan isn't very good; its resolution is quite low and, as a result, it approximates the shape of the lava droplet with 1x1x1 cubes on a 3D grid, each given as its x,y,z position.

To approximate the surface area, count the number of sides of each cube that are not immediately connected to another cube. So, if your scan were only two adjacent cubes like 1,1,1 and 2,1,1, each cube would have a single side covered and five sides exposed, a total surface area of 10 sides.

Here's a larger example:

2,2,2
1,2,2
3,2,2
2,1,2
2,3,2
2,2,1
2,2,3
2,2,4
2,2,6
1,2,5
3,2,5
2,1,5
2,3,5

In the above example, after counting up all the sides that aren't connected to another cube, the total surface area is 64.

To approach today's puzzle, I figured I need to loop over the coordinates twice: once to add a count to all of each points' neighbors and then again to check how many neighbors did each cube have. I would then deduct that count from 6 and calculate the sum of them all.

Read input

from utils import read_input
from collections import namedtuple

Point = namedtuple('Point', ['x', 'y', 'z'])

def transformer(line):
    return Point(*[int(n) for n in line.split(',')])

points = set(read_input(18, transformer))

To find how many neighboring cubes each cube has, I loop over them and add 1 to each of the adjacent coordinates.

from collections import defaultdict

def find_neighbors(points):
    grid = defaultdict(int)
    
    for point in points:
        grid[Point(point.x, point.y, point.z - 1)] += 1
        grid[Point(point.x, point.y, point.z + 1)] += 1
        
        grid[Point(point.x, point.y - 1, point.z)] += 1
        grid[Point(point.x, point.y + 1, point.z)] += 1
        
        grid[Point(point.x - 1, point.y, point.z)] += 1
        grid[Point(point.x + 1, point.y, point.z)] += 1

    return grid

And then for the cubes we have, subtract their neighbors from total number of sides, 6, and tally up the sum.

def calculate_clear_edges(points, grid):
    return sum(6 - grid[point] for point in points)

Part 1

What is the surface area of your scanned lava droplet?

grid = find_neighbors(points)
clear_edges = calculate_clear_edges(points, grid)

print(f'Part 1: {clear_edges}')
assert clear_edges == 3564

Part 2

Something seems off about your calculation. The cooling rate depends on exterior surface area, but your calculation also included the surface area of air pockets trapped in the lava droplet.

Instead, consider only cube sides that could be reached by the water and steam as the lava droplet tumbles into the pond. The steam will expand to reach as much as possible, completely displacing any air on the outside of the lava droplet but never expanding diagonally.

In the larger example above, exactly one cube of air is trapped within the lava droplet (at 2,2,5), so the exterior surface area of the lava droplet is 58.

What is the exterior surface area of your scanned lava droplet?

def find_trapped(points):
    xs = [p.x for p in points]
    ys = [p.y for p in points]
    zs = [p.z for p in points]
    
    min_x = min(xs)
    max_x = max(xs)
    min_y = min(ys)
    max_y = max(ys)
    min_z = min(zs)
    max_z = max(zs)

    trapped = set()
    for point in get_coordinates([min_x, max_x, min_y, max_y, min_z, max_z]):
        if point in points:
            continue
        q = [point]
        visited = set([point])
        while q:
            next_point = q.pop(0)
            if (next_point.x < min_x or 
                next_point.x > max_x or 
                next_point.y < min_y or 
                next_point.y > max_y or 
                next_point.z < min_z or 
                next_point.z > max_z):
                break
            for xd,yd,zd in [(1,0,0), (-1,0,0), (0,1,0), (0,-1,0), (0,0,1), (0,0,-1)]:
                x2 = next_point.x+xd
                y2 = next_point.y+yd
                z2 = next_point.z+zd
                p2 = Point(x2, y2, z2)
                if p2 not in points and p2 not in visited:
                    q.append(p2)
                    visited.add(p2)
        if not q:
            trapped.add(point)

    return trapped

def get_coordinates(limits):
    x_min, x_max, y_min, y_max, z_min, z_max = limits
    for x in range(x_min, x_max+1):
        for y in range(y_min, y_max+1):
            for z in range(z_min, z_max+1):
                yield Point(x,y,z)

def calculate_outer_edges(points, grid, trapped):
    all_edges = sum(6 - grid[point] for point in points)
    trapped_edges = sum(grid[point] for point in trapped)
    return all_edges - trapped_edges

grid = find_neighbors(points)
trapped = find_trapped(points)
part2 = calculate_outer_edges(points, grid, trapped)
print(f'Part 2: {part2}')
assert part2 == 2106