Advent of Code 2022 day 18

After the past few days, day 18 was a bit of a relief! I wonder if this was intended to be a day where people could catch up?

Data structures

This is again direct. I have a droplet (a three-dimensional point) and some lava (a set of droplets).

type Droplet = V3 Int
type Lava = S.Set Droplet

Counting surfaces

The neighbours of a droplet are the positions at ±1 in each dimension.

neigbours :: Droplet -> Lava
neigbours here = S.fromList [ here & d %~ (+) n | d <- [_x, _y, _z], n <- [- 1, 1]]

The surface area contribution of a droplet is 6 faces, -1 for each neighbour that has a droplet. The only wrinkle is converting the lava to a list so it doesn't collapse the result to just the distinct areas.

surfaceArea :: Lava -> Int
surfaceArea lava = sum $ fmap countFree $ S.elems lava
  where countFree droplet = 6 - (S.size $ S.intersection lava (neigbours droplet))

Finding the steam

I started by thinking about how to find if a point was completely enclosed in a polyhedron, but then realised there was a more direct method.

  1. Define a box outside the droplet
  2. Fill it with steam (with the steam not penetrating the droplet)
  3. The enclosed droplet is the box minus the steam

The box is a bit larger than it needs to be, to ensure that the steam can reach all the surfaces of the droplet.

"Filling with steam" is a standard flood-fill process, with a boundary and a set of steam-filled locations. As a point in the boundary is processed, its neighbours are added to the bounary so long as they've not already been processed, are inside the box, and aren't a droplet of lava.

part2 lava = surfaceArea enclosed
  where box = boundingBox lava
        steam = floodFill lava box (S.singleton $ head $ range box) S.empty
        enclosed = (S.fromList (range box)) `S.difference` steam

boundingBox lava = ((V3 minX minY minZ), (V3 maxX maxY maxZ))
  where minX = (fromJust $ minimumOf (folded . _x) lava) - 1
        minY = (fromJust $ minimumOf (folded . _y) lava) - 1
        minZ = (fromJust $ minimumOf (folded . _z) lava) - 1
        maxX = (fromJust $ maximumOf (folded . _x) lava) + 1
        maxY = (fromJust $ maximumOf (folded . _y) lava) + 1
        maxZ = (fromJust $ maximumOf (folded . _z) lava) + 1

floodFill lava box boundary found
  | S.null boundary = found
  | otherwise = floodFill lava box (S.union nbrs $ S.delete here boundary) (S.insert here found)
  where here = S.findMin boundary
        nbrs = S.filter (`S.notMember` lava) 
                $ S.filter (`S.notMember` boundary) 
                $ S.filter (`S.notMember` found) 
                $ S.filter (inRange box) 
                $ neigbours here

Code

You can get the code from my locally-hosted Git repo, or from Gitlab.