7
votes
Day 5: Cafeteria
Today's problem description: https://adventofcode.com/2025/day/5
Please post your solutions in your own top-level comment. Here's a template you can copy-paste into your comment to format it nicely, with the code collapsed by default inside an expandable section with syntax highlighting (you can replace python with any of the "short names" listed in this page of supported languages):
<details>
<summary>Part 1</summary>
```python
Your code here.
```
</details>
Elixir
Both parts
I figured it would help to merge overlapping ranges for part 1, and it turned out to be necessary for part 2.I went a little overboard trying to squeeze stuff onto single lines, my code was more legible earlier. Ah well.
Benchmarks
Very similar. Even though I banged my head against a silly error in the range consolidation, I'm a little proud I have you very slightly beat on this one!
Code
Benchmarks
Also, is it all just tuples, arrays, maps and such usually in Elixir?
Coming from OOP, I feel a little... type-starved :D
But it would've been a good idea to split out the Range-stuff as you did.
Oh wow, I did not realize
in(which is just a macro that compiles to anEnum.member?/2call, I believe) had that much overhead for ranges!If I change the anonymous fn passed to
Enum.any?/2in my part 1 solution to this:it has the exact same performance as yours.
Seems like the generalized logic for ranges with steps other than 1 slows things down a lot. (There's also some overhead from dispatching via the
Enumerableprotocol.)I wonder why they don't just add a separate clause with the faster logic for the most commonly used step size of 1.
Re: limited type options, OOP concept relations
Yeah, the main composite types available in elixir are much simpler than what you can cook up in most OOP languages. There are also structs, but they're basically just maps with well-defined keys.
The limited inventory of core types is partly due to how deeply pattern matching is baked into the language and even the BEAM VM. Simple types allow for powerful, fast pattern matching.
Also related: a big part of Elixir and the larger OTP ecosystem, which you unfortunately won't get much practice with on AoC puzzles, is stateful message-passing processes like
Agents andGenServers. These are conceptually analogous to objects: they bundle some well-defined data structure with functions that act on it and a public/private interface. The big difference is that they behave more like mini-servers internal to your application, running receive-act-respond loops, with strong concurrency guarantees.Strange indeed, the more you know!
Re: Re: ... types ...: Yeah I think for puzzles like this, that's actually pretty fine.
Indeed, it's probably much more convenient than usual OOP type-handling. I'm really enjoying the multiple function definitions to replace if-statement / pattern matching tbh. (Although as a beginner I've already encountered some weirdness around the restrictions of what can be / can not be used as a guard.)
I'm wondering how this would pan out in large applications, where (in my experience) it's very handy to restrict what kind of operations you can do on certain data to prevent "illegal" state. In the same vein, I do wonder how I'd deal with the dynamic typing in that kind of environment once you tackle some larger refactoring. (You see, I've only professionally used static languages)
When I became interested in a BEAM-language, I first looked at Gleam as a static language, but Elixir simply seemed more mature.
I foolishly didn't look at the input until I'd written a solution to Part 1, then when I saw how large the numbers were, I figured "eh, I'll run it and see what happens". Apparently, using up all my memory and crashing my computer is what happens.
And Part 2 had so many off-by-one errors. This is the first time I've had to submit an answer more than twice, and one of my submissions was only 16 away from the correct answer.
Part 1&2 - Python
I had almost the exact same experience! My first approach was to create a
setof all fresh inventory IDs, which promptly consumed all RAM and froze XFCE. I had to switch to a TTY and kill it in htop before it took out my swap too. Then struggled with so many off-by-one errors I wrote a customRangetype to make it easier to get right.Another similar experience here.
Also tried with a set, but I killed the process before anything bad could happen.
Then I tried to be "clever" and thought it might somehow take less memory if I packed it all into bit-mask where 1 indicated presence and 0 indicated non-presence. Turns out, the Erlang VM does not like arbitrarily large bit-shifts.
...and I also struggled with a very basic error because of a case that wasn't handled in the demonstration data, but was in my input.
Very much needed a good day after yesterday frustrated me so much. It just seemed like yesterday everything I tried with the language went wrong, and today it fell together nicely.
Nothing creative on the algorithmic front; I suppose there may be some efficiency gains to be had on both parts by keeping ranges sorted, but the list was short enough it didn't matter.
Prolog Solution
I tried reading up on difference lists and DCGs this afternoon and did not get anywhere with it. I'll have to give it another go with a clearer head.
I keep expecting the problems to suddenly get harder, but I guess we haven't reached that point yet. Surprisingly easy day, though part 2 was pretty interesting. I'm not sure what I'm doing here is the most efficient way to do it (nor am I entirely convinced it does the job in all cases, though it works fine for my input), but it was the first thing I could think of.
(Updated to use Lily's
Listmethods instead of manual loops. It's almost certainly no more efficient than what I had before, but I thought it might be a better showcase of some of Lily's features.)Solution (Lily)
For part 1, I directly implemented some logic to sort the ranges and merge the overlapping ones. The idea was to be able to binary search the ranges to see if an ID is part of one of the ranges. In the end, the binary search idea was barely faster than a linear scan using the sorted+merged ranges. Maybe an approach based on a binary tree would have been faster?
This approach made part 2 extremely trivial since I already had a list of ranges without any overlap.
Part 1 and 2 (Rust)
So, I managed to implement two solutions using BTreeMap, which required to use the nightly build to get access to the Cursor API.
The first approach was to use the key to represent a bound and the value to represent whether it's a starting point or an ending point. When trying to find whether an ID is part of a range, I can query for the largest key that is smaller or equal to the ID. If it corresponds to a starting point, I know my ID is inside a range. One detail with this approach is that I needed to store the ending points with a value + 1 (i.e. the ending point is excluded from the range), otherwise ranges like [5, 5] would collide on the key 5. The side benefit of having excluded ending points is that ranges that are next to each other would still get merged: [3, 5] [6, 8] would become [3, 8].
Using start and end points as keys
The second approach (likely more sane) was to have the key as start point and the value as end point. It's basically the same as having a sorted vector of (start, end) points, except that it's not necessary to pre-sort the list and removing an element in the middle should be cheap (amortized O(1) I think?). Similarly to the previous approach, finding whether an ID is in one of the ranges requires querying for the largest key that is smaller or equal to the ID and checking whether the ID is less or equal to the end point.
Start points as key, end points as values
The second approach was (unsurprisingly) faster than the first one. Sadly, both were slower than the vector-based approaches I used previously. I don't know if this is just the vector based approaches being inherently faster because of the memory layout, amount of branching and data dependency between instructions, or if it's just the BTreeMap implementation not being super well optimized. The Cursor API is still in nightly, so I wouldn't be surprised if the implementation was a bit sub-optimal.
Runtime comparison of various approaches
Not really the best benchmark since I only run each thing once with no warmup. I ran the program a couple of times to get a sense of which runtime was normal and which one was just an outlier (I guess due to the computer doing other stuff in the background). The parsing of the input is excluded.
Ooh first day where the simplest naive solution ran out of memory ^.^
For Part 01, doing a linear search of the ranges worked without issue and is still very straightforward.
Part 02 gave me a bear of a time with the range overlap checks and off-by-one errors. I eventually created a custom type for the ranges that factors out the calculations, making it easier to get right. Once I had that, sorting, merging, and summing the ranges was easy.
I'm not super pleased with my range merging generator. It's clean enough, but I feel like there should be a more concise way to say "take each range and merge it with the previous if they overlap" that doesn't require nested conditionals. Let me know if you've got an
itertools-based approach or something that you think is better!Part 1
Part 2
I was curious myself whether there was a way to do the merging without sorting the list of ranges first. Your logic looks pretty close to my own, though, so maybe that is the best way to do it...? Performance seems fine in any case, so it probably doesn't matter much anyway.
I think it probably is. Making the null range have a length of zero lets me eliminate one of the conditionals, since yielding it won't affect the sum. But I think sorting + overlap check is pretty much required.
Definitely no performance issues though! Both parts run in ~0.2s total.
Running in 337.65µs on my machine, first sub-1ms entry so far this year!
Order the ranges by the lower number. If the higher number in the range is larger than the next low number, merge the ranges.
Part 2
TypeScript
Seems like the range sort/merge approach is probably the only sane way to tackle this one. Once I cleared that mental block it was smooth sailing, and Part 2 ended up being faster and simpler than Part 1 (a welcome change)!
I did have some fun modifying the array of ranges in-place with
Array.prototype.splice()while looping through it, carefully managing the current index to recompare merged ranges after each mutation. Some judicial use ofcontinueandbreakin the Part 1 nested loops helps avoid unnecessary iterations.Parts 1 and 2
On my machine the median input formatting time is 115µs, Part 1 is 225µs, and Part 2 is 12µs. This is my first solution since improving my benchmark methodology earlier today. Eagle eyes may notice slightly different scaffolding syntax in today's code, to support that.
It made me really happy to see <1ms times for all three parts. These accurate benchmarks make me feel more like a real contender after all, even as a JS goof trying to keep up with you hardcore Rust gurus and your ilk. Turns out, JS can be pretty quick too!