Abhinav's Notes

Advent of Code 2020 — Week 4

I’m solving the Advent of Code 2020 in the Haskell REPL (GHCi). You can copy the code and paste it in GHCi to play with it. Here are my solutions for week 4 (Dec 20–25):

Day 20

Problem: https://adventofcode.com/2020/day/20

Solution:

:set -XLambdaCase
import qualified Data.Array.BitArray as BA
c2b = \case { '#' -> True; _ -> False }
b2c = \case { True -> '•'; False -> ' ' }
data Tile = Tile { tId :: Int, tSize :: Int, tArr :: BA.BitArray (Int, Int) }
import Data.List (intercalate, intersperse, delete)
import Data.Function (on)
:{
instance Show Tile where
  show (Tile _ size tile) = intercalate "\n" [
    intersperse ' ' [b2c $ tile BA.! (i, j) | j <- [0..size-1]] | i <- [0..size-1]]
instance Eq Tile where
  (==) = (==) `on` tId
readTile id ti = let size = length ti
  in Tile id size . BA.listArray ((0,0), (size-1, size-1)) . map c2b . concat $ ti
:}
:{
transform f (Tile id size tile) =
  Tile id size $ BA.ixmap (BA.bounds tile) (f size) tile
transforms = [ id
  , transform $ \s (x, y) -> (s-1-x, y)
  , transform $ \s (x, y) -> (x, s-1-y)
  , transform $ \s (x, y) -> (s-1-x, s-1-y)
  , transform $ \_ (x, y) -> (y, x)
  , transform $ \s (x, y) -> (s-1-y, x)
  , transform $ \s (x, y) -> (y, s-1-x)
  , transform $ \s (x, y) -> (s-1-y, s-1-x)
  ]
:}
import qualified Data.Map.Strict as Map
import Data.List.Split (splitOn)
:{
tiles <- map ((\(x:xs) -> readTile (read . take 4 . drop 5 $ x) xs) . lines)
  . init . splitOn "\n\n" <$> readFile "/tmp/input20" :: IO [Tile]
:}
gridSize = floor . sqrt . fromIntegral . length $ tiles
tileSize = tSize $ head tiles

topBorder (Tile _ size tile) = [tile BA.! (0,j) | j <- [0..size-1]]
bottomBorder (Tile _ size tile) = [tile BA.! (size-1,j) | j <- [0..size-1]]
leftBorder (Tile _ size tile) = [tile BA.! (i,0) | i <- [0..size-1]]
rightBorder (Tile _ size tile) = [tile BA.! (i,size-1) | i <- [0..size-1]]
matchLeftBorder leftTile tile = rightBorder leftTile == leftBorder tile
matchTopBorder topTile tile = bottomBorder topTile == topBorder tile
import Control.Monad (guard)
:{
solve :: [Tile] -> Map.Map (Int, Int) Tile
solve tiles = head $ solve' 0 Map.empty tiles
  where
    solve' _ grid [] = return grid
    solve' count grid tiles = do
      let (row, col) = count `divMod` gridSize
          leftTile = grid Map.! (row, col-1)
          topTile = grid Map.! (row-1, col)
      tile <- tiles
      tTile <- map ($ tile) transforms
      guard $ case (row, col) of
        (0, 0) -> True
        (0, _) -> matchLeftBorder leftTile tTile
        (_, 0) -> matchTopBorder topTile tTile
        _  ->  matchLeftBorder leftTile tTile && matchTopBorder topTile tTile
      solve' (count + 1) (Map.insert (row, col) tTile grid) (delete tile tiles)
:}
grid = solve tiles
:{
part1 = product . map (tId . (grid Map.!)) $
  [(0, 0), (0, gridSize-1), (gridSize-1, 0), (gridSize-1, gridSize-1)]
:}

