never executed always true always false
1 {-|
2 Module: Y2021.D12
3 Description: Advent of Code 2021 Day 12 Solutions.
4 License: MIT
5 Maintainer: @tylerjl
6
7 Solutions to the 2021 day 12 set of problems for <adventofcode.com>.
8 -}
9 module Y2021.D12
10 ( parse12
11 , parse12'
12 , part12A
13 , part12B
14 ) where
15
16 import Control.Applicative
17 import Data.Attoparsec.Text
18 import Data.Bifunctor (second)
19 import Data.Either.Utils (fromRight)
20 import Data.List (nub)
21 import Data.Map.Strict (Map)
22 import Data.MultiSet (MultiSet)
23 import Data.Set (Set)
24 import Data.Text (Text)
25 import Witch
26 import Y2015.Util ((<&&>))
27
28 import qualified Data.Map.Strict as M
29 import qualified Data.Set as S
30 import qualified Data.MultiSet as MS
31
32 -- |This GADT helps quite a bit when comparing vaules later on
33 data Room = Start | BigCave Text | SmallCave Text | End
34 deriving (Eq, Ord, Show)
35 -- |Type alias for better readability.
36 type Caves = Map Room (Set Room)
37 -- |Type alias for better readability.
38 type TwiceVisit = Bool
39 -- |Type alias for better readability.
40 type Journey = (TwiceVisit, MultiSet Room)
41
42 -- |Solution to part A
43 part12A :: Text -> Int
44 part12A (parse12' -> rooms)
45 = length $ explore smallOnce rooms (True, mempty) Start
46
47 -- |This is the main recursive function that drives both A and B. The
48 -- higher-order function argument is the main difference, bit otherwise we build
49 -- up a list of all valid routes by traversing the overall `Map`.
50 explore :: (Journey -> Room -> Bool) -> Caves -> Journey -> Room -> [Journey]
51 explore allowed caves (twice', journey') room
52 | room == End = [visited]
53 | otherwise = foldMap (concatMap (explore allowed caves visited)) rooms
54 where
55 journey = MS.insert room journey'
56 twice
57 | twice' = True
58 | otherwise =
59 any ((isSmall . fst) <&&> ((> 1) . snd)) $ MS.toOccurList journey
60 visited = (twice, journey)
61 rooms = S.filter (allowed visited) <$> M.lookup room caves
62
63 -- |The predicate we use for part A, which is essentially "don't visit small
64 -- rooms twice"
65 smallOnce :: Journey -> Room -> Bool
66 smallOnce (_, journey) c@(SmallCave _) = c `notElem` journey
67 smallOnce _ Start = False
68 smallOnce _ _ = True
69
70 -- |Solution to part B
71 part12B :: Text -> Int
72 part12B (parse12' -> rooms)
73 = length $ explore smallTwice rooms (False, mempty) Start
74
75 -- |Given our journey through the caves so far and the room we'd like to proceed
76 -- through, should we continue?
77 --
78 -- Part B requires a little more logic, which is primarily to visit any single
79 -- small cave at most two times.
80 smallTwice :: Journey -> Room -> Bool
81 smallTwice _ (BigCave _) = True
82 smallTwice _ Start = False
83 smallTwice _ End = True
84 smallTwice (seenTwice, j) r
85 | r `MS.notMember` j = True
86 | seenTwice = False
87 | otherwise = True
88
89 -- |Super small utility to find small caves.
90 isSmall :: Room -> Bool
91 isSmall (SmallCave _) = True
92 isSmall _ = False
93
94 -- |An intermediate parsing function; once we get the raw room pairs we turn it
95 -- into the structure we'll work with later.
96 parse12' :: Text -> Map Room (Set Room)
97 parse12' =
98 M.fromListWith S.union .
99 map (second S.singleton) . nub . concatMap backpath . parse12
100 where
101 backpath (a, b) = [(a, b), (b, a)]
102
103 -- |Pairs of cave "rooms", which we parse very simply and postprocess later.
104 parse12 :: Text -> [(Room, Room)]
105 parse12 = fromRight . parseOnly parser
106 where
107 parser = line `sepBy1` endOfLine <* atEnd
108 line = (,) <$> location <* char '-' <*> location
109 location = start' <|> end' <|> bigCave <|> smallCave
110 start' = Start <$ string "start"
111 bigCave = BigCave . into @Text <$> many1 (satisfy (`elem` ['A'..'Z']))
112 smallCave = SmallCave . into @Text <$> many1 (satisfy (`elem` ['a'..'z']))
113 end' = End <$ string "end"