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:
- The extent of the known, finite region
- The pixels we know in that finite region
- 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 Bool
s 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.