Skip to content

Commit

Permalink
Day_21(2023): write-up
Browse files Browse the repository at this point in the history
  • Loading branch information
Sheinxy committed Dec 21, 2023
1 parent 9ef5c90 commit bb0ad39
Show file tree
Hide file tree
Showing 2 changed files with 222 additions and 0 deletions.
13 changes: 13 additions & 0 deletions 2023/Day_21/Day_21.hs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module Main where

import Data.Either
import Data.List (stripPrefix)
import Data.Matrix
import Data.NumInstances.Tuple
import Data.Set (Set, notMember, insert, singleton, size, unions, empty)
Expand Down Expand Up @@ -60,10 +61,22 @@ partTwo input = result
-- Do targetX * coefs and get the result
result = round $ (targetX `multStd` coefs) ! (1, 1)

plot :: (Int, Int) -> Input -> String
plot (width, height) input = unlines rows'
where results = getReachable width input
pointHeight = (last results `div` 100 + 1) * 100 `div` (height - 1)
getChar row res | res `div` pointHeight == row = 'X'
| otherwise = '.'
rows = [[getChar row res | res <- results] | row <- [0 .. height]]
headerRow = replicate (length results) '-'
rows' = map ('|' :) . reverse $ headerRow : rows

compute :: Input -> String -> IO ()
compute input "parse" = print input
compute input "one" = print . partOne $ input
compute input "two" = print . partTwo $ input
compute input "plot" = putStrLn . plot (131, 20) $ input
compute input arg | Just params <- stripPrefix "plot=" arg = putStrLn . plot (read params) $ input
compute input _ = error "Unknown part"

main = do
Expand Down
209 changes: 209 additions & 0 deletions 2023/Day_21/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,210 @@
## Day 21

I don't even want to write something for this one.
Having two days in a row where the input has special properties that you need to find out (properties that are not necessarilly present in the examples) is just annoying in my opinion 😿

## The input:

