Skip to content

Latest commit

 

History

History
90 lines (78 loc) · 2.9 KB

day18.md

File metadata and controls

90 lines (78 loc) · 2.9 KB

Honestly there really isn't much to this puzzle other than applying a basic BFS to solve the maze. It isn't really even big enough that a-star would help.

If you parse the maze into an fgl graph, you can use something like sp :: Node -> Node -> gr a b -> Maybe Path to get the shortest path. However, because we're here anyway, I'm going to paste in my personal BFS code that I use for these challenges that I wrote a while ago, where neighborhoods are given by an n -> Set n function. It uses a Seq as its internal queue, which is my favorite queue type in Haskell.

data BFSState n = BS
  { _bsClosed :: !(Map n (Maybe n))
  -- ^ map of item to "parent"
  , _bsOpen :: !(Seq n)
  -- ^ queue
  }

bfs :: forall n. Ord n => (n -> Set n) -> n -> (n -> Bool) -> Maybe [n]
bfs ex x0 dest = reconstruct <$> go (addBack x0 Nothing (BS M.empty Seq.empty))
  where
    reconstruct :: (n, Map n (Maybe n)) -> [n]
    reconstruct (goal, mp) = drop 1 . reverse $ goreco goal
      where
        goreco n = n : maybe [] goreco (mp M.! n)
    go :: BFSState n -> Maybe (n, Map n (Maybe n))
    go BS{..} = case _bsOpen of
      Empty -> Nothing
      n :<| ns
        | dest n -> Just (n, _bsClosed)
        | otherwise -> go . S.foldl' (processNeighbor n) (BS _bsClosed ns) $ ex n
    addBack :: n -> Maybe n -> BFSState n -> BFSState n
    addBack x up BS{..} =
      BS
        { _bsClosed = M.insert x up _bsClosed
        , _bsOpen = _bsOpen :|> x
        }
    processNeighbor :: n -> BFSState n -> n -> BFSState n
    processNeighbor curr bs0@BS{..} neighb
      | neighb `M.member` _bsClosed = bs0
      | otherwise = addBack neighb (Just curr) bs0

type Point = V2 Int

cardinalNeighbsSet :: Point -> Set Point
cardinalNeighbsSet p = S.fromDistinctAscList . map (p +) $
    [ V2 (-1) 0 , V2 0 (-1) , V2 0 1 , V2 1 0 ]

solveMaze :: Set Point -> Maybe Int
solveMaze walls = length <$> bfs step 0 (== 70)
  where
    step p = S.filter (all (inRange (0, 70))) $ cardinalNeighbsSet p `S.difference` walls

Now if you have a list of points [Point], for part 1 you just solve the maze after taking the first 1024 of them:

part1 :: [Point] -> Maybe Int
part1 = solveMaze . S.fromList . take 1024

For part 2, you can search for the first success, or you can do a binary search.

-- | Find the lowest value where the predicate is satisfied within the
-- given bounds.
binaryMinSearch :: (Int -> Bool) -> Int -> Int -> Maybe Int
binaryMinSearch p = go
  where
    go !x !y
      | x == mid || y == mid = Just (x + 1)
      | p mid = go x mid
      | otherwise = go mid y
      where
        mid = ((y - x) `div` 2) + x
part2 :: [Point] -> Maybe Int
part2 pts = do
    j <- binaryMinSearch (isNothing . solveMaze . (!! wallList)) 0 (length pts)
    pure $ pts !! (j - 1)
  where
    wallList = scanl (flip S.insert) S.empty pts

You should probably use a container type with better indexing than a list, though.