Advent of Code 2021 day 22

    Day 22 was one where a bit of investigation around the subject paid off.

    Sweep algorithms

    My original thought was to maintain the property that each voxel could be a member of at most one active cuboid. That would mean, at the end, I only had to add up the volumes of all the remaining cuboids. If two cuboids intersected, I would split the existing cuboid into (up to) 27 cuboids and remove the fragments that were entirely contained within the new cuboid. (There was also going to be some faffing around with removing zero-volume cuboids.) That would have worked, but there were a lot of fiddly cases to take care of.

    As I was looking into how to find intersections of cuboids, I came across the idea of sweep line algorithms, and realised those would solve all my problems!

    In this case, I don't bother keeping track of intersecting cuboids. All I need to keep track of is the order of application of cuboids, and the value of a voxel is the value of the most recent cuboid that contains it.

    I loop through all the x values. For each x, I find the cuboids that contain that x value. Then, for each y, I filter the remaining cuboids to retain only those that contain the y value. Then, for each z, I can determine the value of each voxel.

    In other words, I sweep some y-z planes across the values of x. For each of those planes, I sweep some z lines for each value of y.

    The clever part is how those scans are optimised, to avoid doing repeated work that I know is redundant. That's easier to explain with some diagrams.

    The diagrams below show two slices through the space in the first example in the puzzle description. They only include the cuboids that intersect the y-z plane that that x value.

    Starting with the x=10 slice, I only need to consider y positions where the collection of cuboids changes. If we consider the y=12 row, the only z positions to worry about are z=10 and z=13. At z=10, the voxel is on, and I know that state will continue until the next interesting position (z=13), so there are 3 on voxels in this region. Similarly, for x=10, y=11, the interesting z positions are 9, 10, 12, and 13. Once I've established that z=10 is off, I don't need to check z=11.

    Similarly, for x=11, y=12 (on the right hand diagram), I need to investigate z positions 10, 11, 13, and 14. Once I get to z=11, I know that nothing could happen until the underlying orange cuboid ends.

    Most of these examples, the small cuboids mean that I end up examining most of the voxels individually. But when the cuboids get larger, I can skip more voxels as the sets of cuboids remains constant.

    Data structure and utility functions

    The basic data structures for cuboids are fairly direct translations from the problem. A Cuboid is a record of bounds, parity, and time (the order in which it was applied. Note that I'm using lenses for this, as there are some reasonably verbose chains of accessors to get to parts of this structure.

    type Coord = V3 Int
    
    data Parity = On | Off deriving (Eq, Ord, Show)
    
    data Cuboid = Cuboid 
      { _bounds :: (Coord, Coord)
      , _parity :: Parity
      , _time :: Int
      }
      deriving (Ord, Eq, Show)
    makeLenses ''Cuboid
    

    There are a few utility functions to help the sweeping. straddles takes a lens (an axis) and a value, and returns all cuboids that contain that value on that axis. events takes some cuboids and an axis, and returns the positions of interest for the sweep to investigate. Finally, isActive returns whether a voxel is active.

    straddles :: (Lens' (V3 Int) Int) -> Int -> Cuboid -> Bool
    straddles f here cuboid = 
      ((cuboid ^. bounds . _1 . f) <= here) && ((cuboid ^. bounds . _2 . f) >= here)
    
    events :: (Lens' (V3 Int) Int) -> [Cuboid] -> [Int]
    events f cuboids = nub $ sort $ ls ++ hs
      where ls = map (^. bounds . _1 . f) cuboids
            hs = map ((+1) . (^. bounds . _2 . f)) cuboids
    
    isActive :: [Cuboid] -> Bool
    isActive [] = False
    isActive cs = ((last scs) ^. parity) == On
      where scs = sortOn (^. time) cs
    

    Sweeping

    Sweeping in each dimension is the same. (I could probably combine the functions with some higher-order trickery, but I think it's probably clearer to keep them separate.)

    I'll start with the z sweep. Assuming we're working with a given x and y, and are given only the cuboids that straddle both those coordinates. I find the events, pair them up into regions, then find out how long each region is. segmentSize does the length calculation, returning 0 if that region was most recently turned off.

    -- assume for a given x and y.
    sweepZ :: [Cuboid] -> Int
    sweepZ cuboids = sum $ map (segmentSize cuboids) $ segment evs
      where evs = events _z cuboids
    
    segmentSize :: [Cuboid] -> (Int, Int) -> Int
    segmentSize cuboids (here, there) 
      | isActive $ filter (straddles _z here) cuboids = (there - here)
      | otherwise = 0
    
    segment :: [Int] -> [(Int, Int)]
    segment [] = []
    segment evs@(_ : tevs) = zip evs tevs
    

    The other two axes are the same. Sweeping along the y axis gives areas, from multiplying the y length by the segment length. Sweeping along the z axis gives volumes, from the x length and area.

    sweepX :: [Cuboid] -> Int
    sweepX cuboids = sum $ map (volumeSize cuboids) $ segment evs
      where evs = events _x cuboids
    
    volumeSize :: [Cuboid] -> (Int, Int) -> Int
    volumeSize cuboids (here, there) = (sweepY cuboidsHere) * (there - here)
      where cuboidsHere = filter (straddles _x here) cuboids
    
    -- assume for a given x
    sweepY :: [Cuboid] -> Int
    sweepY cuboids = sum $ map (areaSize cuboids) $ segment evs
      where evs = events _y cuboids
    
    areaSize :: [Cuboid] -> (Int, Int) -> Int
    areaSize cuboids (here, there) = (sweepZ cuboidsHere) * (there - here)
      where cuboidsHere = filter (straddles _y here) cuboids
    

    And that's it!

    This now makes part 1 harder than part 2, because I have to filter out all the "non local" cuboids in the input.

    Code

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

    Neil Smith

    Read more posts by this author.