:{
removeBorder (Tile id size tile) =
  [((x-1,y-1), b) | ((x,y), b) <- BA.assocs tile
                  , x /= 0, y /= 0, x /= size - 1, y /= size - 1]
:}
newTileSize = tileSize - 2
imageSize = gridSize * newTileSize
:{
removeBorderAndReindex ((x,y), tile) =
  map (\((x',y'), b) -> ((x * newTileSize + x', y * newTileSize + y'), b))
  $ removeBorder tile
image = Tile 0 imageSize . BA.array ((0,0),(imageSize - 1, imageSize - 1))
  $ concatMap removeBorderAndReindex
  $ Map.assocs grid
:}
:{
monster = [ "                  # "
          , "#    ##    ##    ###"
          , " #  #  #  #  #  #   " ]
:}
monWidth = length monster
monLength = length $ head monster
:{
cutouts (Tile _ size tile) = [
    [tile BA.! (i + si, j + sj) | i <- [0..monWidth-1], j <- [0..monLength-1]]
    | si <- [0 .. size - monWidth - 2], sj <- [0 .. size - monLength - 2]]
:}
monsterSig = map c2b $ concat monster
matchMonsterSig = (== monsterSig) . zipWith (&&) monsterSig
:{
(imageWithMonster, monsterCount) = head . filter ((> 1) . snd)
  . map (\transform -> let tImage = transform image
      in (tImage, length . filter matchMonsterSig . cutouts $ tImage))
  $ transforms
:}
monsterBodySize = length $ filter id monsterSig
part2 = BA.popCount (tArr imageWithMonster) - monsterBodySize * monsterCount

Day 21

Problem: https://adventofcode.com/2020/day/21

Solution:

import qualified Text.ParserCombinators.ReadP as P
import Data.Char (isAlpha)
import qualified Data.Map.Strict as Map
import Data.List (intersect, nub)
data Food = Food { fIngs :: [String], fAlrs :: [String] } deriving (Show)
word = P.many1 (P.satisfy isAlpha)
:{
foodP = Food
  <$> (word `P.sepBy` P.char ' ')
  <*> (P.string " (contains "
       *> (word `P.sepBy` P.string ", ")
       <* P.char ')' <* P.eof)
:}
parse = fst . head . P.readP_to_S foodP
foods <- map parse . lines <$> readFile "/tmp/input21"
:{
alrToIngsMap = foldl
  (\m (Food ings alrs) ->
      foldl (\m' alr -> Map.insertWith intersect alr ings m') m alrs)
  Map.empty foods
:}
ingsWithAlrs = nub $ concat $ Map.elems alrToIngsMap
length $ filter (`notElem` ingsWithAlrs) $ concat $ map fIngs foods -- part 1

import Data.List (sortBy, intercalate, (\\))
import Data.Ord (comparing)
:{
prune possibilities =
  if all (== 1) . map length $ Map.elems possibilities
  then Map.map head possibilities
  else let fixedPossibilities =
             concat $ Map.elems $ Map.filter ((== 1) . length) possibilities
           prunedPossibilities = flip Map.map possibilities $ \ings ->
             if length ings == 1 then ings else ings \\ fixedPossibilities
       in prune prunedPossibilities
:}
-- part 2
intercalate "," . map snd . sortBy (comparing fst) . Map.assocs $ prune alrToIngsMap

Day 22

Problem: https://adventofcode.com/2020/day/22

Solution:

:{
playCombat [] p2 = p2
playCombat p1 [] = p1
playCombat (x:xs) (y:ys) = if x > y
  then playCombat (xs ++ [x,y]) ys
  else playCombat xs (ys ++ [y,x])
:}
import Data.List.Split (splitOn)
:{
[player1, player2] <- map (map read . tail . lines) . splitOn "\n\n"
  <$> readFile "/tmp/input22" :: IO [[Int]]
:}
cards = playCombat player1 player2
sum $ zipWith (*) (reverse cards) [1..] -- part 1

import qualified Data.Set as Set
:{
playRecCombat _ [] p2 = Right p2
playRecCombat _ p1 [] = Left p1
playRecCombat seen p1@(x:xs) p2@(y:ys)
  | Set.member (p1, p2) seen = Left p1
  | x <= length xs && y <= length ys =
      case playRecCombat Set.empty (take x xs) (take y ys) of
        Left _ -> playRecCombat seen' (xs ++ [x,y]) ys
        Right _ -> playRecCombat seen' xs (ys ++ [y,x])
  | x > y = playRecCombat seen' (xs ++ [x,y]) ys
  | otherwise = playRecCombat seen' xs (ys ++ [y,x])
  where
    seen' = Set.insert (p1, p2) seen
:}
cards = either id id $ playRecCombat Set.empty player1 player2
sum $ zipWith (*) (reverse cards) [1..] -- part 2

Day 23

Problem: https://adventofcode.com/2020/day/23

Solution:

