13
votes
Day 5: If You Give A Seed A Fertilizer
Today's problem description: https://adventofcode.com/2023/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>
This one was tricky! I was hoping to make top 1000 for at least one problem, and I wound up getting it on part 1 today.
Apologies in advance for the bad variable names, ha.
My pt2 solution took 8m44s to run, it's not a "full" bruteforce in the context of the problem but it certainly is a bruteforce. I'll have to look at some other solutions tomorrow and see what people have done to get a really well-optimized program.
Part 1
Saving the ranges of each map into a dict then itering over seeds.
Part 2
Working backwards; starting at 1 checks every number to see if, starting from the location, the output is a valid seed.
Part 1 was simple, but completing part 2 was a nightmare because I wasn't willing to brute-force the solution, and the algorithm I had in mind for computing the mappings somehow didn't work on the actual puzzle input for the longest time. This is the fourth time in five problems that I've had code that solved the test input, but not the actual input.
If the difficulty gets even higher, I'll have to abort much sooner than I had hoped. :(
(Rust)
Parser
Part 1 & 2
The code is a bit messy, but I'm too exhausted to clean it up right now. Time to go to bed.
Just out of curiousity, have you looked at why the initial solution didn't solve the input? I'm assuming it was some kind of bug not triggered by the test case, but introspecting a little might help you improve your process.
FWIW if something is complicated, I will write unit tests on as many components as can to avoid bugs. Probably I write too many (for speed), but it usually works on the first try in the end.
Perhaps. I did have quite a few unit tests based on the test input or my understanding of the problem, for example, to make sure that the ranges are split properly, etc. But as I said, they were green.
Potential spoiler
My best guess is that there was a bug related to not properly skipping maps that were already used in the conversion in some instances. E.g. mapping to soil, and then mistakenly using a different row of the soil map again instead of going to the next one. In the end I got desperate and deleted everything, wrote everything from scratch again, and that worked.
The biggest problem with my process was that I felt immense time pressure where there should have been none -- I wasn't even intentionally trying to be fast, because there's little point when you start many hours after the puzzle opens, but somehow I felt under pressure the entire time, which meant that a lot of the good practices I normally have (e.g. somewhat frequent commits) went right out of the window. I also felt very close to a solution for a long time and resorted to tweaking things here and there, hoping they would make it work, which is of course awful and should almost never be done...
This problem was doubly-frustrating, because the algorithm itself was immediately obvious to me, but I got hung up on implementation bugs for literally hours...
It's fun to see this here. I've been posting visualizations each day over on /r/adventofcode. If you're stuck on Part 2, today's animation may help.
Here's my Python solution. It takes about 0.06s for Part 2 on my ancient 2009-era desktop. Initially I'd explicitly handled each of the different possible ways the ranges could overlap. But after sleeping on it, I realized that if I represent the ranges as half-open intervals then I could just sort the four end points and pair-wise shingle them to get subranges that cover the parent ranges without cross the bounds. That got my solution down to 27 lines here.
Part 1 and 2
That visualization is really nice. I especially like the way the animation progressively discloses the operation.
Not gonna lie, that Visualization doesn't help me at all. Not sure if that's because it's going too fast or just because I'm struggling with this one overall.
Full Solution in Haskell
Looking back over my code, things look a bit overcomplicated, though I'm not sure exactly what I'd change. I only began using a
Range
type in part 2, and probably would have benefited from using that from the beginning, making each mapping consist of a source range and a displacement.One thing I am happy with is my range utilities:
Range Utilities
Still enjoying exploring Haskell! I'm only about 1/3 of the way through the book I've been following as a guide. I doubt I'll finish by the end of AoC, but it already has my recommendation for getting me this far.
This is killing me mostly just because I am having a hard time understanding the actual problem, I sometimes hate how little example they give. I'm hoping someone can help.
In the example they of course run through the
seed-to-soil
map. For seed 79, it says soil number is 81.What I'm a bit confused about is when we get to the
soil-to-fertilizer
map. We don't touch seed 79, so why is the fertilizer number 81 instead of 79? When I'm going through that first map should I basically be "initializing" all the later values (fertilizer, water, light, etc) to that initial soil number? The wording is a bit unclear sometimes and I really wish they would have an idiot like me help with with description/examples.You can think of them as functions.
The two seed-to-soil maps:
Are the maps
f takes seed 98 and makes it soil 50
f takes seed 99 and makes it soil 51
g takes seed 50 and makes it soil 52
g takes seed 51 and makes it soil 53
...
g takes seed 79 and makes it soil 81
...
g takes seed 97 and makes it soil 99
For all seeds that are not "transformed" be these maps, their number stays the same.
For example, none of the seed-to-soil maps transforms seed 42.
So seed 42 transforms to soil 42.
Yeah I understand all that.
The problem comes when we go to the next map.
Sticking with seed 42.
If I understand correctly, the first line of
0 15 37
would mean that for Seed 42 it would have Soil 42, and now Fertilizer value of 27.However, now we've got seed 99. It has a soil of 51, which isn't in that range, so does that mean fertilizer should be set to 51, or 99?
edit: Also, For that
0 15 37
, that's sayingwhen soil is 15, it matches to fertilizer 0
right?I see what you mean and I guess this was the relevant but confusing statement.
The source category for moving from soil to fertilizer is soil and the destination category is then fertilizer. So with your question:
Yes.
Anyway, it was using the source/destination category terms a few paragraphs up but I could understand how one could miss that given how verbose the problem was.
PS: Is it bad for Tildes that I'm quoting parts of the problem here?
Okay cool. Final (hopefully) question. Going to spoiler it just to be safe.
Spoilers
Am I correct that once we get past the first map, it's possible that the value it's referencing might have more than one matching map?
AKA. Looking at
39 0 15
for thesoil-to-fertilizer
map. When we're doing our range we get to a soil value of14 -> 39
Currently we have soil 14 showing up twice in our list. Once for Seed 14 (it didn't have a soil set in the first map), and once for Seed 29 (which was set in the previous map), should both seed fertilizer values be set to 39?
I didn’t think about this constraint while solving it but I think it would be allowed.
Similarly, some fertilizers would not be accessible. Like fertilizer 0 will not be possible to obtain.
Here's my writeup for today's problem: https://guissmo.com/blog/advent-of-code-2023-day-5/
Finally, something interesting! :)
I initially used my part 1 solution to write a naive part 2 solution, which worked fine on the example but failed on the input for obvious reasons. After trying (and failing) to write a proper solution using intervals and ending up with a bazillion intervals all looking wrong, I slept on it before getting a working solution: change the representation of intervals to (start, end, shift) and augment my intervals with sentinels (with a shift of 0) so that the entire 0 to Long.MAX_VALUE space is partitioned by intervals. The former simplified the arithmetic and the latter reduced 6 cases to 2, and then everything worked. 🎉
Kotlin
I was chatting with a friend about part 2, he brute-forced it and I drew a picture explaining how to not brute-force it, here's the picture
Part 1
Part 2
Today's language of the day is my very first language, Visual Basic for Application. With GUI, no less! I can't remember when was the last time I wrote VBA, but surely I didn't know how to use dynamic sized array.
Part 1
RangeMap.cls
It is so slow that I brute forced part 2 while went out to the doctor, and it haven't finished yet. I thought Excel is good at data processing.
For part 2 I suppose if I want to brute force, doing it in Rust would make it really fast. Add Rayon on top and I can put my i9 to work.
Part 2
This takes 25s of wallclock time and 91s CPU time in total. I got another idea on how to brute force it, maybe I'll look into VBA again...
edit: I got another solution in VBA brute force. That only took about 4 hours and a half at 3,200 iterations per second.
Today, my choice of using TypeScript is biting me.
Part 1 was not very difficult. I made the naive mistake of using actual hashmaps in my first implementation, and then I saw the actual puzzle input, and realized I needed to rework it.
Part 1
Part 2 was not a very big change for me, because like many others, I just brute forced this one. I would like to figure out a more efficient solution, but I have to start work soon, so I will leave that for another time (which is probably never)
Part 2
Had I used C++ or Rust, I don't think it would've taken as long to brute force. My machine clocked in 285s of runtime for this method. Oh well. Another day, another two stars.
My solution in Common Lisp.
One of the reasons I like Common Lisp is that it easily facilitates recursion, and one of the reason I like working recursively is that it lets you break problems apart into small pieces and work on the pieces individually. Here's the function that does the mapping seed (or whatever) ranges for part 2:
map-range-on-range
In the function parameters,
ranges
is a list of three-element tuples, where the elements are the start and end of a “x-to-y map” range and the offset from the source to the destination numbers. (This is slightly different from the format presented in the problem, but I find this structure easier to work with.)category-ranges
is a list of two-element tuples, giving the starts and ends of ranges of seed numbers (or other numbers mapped from seeds). For the example, the initial list of seed ranges is “(55, 68), (79, 93)”Notes: All ranges are half-open; they include the start number but exclude the end number. I keep ranges sorted by increasing start numbers for ease of processing.
accumulated-ranges
are the remapped ranges the function has gotten through already. Building that list as a function parameter (1) lets me easily sort it when I get to the end of the source data, and (2) lets the compiler employ tail-call elimination for all the recursive calls so it doesn't build up a long set of stack frames.So the function is mostly a big if-then statement that figures out which case applies, handles that specific case, and then calls itself recursively with new parameters to figure out and handle the next case.
Language: Python:
Part 1
The first part wasn't too bad... once I realized you should store the ranges and not actually generate all the numbers o_O.
Part 2
This part took me forever, mostly to actually run, but also to fix a few off-by-one errors :|
Fortunately, I was able to re-use most of my code in Part A and just add a new function to search the range of seeds.
Even with
concurrent.futures
and a 24-core machine, it still took me about 30 - 45 minutes to complete with Python (that said, there were only 10 seed range s in the input, so I could only use 10 cores, and of those 5 of the ranges appeared to be really large, leading to a long tail effect).GitHub Repo
I got the solution to part 1 under an hour last night, then banged out the brute force for part 2. It was going to take 8 minutes to finish without even trying to parallelize it, and I knew I was not going to develop the robust algorithm in 8 minutes, so I just let it run.
I went back today and wrote general purpose Range class to do the set operations.
There's an intersect operation that produces all the overlapping segments from two ranges. More properly, I should call it "Overlap" because it's not an intersect in the set notation sense. Each range carries user-defined metadata along with it, and (particularly useful), the Ranges output from the Intersect accumulate metadata from the ranges that overlap with it.
The way I implemented it didn't quite work out, as I was thinking I could just remap the ranges and accumulate offsets from each layer, not thinking about the fact that I actually needed to do the transform at each layer. However, I was able to use the range operations and the metadata as markers to correctly implement the algorithm. So a little bit roundabout, but the end result is pleasingly fast.
All in it took me a lot longer than I expected, but a lot of it was writing unit tests for the Range operations because I hope to reuse them.
The solution is here
One thing I'm noticing is that my code is very verbose compared to a lot of the solutions. In my mind, it's more maintainable or testable, but I'm not even sure that's true, or how to feel about it. Maybe it is a product of early training in Java and C++.
My rust solutions tend to also be a bit more verbose than other peeps it seems, I usually start by making data strucutres to parse the input into and then impl functions on those structures. I also put actual unit tests in my code as well.
In the real world, I want to write good code first, and how fast is a secondary consideration. I think rushing things (skipping review, unit tests, etc) makes you slower overall because debugging weird problems later is a lot more work. Sort of a tortoise and hare situation.
I can see that placing high in AoC requires a different skill, something like "knowing how to do the minimum needed to solve the problem". I'm not sure that is something I want to learn even if placing in the top 100 (or 1000) would be cool.
So maybe I should stop worrying and learn to love 4000th place.
Yah, I dont even stay up late for them, just do them in the afternoon on my lunchbreak. If I can do it the day of, im proud of myself. (day 5 took me an extra day to figure out the range splitting, including notebook scribbles.)
Rust
It's not pretty. Brute forced for part 2, though it's at least parallelized so it takes ~16 seconds on an M2.
Day 5
I was going to wait to post here until I wrote a solution that wasn't brute force, but I haven't been able to. I figured out what I'm supposed to do, but for some reason I can only get my program to work with either the test input or the proper input, not both. The only way I can get the correct answer for the proper input is by forcing my program to have what I believe is the incorrect behavior? It's really weird. Since it's been a day and I haven't figured it out, though, I'll just post my brute force solution from last night.
Solution
I had to hold off on doing part two until today because my brain felt like mush all yesterday and it was just not happening. XD
I decided not to go the bruteforce route with this one.
At first I tried figuring out the different range overlap cases myself, but I eventually gave up and used the ranges library to calculate them.
Part one takes 52.506μs and part two takes 114.60μs on my desktop with a ryzen 2700x.
Rust
Elixir
I just got out of recursion hell implementing the optimized part 2 solution, and it's pretty wild to me that:
Messy part 1 and 2 solutions, featuring triple-nested
Enum.reduce
just realizing I forgot to put my solution out there when actually got it done...
Day 5 part 1 was straightforward, though kinda tedious to implement. Part 2 actually took me till the next day before I came up with a solution, but I ended up with a 1.2 ms solve for both parts! Parsing is probably taking more time than the math is!
https://git.venberg.xyz/Gabe/advent_of_code_2023/src/branch/main/days/day05
Rust
At first I did things the brute-force way, which was fine for part 1 but resulted in part 2 taking 85 seconds(!) to run — given that it was doing calculations for some 1.6 billion seeds on a single core, that's respectable, but I want things to run quickly. With some imports I could have made it multi-core, but I'm still sticking to
std
and that wouldn't have gotten it down to where I wanted anyway.In the end, though, I figured out the same technique of collapsing the layers into one so that the actual comparisons can be done both intelligently and all at once that it seems most people used. Getting all the edge cases nailed out was rough, but I got it and in the end both parts combined run in just under 200 μs.
Of course, between implementing the smart solution and just not having much time the past few days I'm now a bit behind on the other days, so I have some catching up to do.
Parser and data structure — sorry, this one's ugly
Part 1
Part 2
Posting this a few days late because I waited to sit down and think through a non-bruteforce part two strategy. Not super clean but it does the thing (and doesn't take 45 minutes with 4 cores!). Here are my Python solutions:
Part 1
Part 2