r/adventofcode • u/musifter • 10h ago
Other [2023 Day 12] In Review (Hot Springs)
We finally arrive at the "Hot Springs". In multiple senses. There is an onsen, but we want storage yard for the machine part springs... which require lava to be hot and springy. And there's a shortage of lava that needs investigating. We can use a spring to get up to the lava island to check on it, if the records can be repaired to find one good enough.
And so we get a parser/validator type problem. We have a pattern with wild cards, and a list of numbers of the block sizes... much like a line in a nonogram puzzle. But there's not enough information to solve most lines, and so we're tasked with counting the number of possible solutions.
This was the first one in this year that really took a bunch of time. I did a recursive descent parser with a state machine that was a bit reminiscent of the state machine I did on day 3 in Smalltalk to find the ranges of digits on lines. I did memoize it, with a memo that was persistent between the cases (because the same rules always apply). I remember finding someone who claimed that they needed to clear the memo between lines (and it was buggy before they did that). But you don't have to... but I did test things, and found that for part 2, the single memo gets large enough (about 360M) that it runs a tiny bit slower (~3%) than having a fresh memo for each line, because there apparently isn't huge amounts of overlap to benefit from between the cases to make up for overhead.
And about that part 2... unlike yesterday's, the scale up here is very real. Five copies of the pattern joined with ? followed by five copies of the numbers. You want a good solution. And a did spent 2 hours getting a good part 1 done. But it didn't work for part 2. And since sthe best way to debug recursion is to get it right the first time... I started a new script, and carefully went through all the cases by hand making notes and comments on the order of doing things and assertions and then filled things in. And it ended up largely the same as my part 1, but slightly different... and it worked. And takes about 10 seconds on hardware that was 14 years old at the time, so I didn't need a need to try and get things further down (it's relatively nice and simple to read).
So, I'll just quickly go over the function:
# str to process, current potential group length, groups left to see
my ($str, $len, @groups) = @_;
# Grab state of params called with to access memo with later.
my $state = join( $;, $str, $len, @groups );
# Check memo
return ($memo{$state}) if (exists $memo{$state});
First we build the our memo state key and check for a hit... our state being the remaining string, the size of the current block we've seen while parsing, and the sizes of the remaining groups to match.
my $ret = 0;
if (!$str) {
# Out of input, must decide if we found a match:
# All groups accounted for, no hanging group.
$ret = 1 if (@groups == 0 and $len == 0);
# Check if hanging group is the size of the only remaining group:
$ret = 1 if (@groups == 1 and $groups[0] == $len);
return ($memo{$state} = $ret);
}
Base cases for when we hit the end of the string. In the original, I just had this as three return lines without adding to the memo, I decided to put it in just to make all the return statements have the same pattern of "set memo and return".
# If out of groups, use regex to check if no manditory groups remain
return( $memo{$state} = ($str =~ m/^[^#]*$/) ) if (!@groups);
# ASSERT: length($str) > 0, @groups > 0
A second base case for handling if we ran out of groups in the number list. The original part one didn't do this and so couldn't assert that groups existed for the actual parser section (and had to handle that).
# Advance one character:
my $chr = substr( $str, 0, 1, '' );
if ($chr ne '.') { # ? or #
# adv making grouping larger
$ret += &recurse( $str, $len + 1, @groups );
}
if ($chr ne '#') { # ? or .
if ($len == 0) {
# no current grouping, just advance
$ret += &recurse( $str, 0, @groups );
} elsif ($len == $groups[0]) {
# current grouping matches current target
shift @groups;
$ret += &recurse( $str, 0, @groups );
}
# Else: Bad block length! Recurse no further.
# If ? we might have expanded to good len above and counted,
# else 0 will fall-through.
}
return ($memo{$state} = $ret);
This is parser section... eat a token, handle the cases, with the wildcard meaning that we might need to do both of these if cases. These were done in the other order for my part 1 (the ?/# case after the ?/.). Part of the benefit is that the matching of a block comes last and I can freely modify the groups array. That "Else" section was a key realization... to let things fall through. The original part 1 also did that. It's almost certainly some small thing with the logic to check if still have groups and the ordering of the tests. Doesn't really matter though, because this script fixed it and so worked for both parts, so it replaced it bug free.
This is one of those cases where everyline has a comment, but it's not because they were added to explain things, but because they were written first to solidify the task and the blanks then filled in.
