Skip to content

Commit b95ad7e

Browse files
committed
day 21 reflections
1 parent a894c6c commit b95ad7e

File tree

3 files changed

+65
-22
lines changed

3 files changed

+65
-22
lines changed

2024/AOC2024/Day21.hs

+5-13
Original file line numberDiff line numberDiff line change
@@ -95,16 +95,8 @@ buttonGraph = (G.mkGraph (swap <$> M.toList nodes) edges, startMap, endMap)
9595
M.fromList . flip zip [0 ..] $
9696
(Left <$> liftA3 (,,) allPushable' allPushable' allPushable')
9797
++ (Right <$> allPushable')
98-
startMap =
99-
M.fromList
100-
[ (n, i)
101-
| (Left (n, Nothing, Nothing), i) <- M.toList nodes
102-
]
103-
endMap =
104-
M.fromList
105-
[ (n, i)
106-
| (Right n, i) <- M.toList nodes
107-
]
98+
startMap = M.fromList [(n, i) | (Left (n, Nothing, Nothing), i) <- M.toList nodes]
99+
endMap = M.fromList [(n, i) | (Right n, i) <- M.toList nodes]
108100
edges :: [(Int, Int, DirPad)]
109101
edges = do
110102
(Left (b, d, e), node) <- M.toList nodes
@@ -156,8 +148,8 @@ runPath x = \case
156148
dirPathChain :: Int -> Map DirPad (Map DirPad Int)
157149
dirPathChain n = iterate (`composeDirPathLengths` dirPath @Dir) (dirPathCosts @Dir) !!! n
158150

159-
solveCodeNoSearch :: Int -> [NumPad] -> Int
160-
solveCodeNoSearch n = spellDirPathLengths mp . (Nothing :)
151+
solveCode :: Int -> [NumPad] -> Int
152+
solveCode n = spellDirPathLengths mp . (Nothing :)
161153
where
162154
mp = dirPathChain (n - 1) `composeDirPathLengths` dirPath @(Finite 10)
163155

@@ -174,7 +166,7 @@ day21 n =
174166
sum . map solve
175167
}
176168
where
177-
solve p = num * solveCodeNoSearch n p
169+
solve p = num * solveCode n p
178170
where
179171
num = read (map intToDigit (mapMaybe (fmap fromIntegral) p :: [Int]))
180172

bench-results/2024/day21.txt

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
>> Day 21a
22
benchmarking...
3-
time 3.965 μs (3.959 μs .. 3.970 μs)
4-
1.000 R² (1.000 R² .. 1.000 R²)
5-
mean 3.967 μs (3.961 μs .. 3.973 μs)
6-
std dev 17.27 ns (14.75 ns .. 20.43 ns)
3+
time 3.840 μs (3.834 μs .. 3.851 μs)
4+
1.000 R² (0.999 R² .. 1.000 R²)
5+
mean 3.883 μs (3.848 μs .. 4.052 μs)
6+
std dev 222.9 ns (19.15 ns .. 512.1 ns)
7+
variance introduced by outliers: 69% (severely inflated)
78

89
* parsing and formatting times excluded
910

1011
>> Day 21b
1112
benchmarking...
12-
time 3.971 μs (3.958 μs .. 3.988 μs)
13-
0.999 R² (0.996 R² .. 1.000 R²)
14-
mean 4.020 μs (3.971 μs .. 4.206 μs)
15-
std dev 304.7 ns (22.58 ns .. 645.9 ns)
16-
variance introduced by outliers: 80% (severely inflated)
13+
time 3.839 μs (3.831 μs .. 3.849 μs)
14+
1.000 R² (1.000 R² .. 1.000 R²)
15+
mean 3.841 μs (3.835 μs .. 3.845 μs)
16+
std dev 16.92 ns (13.66 ns .. 20.87 ns)
1717

1818
* parsing and formatting times excluded
1919

reflections/2024/day21.md

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
Everything reveals itself if we imagine a lookup table of "best path from A to
2+
B". For my own purposes I've made the functions parameterized by button pad,
3+
using `Maybe a`, where `Nothing` is the `A` key and `Just x` is the `x` key.
4+
5+
```haskell
6+
type LookupTable a b = Map (Maybe a) (Map (Maybe a) [Maybe b])
7+
8+
type LookupTableLengths a = Map (Maybe a) (Map (Maybe a) Int)
9+
10+
toLengths :: LookupTable a b -> LookupTableLengths a
11+
toLengths = fmap (fmap length)
12+
```
13+
14+
The key is that now these maps are composable:
15+
16+
```haskell
17+
spellDirPathLengths :: Ord a => LookupTableLengths a -> [Maybe a] -> Int
18+
spellDirPathLengths mp xs = sum $ zipWith (\x y -> (mp M.! x) M.! y) xs (drop 1 xs)
19+
20+
composeDirPathLengths :: Ord b => LookupTableLengths b -> LookupTable a b -> LookupTableLengths a
21+
composeDirPathLengths mp = (fmap . fmap) (spellDirPathLengths mp . (Nothing :))
22+
```
23+
24+
That is, if you have the lookup table for two layers, you can compose them to
25+
create one big lookup table.
26+
27+
```haskell
28+
data Dir = North | East | West | South
29+
data NumButton = Finite 10
30+
31+
dirPathChain :: [LookupTableLengths NumButton]
32+
dirPathChain = iterate (`composeDirPathLengths` dirPath @Dir) (dirPathCosts @Dir)
33+
34+
solveCode :: Int -> [Maybe NumButton] -> Int
35+
solveCode n = spellDirPathLengths mp . (Nothing :)
36+
where
37+
lengthChain = dirPathChain !! (n - 1)
38+
mp = lengthChain `composeDirPathLengths` dirPath @NumButton
39+
````
40+
41+
The nice thing is that you only need to compute `dirPathChain` once, to get the
42+
final `LookupTableLengths` for a given `n`, and you can re-use it for
43+
everything.
44+
45+
Generating the actual `LookupTable NumButton Dir` and `LookupTable Dir Dir` is
46+
the tricky part. For me I generated it based on the shortest path considering
47+
the third bot up the chain from the bottom: I used an *fgl* graph where the
48+
nodes were the state of three bots and the edges were the actions that the
49+
fourth "controller" would take, and computed the shortest path in terms of the
50+
fourth controller. This seems to be the magic number: anything higher and you
51+
get the same answer, anything lower and you get suboptimal final paths.

0 commit comments

Comments
 (0)