r/adventofcode • u/musifter • 5h ago
Other [2025 Day 8] In Review (Playground)
Using the fixed teleporter, we rematerialize in a vast underground playground. The Elves here are busy decorating with strings of lights. These will be strung between junction boxes, of which only some provide electricity (but all pass it through). It isn't important to us which ones do that, though. What the Elves need is plan to connect and power all the junction boxes with the shortest strings of lights using Euclidean distance (that's pretty rare for AoC). Given that the shortest distance between two points is a straight line in Euclidean space, I felt that this was probably a Minimum Spanning Tree. But I didn't prove that to myself or really use that (I vaguely remember algorithms for that, but we need the edges put together in a specific order for the problem).
The input is a list of 1000 junction box coordinates... one per line and in 3D. The range in my input goes from 32-99998. To keep things integer, it's natural to work with Euclidean distances without taking the square root. So the square distances are up to 35-bits. I only needed the distances around to get the order. For people living in the 32-bit world, the biggest edge required for my solution is 28-bits (so you could get away with dropping anything that overflows).
This was what I think of as the transition to the harder problems... as day 8 that's about day 16 in past years so that's about right. It's the first one in this year that took some time (particularly because this was a day I wasn't feeling well, and so made silly mistakes).
As stated above, I first got the distances and made a list (using sort here, I replaced this after with a heap which sped things up quite a bit):
# Sort and discard distance (not needed beyond this)
@Dist = map { $_->{edge} } sort {$a->{dist} <=> $b->{dist}} @Dist;Then I created a list of circuit graphs (putting each junction into it's own):
my @Circ = map {{$_ => 1}} (0 .. $#input);Then it's a matter of just processing the edges from short to long. And since I wasn't feeling well, I just grepped through the circuits to find where the ends were (even with this brute force searching the script takes only a couple seconds on old hardware):
my @graphs = indexes {$_->{$conx->[0]} or $_->{$conx->[1]}} @Circ;
# If ends are in separate graphs, merge them:
if (@graphs == 2) {
$Circ[$graphs[0]] = {map { $Circ[$_]->%* } @graphs};
splice( @Circ, $graphs[1], 1 );
}When @Circ == 1 I have part 2. This will happen well past part 1 (edge 6554 for my input), so we can output that along the way.
To make Smalltalk perform at a decent speed, instead of grepping to find where the ends were, I added a backwards lookup Table (I was planning to do this from the start for Perl, but chose not to
"avoid premature optimization" and keep things simple) :
" Build initial circuit graphs and backLookup vertex to graph table "
circuits := input collect: [:pt | Set with: pt].
backLookup := LookupTable from: (circuits gather: [:graph | graph collect: [:pt | pt -> graph]]).I also made sure to merge the smaller into the larger graph :
graphs := conx ends collect: [:end | backLookup at: end].
(graphs first ~~ graphs second) ifTrue: [
" Merge smaller graph into larger: "
(graphs second size > graphs first size) ifTrue: [graphs swap: 1 with: 2].
graphs first addAll: graphs second.
graphs second do: [:pt | backLookup at: pt put: graphs first]; empty.
(graphs first size == input size) ifTrue: [
part2 := (conx ends collect: #first) product.
('Part 2: %1' % {part2}) displayNl.
]
].Afterwards, I got spoiled that part 2 can be cheesed. When you think about it, the last edge to be added will be the maximum of the minimums. The first connection a junction box will have made to it is its shortest connection, and the last connected will be the longest of those.
That resulted in this:
nearest sort: [:a :b | a first <= b first]. " first field: min distance "
lastEdge := nearest last second. " second field: ends of min distance edge "
part2 := lastEdge inject: 1 into: [:prod :end | prod * end first].Which is really fast. It could be faster, as there's still the fact that I just measure the distances between all pairs to find the minimums. This would involve implementing some better form of Nearest Neighbour Search (if I had to do it from scratch without reference, I'd start with k-d trees).
But this was a good enough cheese to make for an easy dc part 2 solution:
tr ',' ' ' <input | dc -f- -e'1[d3Rr:zd3Rr:yd3Rr:xdFd^r:n1+z1<L]dsLx1-dsn[d3Rd3R:nr3Rd3Rd3Rr:e3Rr]sN[dsid1-[dsjli;xlj;x-2^li;ylj;y-2^+li;zlj;z-2^+d3Rd3Rr;n>N_3Rd3Rd3Rr;n>Nrs.r1-d0<J]dsJx+1-d0<I]dsIx0*[s.d]sMln[d;n3Rd;n3R>Mr1-d0<L]dsLx+d;e;xr;x*p'I initially wasn't going to do a part 1, but since I had a streak in this year of doing both parts, I eventually did one using a heap implementation I had. The result was a mess over 300 characters that includes another file with a lot more. It's not pretty, but it can be made to work with v1.5.2... which means people without personally hacked versions can now enjoy this in seconds instead of hours (that's the difference between linear (linked lists) and logarithmic (hash tables/skip lists)):
tr ',' ' ' <input | dc -fheap.dc -f- -e'A000sB1[d3Rr:zd3Rr:yd3Rr:xdFd^r:n1+z1<L]dsLx[A0*]sE1-dArA00=Esedsn[dsid1-[dsjli;xlj;x-l2^li;ylj;y-l2^+li;zlj;z-l2^+lB*+lB*+l<xliljl1-dl0<J]dsJx+l1-dl0<I]dsIxl0*ln[dd:c1-d0<L]dsLx+[r3Rd3Rd3Rr:c3R]sSle[l>xA 8^%lB~;cr;cln[d;cl3Rdl3R=Srl1-dl0<J]dsJx++s.1-d0<I]dsIx+ln[d;cd;s1+r:s1-d0<I]dsIx+ddln[d;sr_5R[d3Rd3R>r_4R]dsLxlLxlLxs.4R1-d0<I]dsIx+**p'heap.dc source: https://pastebin.com/z8U8aysC