import Data.List (sortOn)
:{
dest picks left@(current:rest) = head
  . filter (`notElem` picks)
  . tail
  . dropWhile (> current)
  . cycle
  $ sortOn negate left
play (x:xs) = let
    (picks, left) = (take 3 xs, x:drop 3 xs)
    d = dest picks left
    (a,b) = span (/= d) left
  in tail a ++ [d] ++ picks ++ tail b ++ if not (null a) then [head a] else []
:}
input = [3,2,6,5,1,9,4,7,8]
:{
part1 = take (length input - 1)
  . drop 1
  . dropWhile (/= 1)
  . cycle
  $ iterate play input !! 100
:}

input = [3,2,6,5,1,9,4,7,8] ++ [10..1000000] :: [Int]
import qualified Data.Vector.Primitive.Mutable as MV
import Control.Monad (forM_)
cups <- MV.new 1000001 :: IO (MV.IOVector Int)
forM_ (zip input $ tail input ++ [head input]) $ uncurry (MV.unsafeWrite cups)

import Data.List ((\\))
:{
dest :: Int -> [Int] -> Int
dest current picks
  | d < min' = max'
  | d `notElem` picks = d
  | otherwise = dest d picks
  where
    d = current - 1
    max' = maximum $ [999997,999998,999999,1000000] \\ picks
    min' = minimum $ [1,2,3,4] \\ picks

play :: Int -> Int -> IO Int
play 0 _ = do
  a <- MV.unsafeRead cups 1
  b <- MV.unsafeRead cups a
  return $ a * b
play n current = do
  a <- MV.unsafeRead cups current
  b <- MV.unsafeRead cups a
  c <- MV.unsafeRead cups b
  let d = dest current [a,b,c]
  dnext <- MV.unsafeRead cups d
  cnext <- MV.unsafeRead cups c
  MV.unsafeWrite cups d a
  MV.unsafeWrite cups c dnext
  MV.unsafeWrite cups current cnext
  play (n - 1) cnext
:}
play 10000000 3 -- part 2

Day 24

Problem: https://adventofcode.com/2020/day/24

Solution:

:set -XLambdaCase
import qualified Text.ParserCombinators.ReadP as P
data Dir = E | W | NW | NE | SW | SE deriving Show
import Data.Functor (($>))
import Control.Applicative ((<|>))
:{
parser =  P.string "nw" $> NW
      <|> P.string "sw" $> SW
      <|> P.string "ne" $> NE
      <|> P.string "se" $> SE
      <|> P.char 'e' $> E
      <|> P.char 'w' $> W
:}
parse = fst . head . P.readP_to_S (P.many1 parser <* P.eof)
tileDirs <- map parse . lines <$> readFile "/tmp/input24"
:{
interpret :: (Int, Int, Int) -> Dir -> (Int, Int, Int)
interpret (x, y, z) = \case
  E -> (x+1,y+1,z)
  W -> (x-1,y-1,z)
  NE -> (x,y+1,z+1)
  SW -> (x,y-1,z-1)
  NW -> (x-1,y,z+1)
  SE -> (x+1,y,z-1)
:}
tileCos = map (foldl interpret (0,0,0)) tileDirs
import qualified Data.Map.Strict as M
flipCounts = foldl (\m c -> M.insertWith (+) c 1 m) M.empty tileCos
length $ M.filter odd $ flipCounts -- part 1

data Color = White | Black deriving (Show, Eq, Ord)
start = M.map (\x -> if odd x then Black else White) flipCounts
neighbours point = map (interpret point) [E, W, NW, NE, SW, SE]
import Data.Maybe (fromMaybe)
tileColor tile = fromMaybe White . M.lookup tile
import qualified Data.Set as S
:{
day tiles = M.fromSet step
  $ foldl (\s -> S.union s . S.fromList . neighbours) (M.keysSet tiles)
  $ M.keys tiles
  where
    step tile = let
        neighbourColors = map (`tileColor` tiles) $ neighbours tile
        blackNeighbourCount = length $ filter (== Black) neighbourColors
      in case tileColor tile tiles of
           White | blackNeighbourCount == 2 -> Black
           Black | blackNeighbourCount == 0 || blackNeighbourCount > 2 -> White
           color -> color
:}
length $ M.filter (== Black) $ iterate day start !! 100 -- part 2

Day 25

Problem: https://adventofcode.com/2020/day/25

Solution:

:set -XLambdaCase
:set -XStrict
transform s v = \case { 0 -> v; n -> transform s ((v * s) `mod` 20201227) $ n - 1 }
findLoopSize t n v = if v == t then n else findLoopSize t (n+1) $ v * 7 `mod` 20201227
input = [11562782, 18108497]
transform (input !! 1) 1 $ findLoopSize (input !! 0) 0 1 -- part 1

And, that’s a wrap!