π€Ά - 2025 DAY 8 SOLUTIONS - π€Ά
Day 8: Playground
Megathread guidelines
- Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
- You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL
FAQ
- What is this?: Here is a post with a large amount of details: https://programming.dev/post/6637268
- Where do I participate?: https://adventofcode.com/
- Is there a leaderboard for the community?: We have a programming.dev leaderboard with the info on how to join in this post: https://programming.dev/post/6631465
Nim
::: spoiler view code
:::
Runtime: 364 ms
Part 1:
I compute all pairs of Euclidean distances between 3D points, sort them, then connect points into circuits, using, what I think is called a UnionβFind algorithm (circuits grow or merge). After exactly 1000 connections (including redundant ones), I take the three largest circuits and multiply their sizes.
Part 2:
While iterating through the sorted connections, I also calculate the product of each pair xβcoordinates. The last product is a result for part 2.
Problems I encountered while doing this puzzle:
ref objects stored in a HashTable (~ 40 min)Time to solve Part 1: 1 hour 56 minutes
Time to solve Part 2: 4 minutes
Full solution at Codeberg: solution.nim
Upvote for mentioning union-find. It's the little algorithm that could, and almost nobody has heard about it.
Uiua
Just a messy part1 so far. Plus a moment of pure rage when I realised the trap. (I have no idea what algorithm Lemmy uses to highlight Uiua code, but it's always a surprise.)
(edit: part 2 now added. I had a stupid error earlier. Takes 12s native or 20+s in the pad.)
::: spoiler Stupid error I added elements just until the set first hit size 1, rather than continuing until total number of elements equalled number of points. :::
Run it here(for what it's worth)
Re. what connections to process or not? That seems to be like one of those that's either completely obvious when you implement your solution one way, and a nasty pitfall when you do it another. In this case, pre-computing the list of pairs to process vs. finding the next one when you need it.
::: spoiler spoiler Stupider than that: I'd just got halfway through writing a complicated divide and conquer algorithm to calculate nearest pairs when I realised that I could just brute-force it. :-) ::: But yes, precomputing was definitely the way to go.
Kotlin
First tricky puzzle today, instantly leading me to more or less brute force for part2. I took a lot of time to understand which data structures I needed today, and how to compute my matrix.
Runtimes:
part1: 141ms
part2: 45.9 seconds .-.
::: spoiler Solution
:::
full code on Codeberg
That is a very unoptimal pt2 time! Have you tried profiling to see what's causing the slowdown? Mine is 300ms, or worst case, 4s to process all links.
My algorithm is probably pretty horrible, I was purely out to get a solution as fast as possible. I might try improving it after Day12...
That's interesting. I found part 1 to be the hard part and part 2 was just simplifying part 1. They ran in about the same time for me, too.
Part 1 run time:
Part 2 run time:
Code:
Oh wow, I see you went the Kotlin way of hacking together an object with Pairs and Triples. I used to do that too but then it got too confusing lol
It can be confusing, for sure. But I really like Pairs and Triples.
answer = distance.first.first.first * distance.first.second.firstis not very elegant, lol.Futhark
As always, futhark does not support arbitrary inputs, so I have a sed script to transform the input to something readable.
::: spoiler input transformer it produces a textual representation of
[][3]u32, try it on your example or input :]:::
Calculate all the distances (even the redundant ones, I had no idea on how to filter them out). Sort them, keep only the first 1000 for part 1. Keep all for part two. Initialize all boxes to be in no component. Add them to components as time goes on. When connecting two boxes already in a component. Mark all boxes in the second component as part of the first one. Stop when everything is connected.
After improving my implementation of
concatMap(preallocate the entire array), the overall performance improved greatly. My end stats are::: spoiler Program Source
Basic
:::
Haskell
They're getting interesting now!
This is crazy concise and fast! Impressive.
Thank you! βΊοΈ
Python
Both of my solutions make use of a Heap and a Disjoint-Set-Union / Union-Find data structure
Part 1: Use a heap to keep 1000 shortest connections, then union them all in DSU and get the 3 largest connected components.
Part 2: Use a heap to keep all connections, then union the shortest ones until all components are connected.
::: spoiler click to view code
:::
It seems like you forgot the backticks around the code. It's very hard to read this way. Also python comments look like markdown headlines :]
Fixed, thanks!
Haskell
Rust
It's getting spicier, luckily part 2 wasn't really much additional complexity this time. There exists a pretty fancy union-find data structure which would have made representing the subcircuits much faster, but I went with a lazy approach.
View on github
::: spoiler Code
:::
C++
Both parts in 40 ms. As always, the real AoC with C++ is parsing the input, although having a number that changes in the puzzle messes up my test/ solution runners.
Uses Boost's implementations of sets, since they're just plain faster than the STL ones.
::: spoiler Code
:::
I went here for graphs (to find connected components) and binary search (for efficiency)
C
Got stuck for a bit on part 1 on a silly mistake in the group (circuit) merging code, where the nodes from one group are reassigned to the other:
At some point in the loop
pair->b.groupitself is updated, from then on the check is against the new group value. Oops.In the end, my solution's runtime on my (10 year old!) PC is about 160 ms for both parts, which is more than I would like, so maybe I'll look into better set representations.
::: spoiler Code
:::
I think we basically did the same bug, but I did it in rust.
160ms is respectable, I am at 300ms. Probably should remove my sqrt.
Go
God damn it, I thought I would never see the end of part 1. I had a hard time finding a good representation and then I failed at not eliminating valid distances etc. The code is quite messy, but I did it!!!
::: spoiler day08.go
:::
Uiua
Not really proud of this one. Part 1's still ok, just calculated all distances between boxes after realizing it's not that many (499500, a reasonable amount I think, relatively speaking).
The dumbest thing I did this time was manually implementing the function to take the first n rows of an array by using a loop. Only when I was working on part 2 did I realize that I can just use the "take" function Uiua provides. Additionally, I even had some mistake in my reimplementation of it that only caused issues in part 2 somehow.
For anyone interested, it's the commented out line in Pβ below.
Part 2 is just a brute force. For the actual input, I searched manually until I got to the pair number just before the actual solution because I didn't want it to run that long and it takes long enough as is.
Run with example input
:::spoiler Code
:::
Kotlin
I'm still catching up and this one was hard for me.
First I experimented with implementing a BVH (boundary volume hierarchy) due to three dimensions and possible dense connections between all junction boxes.
I couldn't get that to work and figured that basically a graph would suffice.
Implementing a simple graph and actually using it, I noticed that I didn't actually need a graph. The graph was rather slow and I had already calculated all the edges. So I just kept the shortest ones for part 1. I didn't even begin with part 2 at that time.
I used sorted sets as a way to keep the shortest connections because I didn't find a heap; only to find out that a
PriorityQueueis a heap. For part 2 I knew I wouldn't actually need all connections, so I kept the shortest 12 per junction box and sorted them by length, shortest ones first.The main idea of part 1 is to build all connections and keeping the shortest one in a heap. That way longer connections can be replaced easily and the longest one is readily available at the root of the heap.
For part 2 instead of building and using all the connections from shortest to longest, this solution keeps only a small number of shortest connections per source junction box. This way the sorting overhead is minimized whilst still solving the solution.
Building the circuits is something else. In order to preserve performance, all junction boxes and their unmerged circuits are represented by a circuit ID in an array. Merging is done by replacing the target/second circuit ID by the first one.
For part 1 this continues until all connections are made. Then the frequencies, bounded to the amount of junction boxes/circuits, are calculated and sorted from largest to smallest. This gives the necessary circuit sizes.
For part 2 instead of merging until all connections are exhausted, the solution needs to check, whether there are any junction boxes not in the merged circuit. Once all junction boxes are in the same circuit, return the last connection made.
A lot of the code requires the input to be consistent and being able to solve the puzzle but that is given.
Code with comments on GitHub ::: spoiler Code (yes, it's long)
:::
Unoptimized: 27ms and 32ms
Optimized (100 runs): 4ms and 6.5ms
Rust
Not an easy one, mostly because I messed up my merge circuits function (Pop/remove first circuit, load into second, but the pop/remove messed up the indexes, so I was merging the wrong thing).
After getting that it was all good. Pt2 was trivial once pt1 was solved.
::: spoiler spoiler
:::
Rust
Part 1 took a decent while, partly life getting in the way, partly as I was struggling with some Rust things like floats not being sortable without mucking around, plus some weird bugs trying to get
collectto do everything that I eventually just rewrote to avoid.I found the noisy_float crate which let me wrap f64 as a "real" r64 which meant no NaN which meant Ord which meant
sort_by_cached_key()and the rest worked.I'd planned how to partition the closest neighbour search, but the whole thing runs in 24ms so I didn't bother.
::: spoiler other types
:::
(Browser-based) Javascript
My initial approach was to consider each junction as a circuit, and then merge the closest circuits. Took me way to long to realize the problem statement for part 1 made this unworkable (as you need to count redundant connections as "attempts"). Thankfully part 2 doesn't care about how many connections you make, so I was able to reuse that code to solve part 2.
To solve part 1 "the right way", I first thought I had to store a circuit as a collection of pairs of junctions (literally, the collection of connections). Oh god was that messy code; 3 layers of nested for-loops and it still wasn't getting close to the answer. I eventually realized I could reference the junctions' indices in the input to simplify things, allowing me to manipulate simple sets of ints. Combine with pre-calculating the distances once before starting the connecting/merging and you end up with a surprisingly simple and snappy algorithm.
Given I realized these optimizations after restarting part 1, my solution for part 2 doesn't take advantage of any of them and takes an eye-watering 22 seconds to terminate on average. It probably re-computes more distances than it computes new ones, for each iteration...
::: spoiler Code
:::