Recursive caves
Day 12 asks to find paths in a directed cyclic graph, whereby the root and the outer most leaf are only visited once. This is basically a call-tree of a recursive function.
sub day12(:$verbose) {
my @caves =
<start-A start-b A-c A-b b-d A-end b-end>,
<dc-end HN-start start-kj dc-start dc-HN LN-dc HN-end kj-sa kj-HN kj-dc>,
<fs-end he-DX fs-he start-DX pj-DX end-zg zg-sl zg-pj pj-he RW-he fs-DX pj-RW zg-RW start-pj he-WI zg-he pj-fs start-RW>;
my @number-of-paths;
for @caves -> @cave {
say ‚Cave ‘ ~ ++$ ~ ‚:‘ if $verbose;
my %connections.append: @cave».split('-').map(-> [$a, $b] { $a => $b, $b => $a }).flat;
my \paths = gather {
sub walk($node, @so-far is copy, %seen is copy) {
return if %seen{$node}:exists;
@so-far.push: $node;
(take @so-far; return) if $node eq 'end';
%seen{$node} = True if $node eq $node.lc;
.&walk(@so-far, %seen) for %connections{$node}.flat;
}
walk 'start', [], %()
}
paths.&{@number-of-paths.push(.elems); $_}.sort».join(',')».&{.say if $verbose};
}
printf(„There are %d paths in cave %d.\n“, |$_) for (@number-of-paths Z ++$ xx ∞);
}
There isn’t much trickery going on here. I build a list of cave networks and a Hash
for each of them, where the keys are cave names and the leafs are lists of cave names. This mapping has to happen in both ways, because I can’t easily flip keys and values, if the values are lists. If you would watch the call stack, you could see that paths show up in walk($name, ...)
. Raku wont let us walk back the call stack to collect all the $name
s, so I keep my own stack in @so-far
. Once the “end” cave is hit, I exfiltrate that stack with take
.
This is a straight forward solution that comes with a lot of problems. The biggest problem is, that I rarely use any language features that Raku got and lesser languages don’t. After all, the purpose of AoC is to show off. Also, blogposts that state the obvious are a waste-of-time². I don’t need to write them and you don’t need to read them. So let’s have a different version of this program.
sub day12(:$verbose) {
my @caves = <start-A start-b A-c A-b b-d A-end b-end>,
<dc-end HN-start start-kj dc-start dc-HN LN-dc HN-end kj-sa kj-HN kj-dc>,
<fs-end he-DX fs-he start-DX pj-DX end-zg zg-sl zg-pj pj-he RW-he fs-DX pj-RW zg-RW start-pj he-WI zg-he pj-fs start-RW>;
my \connections = @caves.map: { Map.new: $_».split('-').map(-> [$a, $b] { $a => $b, $b => $a }).flat.classify(*.key, :as(*.value)) }
my @number-of-paths = connections.hyper(:batch(1)).map( -> %connections {
say ‚Cave ‘ ~ ++$ ~ ‚:‘ if $verbose;
my \paths = gather {
for 'start', [], %() -> $node, @so-far, %seen is copy {
next if %seen{$node}:exists;
@so-far.push: $node;
(take @so-far; next) if $node eq 'end';
%seen{$node} = True if $node eq $node.lc;
%connections{$node}».&?BLOCK(@so-far, %seen)
}
}
paths.sort».join(',')».say if $verbose;
paths.elems
});
printf(„There are %d paths in cave %d.\n“, $_, ++$) for @number-of-paths;
}
The first step is to use immutable data structures where possible. Every cave is defined by its connections. Those connections can live in a Map
instead of a Hash
. The build-in .map
returns a Seq
I can bind to. By replacing for
with .map
that does immutable stuff, I can sneak a .hyper
in. By replacing the recursive sub
with a recursive &?BLOCK
I finally have used a feature, that is unique to Raku (Haskell got auto-threading iteration and the dot-notation, which is equivalent to .hyper
and ».
).
With the two .hyper
s the program has not gotten slower. The overhead to setup threading does not exceed the gain in execution speed, even with very short lists. A very good sign that large cave systems would benefit greatly from modern CPUs.
It is not enough to get the presents delivered, we also want to please Santa. Don’t we, Elfs?
-
December 13, 2021 at 21:572021.50 _ for Micros – Rakudo Weekly News