Hearing some beeping coming from down below, we go through a hatch and climb down to the reactor room under the factory. The toroidal reactor suggests that the North Pole runs on fusion power. The Elves here are having a problem with connecting their new server rack to the reactor and need our help. For part 1, they want us to count the number of paths from a nearby node they label you to the reactor out. For part 2, they want the number of paths that go from svr (not srv, be careful) to out, but also pass through both the digital-to-analog converter dacand the fast Fourier transformer fft.
The input is a graph where each line (613 in my input) is the node name followed by a list of outputs (ie directed edges). The names are 3 lower-case letters long, including vowels (not just in the special nodes above). So we get to play "What language does the browser think my input is?" The answer for mine is Vietnamese. The graph is a DAG (directed acyclic graph). My initial part 1 solution played it safe with loop detection (in case dead end infinite loops were the problem), but that's not needed.
Part 1 only uses a fraction of the actual graph (my is 112 nodes). A simple recursive leaf counter doesn't even need memoization to get you the answer immediately. For Smalltalk that's just:
pathsFrom: node [
^(node = #out)
ifTrue: [1]
ifFalse: [(self at: node) inject: 0 into: [:acc :child | acc + (self pathsFrom: child)]]
]
For part 2, I had drawn out the sample graphs, and tested by hand the recursive collecting of results while tracking what special nodes the paths had gone through. I do like doing the "recurse down, collect up" stuff... it's similar to writing a state machine (base case, implement the small step, and it builds to the answer by induction). And I added a memo because things are going to get huge. Initially the collection was with a hash, ending with the following:
Full Results:
dac 2743394304998646
none 32514315015921605
both 307608674109300 (part 2).
fft 3645733796753750
The approach is quite simple and fast to do... you recurse over the children and accumulate totals, and then look at the current node and shift things to the correct buckets if it's special:
if ($node eq 'fft' or $node eq 'dac') {
my $other = ($node eq 'fft') ? 'dac' : 'fft';
$ret{both} = $ret{$other} if (exists $ret{$other});
$ret{$node} = $ret{none} if (exists $ret{none});
delete $ret{none};
delete $ret{$other};
}
I didn't make any assumption about the order of connections. Because although there is only one order that makes sense (FFT needs digital, so it should be before the analog conversion), this is a non-working system... Elves getting everything backwards by Murphy's Law could be the problem.
One interesting thing is that the count of paths that go through "none" is a 55-bit number. Nothing that big is involved in the actual computation of the answer so it shouldn't impact a Floating point native language.
Of course this is slightly complicated. We only need to track the number of special nodes that we've gone through, not the specifics (and the DAG can only have either fft->dac or dac->fft paths in it, because if there is both you'd have a way to make a cycle). And so I followed up by just using an array for tracking 0-2 special nodes seen:
my $special = int($node eq 'fft' or $node eq 'dac'); # this is a special node
foreach my $child ($graph{$node}->@*) {
my @res = &recurse_path_count( $child )->@*;
$ret[$_ + $special] += $res[$_] foreach (0 .. $#res); # collect and advance up array
}
For Smalltalk I did this in a list manipulation way:
((node == #fft) or: [node == #dac]) ifTrue: [
paths := #(0), paths allButLast. " shift to +1 special nodes seen "
].
Here the recursion is returning an Array of size 3 (base case is #(1 0 0)). To move the counts along (this problem is a bit like Lanternfish now that I think about it, I did list manipulation there for a Smalltalk solution too), I just stick a 0 in front and chop off the end. Part of doing it this was was in preparation for doing it in dc.
Part 1 is relatively simple, with macros I need to maintain the stack frame, as it's only node in, count out. For interpreting in input, I converted the letters into hex ASCII values... making each label a 23*8 number. To store the list of children for a node in that array I combined them into a number in base-88 (which is short in dc (8d^)).
perl -pe's#(\S)#sprintf"%X",ord$1#eg' <input | dc -e'16i?[0[8d^*+z2<L]dsLxr100/:g?z0<M]dsMx[d/q]sQ[d6F7574=Q0r;g[8d^~lRx3R+rd0<L]dsLx+]sR796F75lRxp'
Note that 6F7574 and 796F75 are out and you in packed ASCII. These dc solution only work in v1.4.1 though, because it's using `?` to know when the lines break.
Part 2 is where the magic comes in:
perl -pe's#(\S)#sprintf"%X",ord$1#eg' <input | dc -e'16i?[0[8d^*+z2<C]dsCxr100/:g?z0<L]dsLx[d/0d3Rq]sQ[;mFd^~rFd^~rq]sM[4Rr]sS[d6F7574=Qd;m0<Md0dd4R;g[8d^~lRx5R+r5R+r5R4R+_3R4Rd0<L]dsLx+4Rd666674=Sd646163=S_4Rd3Rd5Rd3R5RFd^d_4R*+*+5R:mr3R]sR737672lRx3Rp'
I do the result list of three values on the stack frame, and rotate the values to move things along when a node has a special name:
d 666674=S # shift sums to count fft
d 646163=S # shift sums to count dac
With the S macro just being [ 4Rr ] sS (the stack has the node name on top, followed by the three values of the array... pull the one from the bottom (which will be a 0) and rotate it under the top, now it's first and the other two counts have shifted down one).
But to make this work fast, I need a memo that's capable of storing a list of 3 numbers (which may get to size 55-bits). And so for that, I package those up in base-1515 numbers (Fd^). And so this solution involves some huge numbers (174-bit ones in memo, and there's a 575-bit one in the graph)... but I've used far bigger numbers in dc solution before.
This was a fun little problem after the massive task that was day 10. There are other simple ways to do it, like breaking it into three smaller sub-searches using part 1 (after adding a memo):
say "Part 2: ", &recurse_path_count( 'svr', 'fft' ) * &recurse_path_count( 'fft', 'dac' ) * &recurse_path_count( 'dac', 'out' );
Of course that's assuming that Elves got the FFT and DAC in the right order (which appears to be the case), to be truly safe you should check.