Skip to content

Commit 0872902

Browse files
committed
feat(2025/Day07): writeup
1 parent e1299b9 commit 0872902

3 files changed

Lines changed: 194 additions & 4 deletions

File tree

2025/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ Watch me code in Haskell for 12 days straight
1010
| 03 | [Code](https://github.com/Sheinxy/Advent-Of-Code/blob/main/2025/src/Day03/Main.hs) | [WriteUp](./writeups/Day03) |||
1111
| 04 | [Code](https://github.com/Sheinxy/Advent-Of-Code/blob/main/2025/src/Day04/Main.hs) | [WriteUp](./writeups/Day04) |||
1212
| 05 | [Code](https://github.com/Sheinxy/Advent-Of-Code/blob/main/2025/src/Day05/Main.hs) | [WriteUp](./writeups/Day05) |||
13-
| 06 | [Code](https://github.com/Sheinxy/Advent-Of-Code/blob/main/2025/src/Day06/Main.hs) | [WriteUp](./writeups/Day06) | | |
14-
| 07 | [Code](https://github.com/Sheinxy/Advent-Of-Code/blob/main/2025/src/Day07/Main.hs) | [WriteUp](./writeups/Day07) | | |
13+
| 06 | [Code](https://github.com/Sheinxy/Advent-Of-Code/blob/main/2025/src/Day06/Main.hs) | [WriteUp](./writeups/Day06) |||
14+
| 07 | [Code](https://github.com/Sheinxy/Advent-Of-Code/blob/main/2025/src/Day07/Main.hs) | [WriteUp](./writeups/Day07) |||
1515
| 08 | [Code](https://github.com/Sheinxy/Advent-Of-Code/blob/main/2025/src/Day08/Main.hs) | [WriteUp](./writeups/Day08) | | |
1616
| 09 | [Code](https://github.com/Sheinxy/Advent-Of-Code/blob/main/2025/src/Day09/Main.hs) | [WriteUp](./writeups/Day09) | | |
1717
| 10 | [Code](https://github.com/Sheinxy/Advent-Of-Code/blob/main/2025/src/Day10/Main.hs) | [WriteUp](./writeups/Day10) | | |

2025/src/Day07/Main.hs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,14 @@ parseInput = (fst . findJust ((== 'S') . snd) &&& map fst . filter ((== '^') . s
1616
. index2D . lines
1717

1818
findStop :: (Int, Int) -> [(Int, Int)] -> Maybe (Int, Int)
19-
findStop (r, c) splitters = find (\(i, j) -> r <= i && j == c) splitters
19+
findStop (r, c) = find (\(i, j) -> r <= i && j == c)
2020

2121
partOne :: Input -> Output
2222
partOne (start, splitters) = S.size . memoFix go $ start
2323
where go f s = case findStop s splitters of
2424
Nothing -> S.empty
2525
Just (i, j) -> S.insert (i, j) $ S.union (f (i, j - 1)) (f (i, j + 1))
2626

27-
2827
partTwo :: Input -> Output
2928
partTwo (start, splitters) = memoFix go start
3029
where go f s = case findStop s splitters of

2025/writeups/Day07/README.md

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
## Day 07
2+
3+
Dynamic programming? Pruning? Only computing what’s necessary?
4+
5+
Nah, I’mma just memoise :)
6+
7+
## The Input
8+
9+
The input is simply a 2D grid with three types of tiles:
10+
11+
- `.` — an empty tile
12+
- `S` — the starting position
13+
- `^` — a splitter tile
14+
15+
We only actually care about the last two, so my input is a tuple containing the starting position and the positions of all splitter tiles:
16+
17+
```hs
18+
type Input = ((Int, Int), [(Int, Int)])
19+
```
20+
21+
To get them, I start by indexing the grid using a little utility function:
22+
23+
```hs
24+
-- Takes a 2D list and returns an indexed 1D list.
25+
index2D :: [[a]] -> [((Int, Int), a)]
26+
index2D g = [((i, j), x) | (i, row) <- zip [0..] g, (j, x) <- zip [0..] row]
27+
```
28+
29+
Parsing the input is straightforward:
30+
31+
- split into lines,
32+
- index the grid,
33+
- find the element containing `S`,
34+
- filter all elements containing `^`.
35+
36+
```hs
37+
parseInput :: String -> Input
38+
parseInput = (fst . findJust ((== 'S') . snd) &&& map fst . filter ((== '^') . snd))
39+
. index2D . lines
40+
```
41+
42+
## Part One
43+
44+
### The Problem
45+
46+
A beam starts by going down from the starting position. When it hits a splitter, it splits into two beams (one going left, one going right), then continues downward. When two beams arrive at the same position, they merge into a single beam.
47+
48+
How many times does the beam split?
49+
50+
Or phrased differently: how many splitter tiles do the beams touch?
51+
52+
### The Solution
53+
54+
Let’s start naïvely and work up from there.
55+
56+
First question: given a starting position, which splitter (if any) will the beam encounter?
57+
58+
This is simple: it’s just the first splitter below it in the same column.
59+
60+
Because my list is ordered (thanks to how I parsed the input), I can use [`find`](https://hackage.haskell.org/package/base-4.21.0.0/docs/Data-List.html#v:find) to locate the first valid candidate:
61+
62+
```hs
63+
findStop :: (Int, Int) -> [(Int, Int)] -> Maybe (Int, Int)
64+
findStop (r, c) = find (\(i, j) -> r <= i && j == c)
65+
```
66+
67+
Once we find a splitter, what do we do?
68+
69+
Pretty simple: create two beams and recurse.
70+
71+
```hs
72+
partOne :: Input -> Output
73+
partOne (start, splitters) = length $ go start
74+
where go s = case findStop s splitters of
75+
Nothing -> []
76+
Just (i, j) -> (i, j) : (go (i, j - 1) ++ go (i, j + 1))
77+
```
78+
79+
Does it work?
80+
81+
No :)
82+
83+
```
84+
➜ Advent-Of-Code git:(main) ✗ cabal run AOC2025 07 toomanycooks 2025/inputs/07.sample
85+
Day 07:
86+
39
87+
Part toomanycooks: 252.2 μs
88+
Total:
89+
1.070 ms
90+
```
91+
92+
The issue is pretty easy to grasp. Consider the following example (taken from the puzzle description):
93+
94+
```
95+
.......S.......
96+
...............
97+
.......^.......
98+
...............
99+
......^.^......
100+
...............
101+
.....^.^.^.....
102+
```
103+
104+
Multiple beams can hit the same splitters. The simple fix is to `nub` the result, or store it in a `Set`.
105+
106+
```hs
107+
partOne :: Input -> Output
108+
partOne (start, splitters) = length (go start)
109+
where go s = case findStop s splitters of
110+
Nothing -> []
111+
Just (i, j) -> S.insert (i, j) $ S.union (go (i, j - 1)) (go (i, j + 1))
112+
```
113+
114+
However, we’ve got another issue: this is slow because we're recomputing the same things over and over.
115+
We *could* optimise by not recomputing already computed things, but honestly: memoizing everything is much easier :)
116+
117+
To do that, I’m using [`memoFix`](https://hackage.haskell.org/package/memoize-1.1.2/docs/Data-Function-Memoize.html#v:memoFix). (Maybe one day I'll write a blog article explaining how it works, because I love this function).
118+
119+
```hs
120+
partOne :: Input -> Output
121+
partOne (start, splitters) = S.size . memoFix go $ start
122+
where go f s = case findStop s splitters of
123+
Nothing -> S.empty
124+
Just (i, j) -> S.insert (i, j) $ S.union (f (i, j - 1)) (f (i, j + 1))
125+
```
126+
127+
Sure, it’s not the *fastest* possible solution. We could optimise in a few ways:
128+
129+
- prune search paths since beams only go downward,
130+
- avoid recomputing beams we later `nub` anyway.
131+
132+
But honestly, I don’t care. It’s simple, elegant, and fast enough:
133+
134+
```
135+
➜ Advent-Of-Code git:(main) ✗ cabal run AOC2025 07 one 2025/inputs/07
136+
Day 07:
137+
1711
138+
Part one: 40.09 ms
139+
Total:
140+
40.78 ms
141+
```
142+
143+
## Part Two
144+
145+
### The Problem
146+
147+
Now we need to count the number of unique paths a beam can take.
148+
149+
### The Solution
150+
151+
Since the beam always travels downward, there are only two possibilities when it starts:
152+
153+
- it never encounters a splitter -> exactly 1 path
154+
- it hits a splitter -> the total paths equal the sum of the paths from both new beams
155+
156+
```hs
157+
partTwo :: Input -> Output
158+
partTwo (start, splitters) = go start
159+
where go s = case findStop s splitters of
160+
Nothing -> 1
161+
Just (i, j) -> go (i, j - 1) + go (i, j + 1)
162+
```
163+
164+
But once again, we’re recomputing the same things a lot.
165+
166+
The easiest solution is once again to cache the result in order to not compute them again. Let's memoize!
167+
168+
```hs
169+
partTwo :: Input -> Output
170+
partTwo (start, splitters) = memoFix go start
171+
where go f s = case findStop s splitters of
172+
Nothing -> 1
173+
Just (i, j) -> f (i, j - 1) + f (i, j + 1)
174+
```
175+
176+
Still very fast :D
177+
178+
```
179+
➜ Advent-Of-Code git:(main) ✗ cabal run AOC2025 07
180+
Day 07:
181+
1711
182+
Part one: 41.60 ms
183+
36706966158365
184+
Part two: 11.67 ms
185+
Total:
186+
54.23 ms
187+
```
188+
189+
## Conclusion
190+
191+
Memoization goes brrrrrrrrrr :3

0 commit comments

Comments
 (0)