Another grid problem, another use for [Data.Matrix](https://hackage.haskell.org/package/matrix-0.3.6.3/docs/Data-Matrix.html)

```hs
data Input = Input { start :: (Int, Int), grid :: Matrix Char } deriving (Show)

parseInput :: String -> Input
parseInput input = Input start grid
where grid = (fromLists . lines) input
start = head [(i, j) | (i, row) <- zip [1 .. ] (lines input),
(j, char) <- zip [1 .. ] row,
char == 'S']
```

I simply split my input file by lines, and transform it into a matrix. To get the starting point, I get the coordinate of the "S" tile. Nothing fancy here.

### 😿 Stupid input property number one: The starting point is always at the center of the file.

## How to do part one in multiple iterations:

Part one is about finding reachable tiles within a given number of steps. This screamed "[BFS](https://en.wikipedia.org/wiki/Breadth-first_search)!" in my head.

So my first algorithm was simple: I had a queue of pairs of the form (step_number, position) for each tile I would visit.
Then when the element in my queue has the target step_number, I put it in a list.
When the first element is not the target step number, I simply get my neighbours and put them back in the queue before going to the next element of my queue.

This was a silly algorithm. It worked, but quite slowly (4 seconds). My first mistake was to separate elements of my queue by positions instead of separating them by step number.

In fact, by reworking my algorithm to use pairs of the form (step number, positionS) where positions is the set of tiles at the beginning of the step, I was able to:
- Speed up my code from 4s to 0.4s
- Remove entirely the notion of queue. I was just changing the state instead.

One last optimisation that I did is, in my opinion, quite interesting:
- Let's say that I am currently on state B. My next state will be step C and my previous step will be state A.
Because of the way moving works, every tile in step A will be also present in step C (basically we're moving backwards)
Therefore, to get the number of tiles at a new state, I simply need to look at the tiles that do not involve moving backwards. I simply need to look at the tiles that weren't in the previous state, and add the size of the previous state to the new state's size to get the answer.

In order to implement that, instead of returning my list of tiles, my BFS returns the list of intermediate results (the size of each tile set) which is computed through an accumulator.
When computing the size of the current state, I simply need to add the second element in my accumulator (starting with 0 for the first two states) to get my actual result.

Once I'm done, I can reverse my accumulator and remove the first two elements to re-order the states by number of steps (0 step will now be at index 0, 1 step at index 1 etc.)

```hs
getReachable :: Int -> Input -> [Int]
getReachable n (Input start grid) = drop 2 . reverse . bfs (0, singleton start) empty $ [0, 0]
where wrapAround (r, c) = ((r - 1) `mod` nrows grid + 1, (c - 1) `mod` ncols grid + 1)
isInWall pos = '#' == grid ! pos
bfs (step, poss) old acc | step == n = acc'
| otherwise = bfs state' poss acc'
where neighboursOf pos = S.fromList .
filter (not . isInWall . wrapAround) .
filter (`notMember` old) $
[pos + dp | dp <- [(-1, 0), (1, 0), (0, -1), (0, 1)]]
acc' = (acc !! 1 + size poss) : acc
state' = (step + 1, unions . S.map neighboursOf $ poss)
```

And voilà! This runs way faster!

This iterative process of optimising my solution was actually quite interesting, and made me not totally dislike today's puzzle (wait for when I'm going to talk about part 2 to understand why I dislike it)

It also explains why my getReachable function looks like it does (it explains why I didn't use iterative, why (step, poss) is a tuple but old and acc are two separate arguments etc etc.)

And now to get my actual answer, I simply get the last value after 64 steps:
```hs
partOne :: Input -> Output
partOne = last . getReachable 64
```

## Too many things to notice:

Alright, it's time to look at the input and notice:
- S is at the center of the map
- The middle row and column are empty of any wall, making sure that it is possible to reach the edge in 65 steps (side length / 2). This means that in 131 steps you are "back" at the center of a chunk.
- 26501365 is 2023 * 100 * 131 + 65, which means that it is 65 modulo 131.
- If you plot the (step, number of tiles) points on a graph, you get this:

```
|.......................................................................................................................................................
|.......................................................................................................................................................
|.....................................................................................................................................................XX
|...................................................................................................................................................XX..
|.................................................................................................................................................XX....
|...............................................................................................................................................XX......
|............................................................................................................................................XXX........
|..........................................................................................................................................XX...........
|........................................................................................................................................XX.............
|......................................................................................................................................XX...............
|....................................................................................................................................XX.................
|..................................................................................................................................XX...................
|...............................................................................................................................XXX.....................
|.............................................................................................................................XX........................
|...........................................................................................................................XX..........................
|........................................................................................................................XXX............................
|......................................................................................................................XX...............................
|...................................................................................................................XXX.................................
|.................................................................................................................XX....................................
|..............................................................................................................XXX......................................
|............................................................................................................XX.........................................
|.........................................................................................................XXX...........................................
|......................................................................................................XXX..............................................
|...................................................................................................XXX.................................................
|................................................................................................XXX....................................................
|.............................................................................................XXX.......................................................
|..........................................................................................XXX..........................................................
|......................................................................................XXXX.............................................................
|...................................................................................XXX.................................................................
|................................................................................XXX....................................................................
|............................................................................XXXX.......................................................................
|........................................................................XXXX...........................................................................
|....................................................................XXXX...............................................................................
|................................................................XXXX...................................................................................
|...........................................................XXXXX.......................................................................................
|......................................................XXXXX............................................................................................
|................................................XXXXXX.................................................................................................
|..........................................XXXXXX.......................................................................................................
|..................................XXXXXXXX.............................................................................................................
|........................XXXXXXXXXX.....................................................................................................................
|XXXXXXXXXXXXXXXXXXXXXXXX...............................................................................................................................
|-------------------------------------------------------------------------------------------------------------------------------------------------------
```

THIS IS JUST A QUADRATIC EQUATION... 😾

Well, it is not an exact quadratic equation, but it turns out that, thanks to the properties of the input, it is possible to find one that will be exact for every x that is 65 modulo 131!

In order to do that, I simply perform some simple maths:
- First of all, I get three points that are 65 modulo 131. I choose 65, 196 and 327 (that is half side, half side + side, half side + 2 * side)
- Next I apply the following maths:
```
Basically I want to find a b and c such that
y0 = ax0^2 + bx0 + c
y1 = ax1^2 + bx1 + c
y2 = ax2^2 + bx2 + c
So I have a matrix matY:
(y0)
(y1)
(y2)
and a matrix matX:
(x0^2 x0 1)
(x1 ^2 x1 1)
(x2^2 x2 1)
And I want my matrix coefs:
(a)
(b)
(c)
such that
matY = matX * coefs
so:
coefs = matX^-1 * matY
Now, I simply take the matrix targetX:
(26501365^2 26501365 1)
and I do targetX * coefs, which is just doing
a * 26501365^2 + b * 26501365 + c
so I have my result
and I round because computers suck at maths
```

(Explanations pasted from a discussion I had with a friend 😸)

Put in code, this gives me:
```hs
partTwo :: Input -> Output
partTwo input = result
where rows = nrows $ grid input
-- Get the results at three points: (half height, half height + height, half height + 2 height)
-- We will call them x1, x2, x3
results = getReachable ((5 * rows) `div` 2) input
xs = [i * rows + rows `div` 2 | i <- [0 .. 2]]
ys = [fromIntegral $ results !! x | x <- xs]

{- Make two matrices:
- a 3x3 matrix where each row is [x ^ 2, x, 1] for each x in our three points
- a 3x1 matrix where each row is the number of tiles reachable after x1, x2 and x3
-}
matX = fromLists [map fromIntegral [x ^ 2, x, 1] | x <- xs]
matY = fromList 3 1 ys

-- Solve matY = matX * coefs for unknown coefs
Right iX = inverse matX
coefs = iX `multStd` matY

-- Get the [x^2, x, 1] for the target step count
target = 26501365
targetX = fromList 1 3 [target ^ 2, target, 1]

-- Do targetX * coefs and get the result
result = round $ (targetX `multStd` coefs) ! (1, 1)
```


Goodbye cruel puzzle!

0 comments on commit bb0ad39

Please sign in to comment.