12
votes
Day 15: Chiton
Today's problem description: https://adventofcode.com/2021/day/15
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>
Oh god, a shortest path algorithm. It's been a minute since I've done one of these. I don't know if I'll finish tonight. We'll see how it goes
Edit: Wow, this one is just kinda dickish on multiple levels
Edit 2: This felt like a bad tech interview sort of gotcha question. Did anyone enjoy this? Or am I the only one mad about it.
Day 15 Part A - Python
I had to look up how Djikstra's algorithm works since it's been a while since I've written a shortest path algorithm. IMO they're pretty tedious and rarely useful to know
Day 15 Part B - Python
Wow. Really? Wow. Just fuck you. I did not enjoy this problem. After running into an actually pretty funny bug that required me to rewrite my display program, I just made a bigger grid and ran it. It took like 30-60 minutes to run, and I'm pretty sure that most of the time was from how I was handling my memory and I really didn't want to get into that sort of optimization.
I'm well aware my implementation is janky, but I don't think this one had a different way to think about it like some of the other optimization problems did. It's just 'be better at algorithms' or 'use a library'
Tips/Comments
Ugh, fuck this problem, there are a LOT of ways to make it take an extremely long time to solve. It felt like a substantial jump in difficulty
For the first part at least I highly recommend looking up some shortest path algorithms such as Djikstra's Algorithm. It's the sort of problem where you're extremely unlikely to be able to have a spark of insight unless you've studied that sort of problem
Even with a decent algorithm part B might take a while. My code wasn't great but it still took somewhere between 30-60 minutes to run while being able to handle part A in <1 second.
An out of the box pathfinding solution will probably have good memory management Deep Magic that will substantially speed up the problem, but also that doesn't feel like really engaging with the problem. I don't know.
I enjoyed today's problem. I came up with a working solution relatively quickly (though my part 2 solution took over a minute to run even with a Rust --release build). Then, wanting to optimize and come up with a fast solution, I ultimately learned about binary heaps/priority queues. So I not only got the satisfaction of being able to solve it myself (slowly), I also then later got the satisfaction of learning something new.
I did struggle to generate the expanded map today. I kept making silly errors with that.
I'm glad you enjoyed it and had a chance to learn from it. For some reason this one just made me really grumpy
Day 15 (Rust)
After yesterday's twisted path of debugging, today was very straightforward and fun.
Imports
This uses the
pathfinding
crate and two structs I've extracted from previous problems, a (x, y)-Position and a Grid (a simple wrapper aroundndarray
).You could consider using the crate for pathfinding cheating, but if a library exists, why not use it? I've written plenty of pathfinders in my life, so I don't feel bad about skipping this one.
Interestingly, it turns out that Dijkstra works faster here than A* or Fringe search, both of which use a heuristic to guide the search and should in theory be faster.
Parsing
Again, re-used from day 9.
Helper functions
Solving
[Edit] Bonus
Made my own Dijkstra-Algorithm after all. The only problem I had was that BinaryHeap is a max-heap, not a min-heap, so that caused some head scratching.
It takes 30ms more than the one from the pathfinding crate, for a total of 100ms.
Another thing you can do to make a min-heap in Rust is make it of type BinaryHeap<Reverse<your dat>> and then push like
heap.push(Reverse(data))
and do the same when popping. With something like this where you have to implementOrd
on your own anyways it's definitely easier to just do that backwards like you didData Structure
I chose a 2d matrix since the problem has fixed dimensions that are small (500 x 500 for part 2.)
Part 1
Is it cheating that I wrote my own little library of graph search algorithms in 2016?
I always regret reaching for it because it's slooooooooooooow, it eats memory and I always make
mistakes writing the functions needed to use it.
Still it has classic A* and I used a manhattan distance heuristic - not great, not terrible.
To use it I have a struct
problem
that holds:bfs
,dfs
,dijkstra
andA*
all use this struct to do their thing.They all return a list of 3 items:
To avoid having to pass round the risks and it's dimension/s I abuse
Racket's parameters.
They provide dynamic (runtime) binding.
(define v (make-parameter "This string could be <ANY OBJECT>"))
creates a parameter
v
which is a procedure that returns the value.(v)
- returns"This string could be <ANY OBJECT>"
You can reset the value by calling it with an argument
(v 7)
Further calls to
(v)
thereafter return this new value.(v)
- returns7
.I need a few small procedures to help
Part 2
I need to extend the risks with the extra tiles and then basically do the same.
I little fed-up of these problems try and trip you up and trick you e.g.
by switching the start and end of the path searched between parts 1 and 2.
You could argue that it represents real world problem solving, like when
your clients completely change their minds, but to that I'd ask:
What does that achieve in terms of making an enjoyable puzzle?
For me the answer is: Literaly nothing.
You've already forced us to do the fussy work of replicating the cave with a rule
to change the values.
I also get a bit fed-up with all the intricate little gotchas. Like:
divisilbe by the fifth prime greater than the highest common factor of the 2 previous results
that included a capital letter missing from it's position in the palidrome of the next day's
secret offset by the index of the previous capital letter.
Still it's a small gripe considering the amount of effort that goes into creating and hosting this every year. I feel a mean and petty mentioning them but I think it's better to get it off your chest than let it fester.
The horror of search
All the different search procedures are wrappers for this brutish beast
Internal data structures.
Yeah it eats memory and takes a long time and while it was fun writing I'm a bit embarrassed by its sluggish, bloated behaviour.
Interesting. I was wondering why you switched start and end around in your code when reading through it. With all the negative sentiment here, it feels like I did a completely different puzzle -- my puzzle description emphasizes for both paths that they start at the top left and go to the bottom-right, for example. I suppose the odds for that are 1:4 if they randomize it.
Well now I'm, confused. It states top-left to bottom-right for BOTH parts. No idea why I thought part 1 was bottom-right to top-left.
When I was solving part 2, I got a different answers for the test input. I swapped start and end got the right answer. At the time I wondered how it cold possibly be different but shrugged it off. I mean it can't be different, so I must have had some other bug in there.
Potential spoiler
The start location matters because it is not counted towards the total path cost. In the example input, both top-left and bottom-right are '1', so it doesn't actually matter for part 1. Part two has '1' and '9' in the example input, so there it matters where you start.
Now I feel stupid.
But thanks for explaining what I failed see.
Rust
Later I think I need to switch this to a
priority_queue
. This takes a whole minute for part 2 with a regularVecDeque
.Edit: Switched to a
BinaryHeap
, now it's fastRust
Python
Added time:
Welp, this is the first problem I didn't like this year. It felt a bit bland how the problem is just a bog-standard pathfinding problem solved by bog-standard Dijkstra's with no changes whatsoever. I did like part 2's extension, but the run-time to solve it was a bit annoying, to say the least.
Part 1
Part 2
Elixir
It terminates, it passes the provided test cases and gives the correct answer for part 1, but it apparently gives an incorrect (too low) answer for my real part 2 input. I'm stumped.
I was not interested in writing an efficient Dijkstra's algorithm implementation since I've already done that at least once in college, so I grabbed a
libgraph
library that does it for me. It's possible that the bug lies somewhere within that library, idk.Really not a fan of this puzzle.
Part 1 and (buggy, I guess?) part 2
I am in a similar boat to @jzimbel for my solution for this problem. No matter what I try, I get a too high result from part 2 with the actual input, even though it is dead on for the test input. I went with an algorithm I remembered from the one time I was taught robotics, apparently called a wavefront expansion algorithm, though I implemented it completely without queues and just created a mirror map with the costs. Run time is fine, ~210 ms with a debug build, but I can see why it would need to be optimized for a bigger problem. I actually don't mind the path finding problem per se, but I really hate when part 2 (apparently) introduces a bug, but with no additional test data for sussing it out. If anyone has their input data I would be interested in trying it out.
Part 1 and buggy Part 2