Advent of Code 2021 day 20

The day 20 puzzle was about handling points in a range, and updating states. Therefore, I made heavy use of the Data.Ix library again, and the RWS monad (reader-writer-state), only not using the writer part.

We're asked to operate over an infinite plane, even if we're only given a finite portion of it. That means I need to keep track of three things:

  1. The extent of the known, finite region
  2. The pixels we know in that finite region
  3. The value of every pixel outside the known region.

That led me to this data structure of storing an image

type Pixel = V2 Int -- row, column

data Image = Image 
  { grid :: S.Set Pixel
  , distantPixel :: Bool
  , explicitRegion :: (Pixel, Pixel)
  } deriving (Eq, Show)

Using regions and determining things inside and outside a region is entirely within the domain of the Ix class. Given the parts of an Image I define the function findContents that will determine if a pixel is lit, whether or not it is inside the known region.

findContents :: S.Set Pixel -> Bool -> (Pixel, Pixel) -> Pixel -> Bool
findContents grid distant region here 
  | inRange region here = here `S.member` grid
  | otherwise           = distant

This is essentially a member function for the infinite image. Armed with this, I can put together the rest of the solution.

But before I get to that, the problem involves updating an image in steps, and doing so in the context of a fixed enhancement scheme. That's computing with a state and in an environment, as modelled in Haskell with a State and Reader monad respectively. Normally, you'd want track some output at the same time and throw in a Writer monad, and those are neatly bundled in the RWS monad. I thought it was easier to use the convenient bundle rather than building my own monad stack.

The use of the monad makes the "update the image" functions a bit clearer, with shorter parameter lists than otherwise. findStencil returns a list of Bools representing the surroundings of the current pixel. (In this case, unwrapping the values from the record inside the monad makes the call to findContents much quicker, halving the overall runtime.) newPixel returns whether the current pixel is lit in the new state.

newPixel :: Pixel -> ImageEnhancer (Maybe Pixel)
newPixel here =
  do  stencil <- findStencil here
      let i = intify stencil
      enh <- ask
      return $ if enh!!i then Just here else Nothing

findStencil :: Pixel -> ImageEnhancer [Bool]
findStencil here = 
  do  let nbrs = map (here ^+^) neighbours
      g <- gets grid
      d <- gets distantPixel
      r <- gets explicitRegion
      return $ map (findContents g d r) nbrs

neighbours :: [Pixel]
neighbours = [V2 r c | r <- [-1, 0, 1], c <- [-1, 0, 1]]      

The actual image enhancement is done with newImage. It follows the steps as set out in the puzzle description. The trick is that, as the first step, the explicit region is expanded by 1 step in each direction, and the new finite grid is calculated in that new, expanded region. Ix provides the handy range function that finds all the pixels in the new region, then the combination of newPixel and catMaybes generates the new grid. The final thing is to reset the value of the pixel outside the known region.

newImage :: ImageEnhancer ()
newImage =
  do  region <- gets explicitRegion
      let region' = expandRegion region
      let heres = range region'
      newPixels <- mapM newPixel heres
      let grid' = S.fromList $ catMaybes newPixels
      distant <- gets distantPixel
      enhancement <- ask
      let distant' = if distant then (last enhancement) else (head enhancement)
      put $ Image {grid = grid', distantPixel = distant', explicitRegion = region'}

Given all that effort, iterating newImage is handled by primitive recursion and the monad is executed with evalRWS.

part1 enhancement image = fst $ evalRWS (enhanceImage 2) enhancement image

part2 enhancement image = fst $ evalRWS (enhanceImage 50) enhancement image

enhanceImage :: Int -> ImageEnhancer Int
enhanceImage 0 = do image <- get
                    return $ S.size $ grid image
enhanceImage n = do newImage
                    enhanceImage (n - 1)

Code

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