Advent of Code 2024-Day 19: Linen Layout
from advent import parse, words, cache
pats, designs = parse(19, words, sep='\n\n')
@cache
def check_design(design):
if design == '':
return 1
else:
return sum(check_design(design.removeprefix(pat)) for pat in pats if design.startswith(pat))
counts = [ cnt for design in designs if (cnt := check_design(design)) > 0 ]
assert len(counts) == 353 # Part 1
assert sum(counts) == 880877787214477 # Part 2
NOTE: see Postscript below for a version that's about 10x faster!
Parsing
We're given input of the following form:
I'd like two lists of words, one for the patterns and one for the designs:
Solution
I spent a lot of time messing around with a regex solution for part 1, but I ran into "catastrophic backtracking"
issues. I expect the patterns are designed to cause that issue! One good thing that came out of that struggle was
finding the regular expressions 101 website, which seems to be very handy for debugging
regexes. It had a couple suggestions for dealing with the issue, but in the end, I figured part 2 may make it
difficult for a pure regex solution anyway, so I switched gears to doing a search.
I originally did a breadth first search for part 1, but when I saw part 2, I felt a depth first solution might be
better, since we want an exhaustive search, so I unified the parts to use on depth first search function. Adding
the @cache decorator to memoize the function made it perform well enough for the stars, but it's not blazingly
fast by any stretch!
@cache
def check_design(design):
if design == '':
return 1
else:
return sum(check_design(design.removeprefix(pat)) for pat in pats if design.startswith(pat))
Rather than do the work twice, I compute a list of counts for each design (excluding zeros):
With that, the parts are easy:
- Part 1 is the length of the counts
- Part 2 is the sum of the counts
New Python Concepts
removeprefix()startswith()
Conclusion
The leaderboard showed this to be a very easy puzzle, but it took me longer than I would've liked. I bet there is an even more straightforward approach, and I'm sure there are definitely solutions that compute the answer quicker, but I'm satisfied enough with my solution to move on :)
Postscript
I found an idea from reddit user TheZigerionScammer that speeds things up by a factor of 10! In my version I iterate over all the patterns, and if the design starts with one of the patterns, I recur. The following version only loops over the first N characters of the design (up to the maximum pattern length), and if the prefix matches one of the patterns (fast lookup in a set), it recurs.
My original took 800 ms, this improved version takes 35 ms.
from advent import parse, words, cache, timeit
towels, designs = parse(19, lambda s: set(words(s)), sep='\n\n')
max_len = max(len(pat) for pat in towels)
def prefixes_suffixes(design):
for prefix_len in range(1, min(max_len, len(design)) + 1):
yield design[0:prefix_len], design[prefix_len:]
@cache
def check_design(design):
arrangements = 1 if design in towels else 0
for prefix, suffix in prefixes_suffixes(design):
if prefix in towels:
arrangements += check_design(suffix)
return arrangements
counts = [ cnt for design in designs if (cnt := check_design(design)) > 0 ]
assert len(counts) == 353 # Part 1
assert sum(counts) == 880877787214477 # Part 2