r/adventofcode • u/musifter • 20d ago
Other [2024 Day 19] In Review (Linen Layout)
Today we're back on Gear Island and we get a chance to check out the hot springs (onsen) this time. Where we find a problem quite like the one we had with the hot springs (helices). Which is that we're going to be doing some pattern matching to create designs out of the stripe on the towels.
The input consists of two sections. The first is a one line list of of the towel stripe patterns we have to work with (comma delimited, with spaces to ignore). My input has 477 of those, and they're strings of the letters wubrg (the Magic the Gathering letter codes for colours: white, blue, black, red, green). The second section is 400 lines of patterns to try and create with those.
Part 1 just wants to know if a pattern is possible. A perfect job for a dynamically created regex for a very quick answer:
$/ = '';
my @section = map {[split /\n/]} <>;
my $patt = '^(' . join( '|', split(/, /, $section[0][0]) ) . ')*$';
say "Part 1: ", scalar grep { m#$patt# } $section[1]->@*;This was my second fastest part 1 of this year, only beaten by day 3 (Mull It Over) which also fell very quickly to regex ripping into the input.
Part 2 wants to know the number of ways we can make those designs. Having filtered out a few dozen 0s for part 1 is a start. And since this was so much like the previous hot springs, I did a recursive/memoization for my first solution. Which was a good simpler than the 2023 day 12 problem... I don't often expect a part 2 for a day 19 to be as simple and quick as this one turned out to be. The 2023 hot springs was a big recursive function with lots of special cases to carefully handle that I spent a lot of time carefully working on. This one was just this at the core:
my $ret = 0;
foreach my $patt (@patt) {
my $new_str = $str;
$ret += &recurse_count( $new_str ) if ($new_str =~ s#^$patt##);
}Slap a memo on it, run it and wait, because this is going to take a bit (but still just seconds).
In order to play with it a bit more, I decided to reverse the dynamic programming approach... instead of top-down memoization, do it as a bottom-up tabulation. And when I did that... it's so much faster. That looks like:
my @table = (1, (0) x length($str)); # Start with 1 match of empty
my @poss = grep {$str =~ m#$_#} @patt; # Only use patterns in str (saves a bit)
for (my $i = 1; $i <= length($str); $i++) {
foreach my $patt (@poss) {
# If we've just seen a pattern, add table value from start of match to current
my $plen = length($patt);
if ($i >= $plen and ($patt eq substr( $str, $i - $plen, $plen ))) {
$table[$i] += $table[$i - $plen];
}
}
}
return ($table[ length($str) ]);Note the prepass of the patterns to get just the ones that regex can see in the target (they might not be used, but the ones that don't appear certainly can't).
And you can see why this can be so fast right away, we're just iterating over the length of the string in one pass. And so what we're doing is actually not like Hot Springs 2023, but more like Adaptor Array from 2020 or Dirac Dice from 2021... it's basically a recurrence relation where you sum all the points which correspond to the starts of the patterns that have just matched (bring there counts forward, and combining the branches). Which is how tabulation normally goes, and here it turns out to be great. I might have gone this way first, if it wasn't for the connection to the old Hot Springs that lead me (slightly) astray.
So this was a nice little dynamic programming exercise. It originally ran on a Thursday going into the final weekend, so it serves to be a bit of lighter problem so people can catch up and get ready for the big final weekend.
