Skip to content

Commit 4e46f67

Browse files
committed
Add day 19 article.
1 parent e30abb5 commit 4e46f67

File tree

1 file changed

+187
-0
lines changed

1 file changed

+187
-0
lines changed

docs/2024/puzzles/day19.md

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,197 @@ import Solver from "../../../../../website/src/components/Solver.js"
22

33
# Day 19: Linen Layout
44

5+
by [Paweł Cembaluk](https://github.com/AvaPL)
6+
57
## Puzzle description
68

79
https://adventofcode.com/2024/day/19
810

11+
## Solution Summary
12+
13+
The puzzle involves arranging towels to match specified patterns. Each towel has a predefined stripe sequence, and the
14+
task is to determine:
15+
16+
- **Part 1**: How many patterns can be formed using the available towels?
17+
- **Part 2**: For each pattern, how many unique ways exist to form it using the towels?
18+
19+
The solution leverages regular expressions to validate patterns in Part 1 and employs recursion with memoization for
20+
efficient counting in Part 2.
21+
22+
## Part 1
23+
24+
### Parsing the Input
25+
26+
The input consists of two sections:
27+
28+
- **Towels**: A comma-separated list of towels (e.g., `r, wr, b, g`).
29+
- **Desired Patterns**: A list of patterns to match, each on a new line.
30+
31+
To parse the input, we split it into two parts: towels and desired patterns. Towels are extracted as a comma-separated
32+
list, while patterns are read line by line after a blank line. We also introduce type aliases `Towel` and `Pattern` for
33+
clarity in representing these inputs.
34+
35+
Here’s the code for parsing:
36+
37+
```scala 3
38+
type Towel = String
39+
type Pattern = String
40+
41+
def parse(input: String): (List[Towel], List[Pattern]) =
42+
val Array(towelsString, patternsString) = input.split("\n\n")
43+
val towels = towelsString.split(", ").toList
44+
val patterns = patternsString.split("\n").toList
45+
(towels, patterns)
46+
```
47+
48+
### Solution
49+
50+
To determine if a pattern can be formed, we use a regular expression. While this could be done manually by checking
51+
combinations, the tools in the standard library make it exceptionally easy. The regex matches sequences formed by
52+
repeating any combination of the available towels:
53+
54+
```scala 3
55+
def isPossible(towels: List[Towel])(pattern: Pattern): Boolean =
56+
val regex = towels.mkString("^(", "|", ")*$").r
57+
regex.matches(pattern)
58+
```
59+
60+
`towels.mkString("^(", "|", ")*$")` builds a regex like `^(r|wr|b|g)*$`. Here’s how it works:
61+
62+
- `^`: Ensures the match starts at the beginning of the string.
63+
- `(` and `)`: Groups the towel patterns so they can be alternated.
64+
- `|`: Acts as a logical OR between different towels.
65+
- `*`: Matches zero or more repetitions of the group.
66+
- `$`: Ensures the match ends at the string’s end.
67+
68+
This approach is simplified because we know the towels contain only letters. If the input could be any string, we would
69+
need to use `Regex.quote` to handle special characters properly.
70+
71+
Finally, using the `isPossible` function, we filter and count the patterns that can be formed:
72+
73+
```scala 3
74+
def part1(input: String): Int =
75+
val (towels, patterns) = parse(input)
76+
patterns.count(isPossible(towels))
77+
```
78+
79+
## Part 2
80+
81+
To count all unique ways to form a pattern, we start with a base algorithm that recursively matches towels from the
82+
start of the pattern. For each match, we remove the matched part and solve for the remaining pattern. This ensures we
83+
explore all possible combinations of towels. Since the numbers involved can grow significantly, we use `Long` to handle
84+
the large values resulting from these calculations.
85+
86+
Here’s the code for the base algorithm:
87+
88+
```scala 3
89+
def countOptions(towels: List[Towel], pattern: Pattern): Long =
90+
towels
91+
.collect {
92+
case towel if pattern.startsWith(towel) => // Match the towel at the beginning of the pattern
93+
pattern.drop(towel.length) // Remove the matched towel
94+
}
95+
.map { remainingPattern =>
96+
if (remainingPattern.isEmpty) 1 // The pattern is fully matched
97+
else countOptions(towels, remainingPattern) // Recursively solve the remaining pattern
98+
}
99+
.sum // Sum the results for all possible towels
100+
```
101+
102+
That's not enough though. The above algorithm will repeatedly solve the same sub-patterns quite often, making it
103+
inefficient. To optimize it, we introduce memoization. Memoization stores results for previously solved sub-patterns,
104+
eliminating redundant computations. We also pass all the patterns to the function to fully utilize the memoization
105+
cache.
106+
107+
Here's the code with additional cache for already calculated sub-patterns:
108+
109+
```scala 3
110+
def countOptions(towels: List[Towel], patterns: List[Pattern]): Long =
111+
val cache = mutable.Map.empty[Pattern, Long]
112+
113+
def loop(pattern: Pattern): Long =
114+
cache.getOrElseUpdate( // Get the result from the cache
115+
pattern,
116+
// Calculate the result if it's not in the cache
117+
towels
118+
.collect {
119+
case towel if pattern.startsWith(towel) => // Match the towel at the beginning of the pattern
120+
pattern.drop(towel.length) // Remove the matched towel
121+
}
122+
.map { remainingPattern =>
123+
if (remainingPattern.isEmpty) 1 // The pattern is fully matched
124+
else loop(remainingPattern) // Recursively solve the remaining pattern
125+
}
126+
.sum // Sum the results for all possible towels
127+
)
128+
129+
patterns.map(loop).sum // Sum the results for all patterns
130+
```
131+
132+
Now, we just have to pass the input to the `countOptions` function to get the final result:
133+
134+
```scala 3
135+
def part2(input: String): Long =
136+
val (towels, patterns) = parse(input)
137+
countOptions(towels, patterns)
138+
```
139+
140+
## Final code
141+
142+
```scala 3
143+
type Towel = String
144+
type Pattern = String
145+
146+
def parse(input: String): (List[Towel], List[Pattern]) =
147+
val Array(towelsString, patternsString) = input.split("\n\n")
148+
val towels = towelsString.split(", ").toList
149+
val patterns = patternsString.split("\n").toList
150+
(towels, patterns)
151+
152+
def part1(input: String): Int =
153+
val (towels, patterns) = parse(input)
154+
val possiblePatterns = patterns.filter(isPossible(towels))
155+
possiblePatterns.size
156+
157+
def isPossible(towels: List[Towel])(pattern: Pattern): Boolean =
158+
val regex = towels.mkString("^(", "|", ")*$").r
159+
regex.matches(pattern)
160+
161+
def part2(input: String): Long =
162+
val (towels, patterns) = parse(input)
163+
countOptions(towels, patterns)
164+
165+
def countOptions(towels: List[Towel], patterns: List[Pattern]): Long =
166+
val cache = mutable.Map.empty[Pattern, Long]
167+
168+
def loop(pattern: Pattern): Long =
169+
cache.getOrElseUpdate(
170+
pattern,
171+
towels
172+
.collect {
173+
case towel if pattern.startsWith(towel) =>
174+
pattern.drop(towel.length)
175+
}
176+
.map { remainingPattern =>
177+
if (remainingPattern.isEmpty) 1
178+
else loop(remainingPattern)
179+
}
180+
.sum
181+
)
182+
183+
patterns.map(loop).sum
184+
```
185+
186+
## Run it in the browser
187+
188+
### Part 1
189+
190+
<Solver puzzle="day19-part1" year="2024"/>
191+
192+
### Part 2
193+
194+
<Solver puzzle="day19-part2" year="2024"/>
195+
9196
## Solutions from the community
10197

11198
- [Solution](https://github.com/nikiforo/aoc24/blob/main/src/main/scala/io/github/nikiforo/aoc24/D19T2.scala) by [Artem Nikiforov](https://github.com/nikiforo)

0 commit comments

Comments
 (0)