@@ -2,10 +2,197 @@ import Solver from "../../../../../website/src/components/Solver.js"
2
2
3
3
# Day 19: Linen Layout
4
4
5
+ by [ Paweł Cembaluk] ( https://github.com/AvaPL )
6
+
5
7
## Puzzle description
6
8
7
9
https://adventofcode.com/2024/day/19
8
10
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
+
9
196
## Solutions from the community
10
197
11
198
- [ 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