@@ -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
79https://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