13
votes
Day 3: Lobby
Today's problem description: https://adventofcode.com/2025/day/3
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>
Obvious dynamic programming problem for today. You only need to keep track of maximum joltage you can get using 1 to n previous batteries and when adding one more battery, you update the max of size k using if you can get a better value from using the max of size k-1 and the current battery.
It's my cleanest implementation so far, but that's mostly because it's just so short.
Part 1 and 2 (Rust)
Daaang, that's nice. I thought what I had was fast at 158 µs, but for part 2 your dynamic programming approach is ~1.7x faster at 90 µs, and it gets the part 1 solution for free to boot. If I had to guess, I'd bet your complete lack of any branching is what gives it that boost, as I ended up with an unfortunate branch I couldn't figure out how to eliminate.
I checked how my implementation complies and it's indeed what I expected: the inner loop is fully unrolled and each iteration is avoiding any branching by using a conditional move. This means there is no risk of (failed) speculative execution and the iterations can be efficiently pipelined inside the CPU. Even the vector of 13 elements ends up being mostly stored in registers, with only a few positions being on the stack because there aren't enough registers. I guess the main point is that my version compiles into something that the CPU can execute extremely efficiently, which makes up for the fact that it might require more operations.
If the accumulators could be u8 instead of u64, the compiler could even use vectorized instructions to run many iterations at the same time since there is a vectorized max instruction to replace the conditional moves.
Elixir
I'm including my original code for part 1 even though my part 2 solution can be used for both. Because I'm proud of my inscrutable recursive function 🥰
My general
max_joltage/2function implements what @Hvv describes better than I could in their "But can we do it faster?" box.edit: Actually I think I went only halfway with the optimizations. I didn't do as much preprocessing as I could have! (And didn't keep the input rows as strings, and didn't do one of the skip-ahead optimizations)
Parse input
Note:
?<unicode codepoint>is a funky bit of built-in syntax that gives the integer value of a unicode codepoint. For basic ascii characters, it's equivalent to C's'<character>'syntax. For example,?a == 97.When the puzzle input string is a bunch of digit characters that need to be parsed into a list of integers,
digit_char - ?0is a little shortcut for parsing each one.Original part 1 solution
General solution for both parts
Benchmarks
(Using the general solution--which for part 1 is a few hundred μs slower than the bespoke solution)
Top of the leaderboard baby! (I know it will not last but lemme enjoy it!!) It's always nice when my intuitive solution turns out to be (1) correct and (2) about as fast as you can get. I guess I'm getting better at these now that I've done them for a couple years.
Thoughts
Another relatively straightforward problem today. For part 1, with only 2 digits, it's a simple check to find the biggest digit (as long as it's not the last one) and pair it with the biggest digit to its right.
For part 2, it's simply a matter of expanding that to a more general solution. I chose to write it recursively—to find the biggest 12-digit number, find the biggest digit (as long as it's not in the 11-rightmost ones); then take the rest of the digits and find the biggest 11-digit number. Repeat until you're down to finding the biggest one-digit number.
There are a couple micro-optimizations I found that probably didn't change much (my solution already ran in under 1ms):
C# code
Got stuck on this last night (probably was just too tired), so I had to come back to it today. Luckily, it didn't take me too long after that. For part 1 I was originally building each individual combination, which ended up being way too slow for part 2, so I scrapped my old code and came up with a new solution, which runs pretty much instantly on my machine.
Explanation
I eventually realized that caring about combinations at all was the wrong approach. As an example, let's say we're trying to find the maximum joltage with three batteries from the bank
482651:4826is8.2651is6.51is5.So, the maximum joltage is
865. I'm not sure what the time complexity of this is, but it seems pretty efficient.Probably others arrived at something like this too, though I haven't looked at many other solutions yet.
Solution (Lily)
Yep, looks like a few of us arrived at that faster approach. Actually, my mind instantly went to that approach when I read the puzzle... it never would have occurred to me to try what, apparently, a bunch of folks reached for first! Funny how people are wired differently like that.
Of course it's only a matter of time before I end up spinning my wheels on a bad strategy for something everybody else thinks is a breeze, lol.
My friend if you're reading through other people's solutions on Tildes you're already in the trenches. Might as well just commit, lol.
The steam deck is a perfectly cromulent little laptop, I use Blender on the thing for making game mods. Admittedly, you kind of do need a keyboard.
Part 1
Since there are only two digits to search, it is absolutely possible to just check all 2-digit combinations in each string and just take the maximum to reach the answer.Part 2
Okay so we can't just take the maximum over all 12 digit combinations (at least if we want to have the program run within a reasonable amount of time, though I think a sufficiently optimized brute force wouldn't take our entire lifetime?).The first simple enough solution I could come up with was to maintain an array that keeps track of the largest K-digit number that can be formed from the given digits, from 0 to 12 digits (yes, the 0th element is always 0 but I will eat a few bytes of wasted memory to avoid weird off-by-one situations). This array starts with all 0s, but we go left to right and analyzing each new digit of the digit string to update our array, ending with an array with the true answer in the 12th element.
When we look at each new digit, we can update our array by seeing that each largest K-digit number either doesn't use the rightmost digit or it does use this rightmost digit.
If it doesn't, then the answer is unchanged, since we already knew the largest K-digit number without the rightmost digit.
If it does, then we consider what the first K-1 digits would look like, and realize that this itself is a number, in fact that it's the largest K-1 digit number that could be formed without the rightmost digit (what
planningluck! We already have that in our array!)To cover both options, we can simply take the max over these (and take care for the order of operations, since an in-place array update might destroy that "K-1 digit number" information) and repeat for all K and all the way from left to right to get the full answer.
(I will note that the kids call this algorithm strategy Dynamic Programming, and given that it has a fancy name I'm not convinced it's the simplest algorithm.)
Part 1/2 Code (C++)
But can we do it *faster*?
If you do a bit of analysis on the above Part 2 solution, you'll recognize to solve the generalized problem of finding the maximum K-digit subsequence on a string of N digits, using our old friend Big O notation this solution takes O(NK) time. There is, however, a theoretically faster solution which uses a completely different approach.We instead observe that, for numbers with the same digit, numbers with larger first digits are larger, no matter what digits follow (for example 20000 > 19999, despite 199 having lots more 9s).
This gives us a different solution: If we just seek out the biggest leftmost digits and merely ensure that we can fill out the remaining digits, we get another solution that may be even faster:
Starting from the very left of the digit string, move forwards to the first 9 and see if there are enough remaining digits to build out the rest of the number (this is actually just an string index check, since you can always fill out a number by just taking all the digits after it). If it works, take it and continue building the number after the 9. If the 9 doesn't work (or there are no 9s in the string), try the next 8, then the next 7, etc. (you're not going to exhaust all the digits unless you're trying to build a number with more than N digits, which is easy to check).
Repeating this enough times, we will eventually end up with enough digits for a number.
But is it faster?
At first it doesn't look like it's faster, because we have to seek out the next digit each time, and that would make it O(NK) like before. But we can make an optimization that saves a ton of time on those seeks: just pre-compute for each position where the next occurrence of each digit is, and those seeks become O(1) array lookups.
Well that's a new problem: how do we pre-compute at each position where the next occurrence of each digit is?
The secret is to build this pre-computed array from right-to-left. Now the next occurrence of each digit (left-to-right) is just the most recently seen occurrence of each digit (right-to-left), and we can just update each value right-to-left as we see it.
This precomputation takes O(10 N) = O(N) time, and now that we only need to do an array lookup to build our number of K digits, the full solution takes O(10 N+10 K) = O(N+K) = O(N) time (remembering that K <= N in any reasonable version of this problem), which is definitely the fastest we can go.
Bonus problem: What if the base isn't 10? Given a N-digit string in base M, this solution will take O(NM) time. Can you find an even faster solution that covers this case? (I think there is one, but the solution that comes to me is exponentially more horrible and contains some super weird ideas).
Bit of a better day, as I guessed Part 2 so was already planning out the algorithm. My first thought was a DP solution (which napkin-math suggests might have worked?), but figured out a better approach instead. Went back and refactored to add a couple new utilities and make the main solver tail-recursive.
I got spooked at first thinking it might be 2D arrays and realized I'll need to read up to figure out how best to handle that, as it's sure to be coming in the next few days.
Prolog Solution
Rust
Part 1 - After day 2 nerd-sniped the hell out of me I was braced for something crazy, but this puzzle solved easily with a simple byte-by-byte traversal.
Part 2 - I was pleasantly surprised that making my part 1 solution generic to an arbitrary number of batteries was very straightforward, and the performance impact for the extra math needed to set up the inner loop is almost negligible.
I was able to solve days 1 and 2, but I wasn't satisfied at all with the quality of my solutions. Today's puzzle I think kind of forces you to come up with a relatively efficient solution, and I'm happy enough with it to post it. I'm just a hobby programmer and I haven't done a ton of leetcode or previous years of Advent of Code, so for a lot of these problems, I lack prebuilt intuitions on how to solve them (particularly the more generalized part 2 problems).
Solution (Crystal)
Took a lot of time for this one. First part took me a long time to debug because I did not know strings extracted from a .txt file ends with a newline, and second part is the same problem, for which I learned rstrip exists to use rather than assuming and use string[:-1].
Part 1
Part 2
I got the gist of the logic: basically the only valid N string in a N-input is the string itself. Appending a new digit to the string, we can compare suffixes until there exist a new suffix that can replace our current suffix, and we use the new suffix to propagate the change down. The intuitive way to think of this I guess would be "if I take this new digit, what is the leftmost digit I can remove?"Not too bad today. Happier with my solution than I have been for the previous days.
I did have one weird thing that I couldn't make work, and maybe someone more familiar with Python can tell me if it's possible. I'm moving a "pointer" demarking the end of a slice. I had originally tried iterating it from -11 through to 0, but a slice of a python
list[:-1]is very different thanlist[:0]. Is there a good way to do this? I ended up just iterating overlist[:len(list) - (11-n)], which works fine, but seems rather inelegant.Part 1 & 2 (Python)
I'm hoping someone has an actually clean fix, as I've run into that in the past without much luck.
Couple of ideas:
list[n:]works fine.list[:n] if n else listI'll be honest I just couldn't wrap my head around part two for some reason. I ended up doing the shameful thing and took a look at what @Berdes had. It was really well done, and very concise for both parts. I ended up adapting some of your code into mine for the second part and left the first one in my mangled spaghetti. I'm also amazed at how concise yours and @zkxs 's are. One day I'll get there, but kudo's to you both!
Rust Parts 1 and 2
I did have a working solution with the test numbers provided but when we went from 15 digits to 100, well... exponential growth hurts and brute forcing it was not cutting it.
TypeScript
Pretty straightforward today. Initially I wrote my Part 1 solution with two sequential loops (get the first digit, then get the second digit). When I got to Part 2, I realized I could just refactor that into a single wrapper loop that iterates n times depending on how many digits are needed. So my solution below uses the same generic solver function, just passing it
2for Part 1, and12for Part 2.Parts 1 and 2
Part 1 completes in 3ms on my machine; Part 2 completes in 3ms also.
Python
I almost gave up. I did not write my part 1 to be generalized for the second part, but when I figured out how to generalize it, I was able to keep the same efficiency as part 1. On my machine, my benchmark times (thank you
hyperfine) were 8.4ms and 9.4ms respectively.Part 1
Part 2
Well, I thought my solution was pretty acceptable until I read some others here. My strategy was to iterate 12 times across the string slices that were after the last chosen digit and before the required remainder.
I also feel like I am using strings incorrectly here, but the compiler wanted nothing to do with me tonight.
Part 2
Rust's strings take a little getting used to. It's mostly because they're UTF-8 aware, meaning a char can be more than one byte long, and also that not all byte sequences are valid Strings.
In a context like Advent of Code where all the inputs are normal ASCII text this UTF-8 support is a bit overkill, which might explain your feeling that things aren't quite right.
AoC seems like a great way to get used to Rust's quirks though, I hope you have fun and learn some new tricks! Definitely don't let other people's fancy solutions wig you out--we're all here for different reasons and with different backgrounds. (Do let the guy who's doing it all in Excel wig you out though, that's crazy)
I think my confusion comes down to the borrow checker. I don't fully understand why I had to(?) make a copy of line into temp_line when I wasn't manipulating the line, just taking length and slices.
When you do
let temp_line = line.expect("")you aren't copying the line--you're unwrapping theResult<&str, Error>to get to the&strinside.Unlike most languages where assignment does a copy-flavored thing, in Rust it does a move-flavored thing. So that string isn't getting copied! That's why the compiler isn't letting you use
lineanymore after that point--it's gone, because you moved it intotemp_line.I see.
I am going to need to play with the language a bit more to make that intuitive!
What a great puzzle for Day 03! It makes for such a satisfying solution.
The key insight for me from Part 01 was that the first digit would never be the end digit, and the second digit would always be to the right of the first digit. So the first digit is the
max(bank[start:end-1])and the second digit is themax(bank[first_index+1:end]).That generalized pretty naturally to any number of digits in Part 02 with this solution:
Part N