🪐 - 2024 DAY 11 SOLUTIONS -🪐
Day 11: Plutonian Pebbles
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
Zee
Zee is my Dutch dialect of C. Since Dutch has compound words, so does Zee: "const char **" becomes vasteletterverwijzingsverwijzing, not vaste letter verwijzing verwijzing, which would be incorrect. A pointer to a long long unsigned int is, obviously, a zeergrootnatuurlijkgetalverwijzing.
::: spoiler Code
:::
And of course we don't have a Makefile but a Maakbestand:
Yet more proof that the dutch are mad :D
Is it your own esolang, or is it commonly used by dutch speakers?
No it's just me messing about with macros (but it does work!)
I do want to explore the type naming rules, see if I can write a parser for it. The C rules are funky by themselves but this is another level. "vaste letterverwijzing" is "char * const" but "vasteletterverwijzing" (without the space) is "const char *". Then there's grammatical gender: "vast getal" (const int) but "vaste letter" (const char)
Uiua
I thought this was going to be trivial to implement in Uiua, but I managed to blow the stack, meaning I had to set an environment variable in order to get it to run. That doesn't work in Uiua Pad, so for any counts larger than 15 you need to run it locally. Built-in memoisation though so that's nice.
Haskell
And now we get into the days where caching really is king. My first attempt didn't go so well, I tried to handle the full list result as one cache step, instead of individually caching the result of calculating each stone per step.
I think my original attempt is still calculating at home, but I finished up this much better version on the trip to work.
All hail public transport.
::: spoiler C#
:::
Python
Part 1: ~2 milliseconds, Part 2: ~32 milliseconds, Total Time: ~32 milliseconds
You end up doing part 1 at the same time as part 2 but because of how Advent of Code works, you need to rerun the code after part 1 is solved. so Part 2 is technically total time.
:::spoiler Fast Code
:::
Stepping through this code is what made it click for me, thanks. I was really mentally locked in on "memoizing" of the transform function, instead of realizing that the transform function only needs to be applied once per stone value.
Yours is still a lot faster than my rust version, so i'll have to work out what is happening there.
Learning to profile code gives you a chance to learn what is inefficient code! I definitely like to spend sometime looking at it but at the end of the day. I really need to learn more languages. for now, I am sticking with trusty python. image bellow is in microseconds.
:::spoiler screenshots
if the python process was kept alive, then we only are saving 25 milliseconds from ~250 to ~235! However, in real world testing, it seems that the profiler is not really a proper enough test! likely because the profiler is adding some overhead to each line of code.
notice here, if I add this line of code:
Notice I load from file a transform_cache from a previous run. However because of the "if stone in transform_cache" check, the loaded transform_cache is for some reason slower than allowing it to be filled up again. however, loading it and clearing it, is faster because the cpu/ram/OS is probably doing their own caching, too. if we remove the "if stone in transform_cache" check and keep the transform_cache fully loaded, then it is faster by ~1 millisecond, down to 29 milliseconds! these are the niche problems with caching things and squeezing all the performance out of code.
:::
Yeah, I have been using these slow challenges to improve my profiling ability. It is a bit of a dark art though, especially with compiled languages.
My slowest part seems to be the hashmap, but there isnt much I can do about that I think.
Also, if I do a release run I can get 10ms, but that feels like cheating :D
Hey that is what release mode is for! optimizing by unrolling and other tricks is needed, but in your case, I think I remember someone mention their rust release mode is closer to 2 ms.
I did try something like PyPy3 but it seems to be slower by ~3 milliseconds! So I don't know where I could improve my code any further without unrolling the range(75) loop. though would only make it closer to ~29 ms on avg.
Edit: apparently they updated their code to be closer to 250 micro seconds. that is blazing fast [ link ]
Using release to beat python code is just a very hollow victory :D
It does somewhat depend on how we measure as well, you are benching the algorithm itself, and I'm doing the entire execution time
you are right, I dont think loading the file from disk should be part of it because OS process priority/queue, disk and cache and more headaches on figuring out what is slow. If you want to compare the entire execution including python startup overhead and reading from file and anything extra. it is closer 50 to 60 ms on linux and 80-90 ms on windows. (both hosts, not virtual machines)
My reasoning is that loading the input will eventually either pull from the website or disk. that is not part of the challenge. you could simply just hard code it.
So maybe you should look into adding code to your debug mode or both modes for measuring solving it instead of the entire process loading.
however, someone claims their rust code can do 250 microseconds, so I doubt you have much excuse aside from having "inefficient" code.(you are still fast, just not at the limit of your Language's performance) only measuring my python algorithm, it is only able to finish in 32000 microseconds.
https://github.com/maneatingape/advent-of-code-rust/blob/main/src/year2024/day11.rs
however, now that I am looking at their
main.rsfile, they do calculate time for completion after process startup and only the algorithm.Yeah, disk loading definitely shouldn't count if I was timing properly, I'm just lazy and dont want to do proper timing. :D
Most of my slowdown is in the hashmap, looks like that answer deconstructs the hashmap and builds it from a fastmap and a vec. Not sure I want to go down that road, at this stage.
Thanks again for your code and help :)
Rust
Part 2 is solved with recursion and a cache, which is indexed by stone numbers and remaining rounds and maps to the previously calculated expansion size. In my case, the cache only grew to 139320 entries, which is quite reasonable given the size of the result.
::: spoiler Solution
:::
Also on github
Dart
I really wish Dart had memoising built in. Maybe the new macro feature will allow this to happen, but in the meantime, here's my hand-rolled solution.
Nim
Runtime: 30-40 ms
I'm not very experienced with recursion and memoization, so this took me quite a while.
Edit: slightly better version
Codeberg repo
I had a very similar take on this problem, but I was not caching the results of a blink for a single stone, like youre doing with
subtree_pointers. I tried adding that to my solution, but it didn't make an appreciable difference. I think that caching the lengths is really the only thing that matters.C#
Haskell
Sometimes I want something mutable, this one takes 0.3s, profiling tells me 30% of my time is spent creating new objects. :/
Some nice monadic code patterns going on there, passing the cache around! (You might want to look into the State monad if you haven't come across it before)
Thank you for the hint, I wouldn't have recognized it because I haven't yet looked into it, I might try it this afternoon if I find the time, I could probably put both the Cache and the current stone count into the monad state?
Your code as it stands is basically
State BlinkCachewritten out explicitly, which is I think a natural way to structure the solution. That is, the cache is the state, and the stone count is the (monadic) return value. Good luck!Python
I initially cached the calculate_next function but honestly number of unique numbers don't grow that much (couple thousands) so I did not feel a difference when I removed the cache. Using a dict just blazes through the problem.
Haskell
Yay, mutation! Went down the route of caching the expanded lists of stones at first. Oops.
Does the IORef go upwards the recursion tree? If you modify the IORef at some depth of 15, does the calling function also receive the update, is there also a Non-IO-Ref?
The IORef is like a mutable box you can stick things in, so
readIORefreturns whatever was last put in it (in this case usingmodifyIORef'). "last" makes sense here because operations are sequenced thanks to the IO monad, so yes: values get carried back up the tree to the caller. There's alsoSTReffor the ST monad, or I could have used the State monad which (kind of) encapsulates a single ref.Kotlin
Gone mathematical.
Also overflowing indices screwed with me a bit.
::: spoiler Here's the code:
:::
Try it out here.
And this is the full repo.
C
Started out a bit sad that this problem really seemed to call for hash tables - either for storing counts for an iterative approach, or to memoize a recursive one.
Worried that the iterative approach would have me doing problematic O(n^2) array scans I went with recursion and a plan to memoize only the first N integers in a flat array, expecting low integers to be much more frequent (and dense) than higher ones.
After making an embarrassing amount of mistakes it worked out beautifully with N=1m (testing revealed that to be about optimal). Also applied some tail recursion shortcuts where possible.
::: spoiler Code
:::
https://github.com/sjmulder/aoc/blob/master/2024/c/day11.c