Speeding up my Advent of Code solution with Dijkstra's Algorithm
blog.siraben.dev
blog.siraben.dev
I haven't yet attempted this year's AOC but I suspect this is one where working backwards from the bottom right and dynamically storing the best value at each cell would work much better than dijsktra.
I find C++'s set and map and related data structures are also very useful for solving cp problems.
Here the number of edges is linear in the number of vertices (at most 4x) so it doesn't make a difference (relative simplicity of priority queue probably wins out here in practice) but it's even more efficient to solve via memoized recursion as briefly mentioned in the article, assuming that you don't blow your stack.
That said, the title aside, it's still a worthwhile post. It's sometimes easy to accidentally nuke your algorithm's performance with an innocent-looking function call or operation that's secretly turning it quadratic or worse.
And initially implemented it naively with wrong data structures. Which of course resulted in implementation working too slow for everything except first toy example.
Then I gradually optimized it bit by bit. I eventually landed on permanently sorted array of nodes to evaluate with simple insertion sort (with linear search) and it became blazing fast.
Along the way I learned the hard way that the heuristic in a* algorithm should never return higher value than the minimal value that that will be actually assigned to the path to destination. That is, if you want algorithm to result in optimal path. Because somehow I missed that when reading about a*.
I had a blast. Advent of code is absolutely wonderful thing.
That's true, but if you don't mind sacrificing a bit of optimality, then overstating the heuristic can speed up the calculation. This is called "overdo" in 'Fast shortest path computation in time-dependent traffic networks' (Lefebvre and Balmer).
Dijkstra's algorithm is the algorithm as specified with the runtime characteristics as specified. He wrote a different algorithm which can solve the same problem and works a lot like Dijkstra's but is much slower. Then he finally actually implemented Dijkstra's and the result was the speedup.
The title lead me to believe that he found some way to gain big speedups in Dijkstra's algorithm.
This is like me writing an article about prime factorization and doing naive trial division and then making claims like this when I instead choose to use a sieve.
The author is an undergraduate student so probably just missed that part about Dijkstra's algorithm, but the title should be changed nonetheless.
Going from the obvious backtracking algorithm to the naive Dijkstra algorithm, reduce the complexity from exponential to quadratic, that is a huge difference and is visible even in small boards. Going from quadratic to loglinear is a nice improvement, and it's easier if you have already understood the other part.
Perhaps he was taught the naive version and rediscovered the full version. Perhaps he was taught both and should have been more clear about that. I agree that this is not a new groundbreaking discovery, but I think it's a nice post anyway.
I was working in Idris. I knew better, but figured the data was small enough that I could just do a linear scan for the next node instead of implementing a heap. In the fast version I ended up using a SortedMap (Int,(Int,Int)) () for the queue.
I only added points to the queue as I discovered them (neighbors of points that I visit), so it maxed out at 913 elements. It sounds like OP may have started out with all points in their queue?
(example: https://github.com/aldanor/aoc-2021/blob/master/src/day15/mo...)
[1] https://en.wikipedia.org/wiki/Bellman%E2%80%93Ford_algorithm
[2] https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorit...
> the possible moves are going right, down or up by one
but the actual problem (https://adventofcode.com/2021/day/15) allows moving in all four directions.
The difference is significant — the naïve attempt described in the article does not work on the actual problem.
The problem as stated in the article is actually quite interesting. The case where the only possible moves are down and right has a standard dynamic programming solution. A variation moving up is also allowed. The time complexity is O(n^2) where n is the grid size, which is faster than using Dijkstra's algorithm.
And regarding the title, I am glad you changed it, but I'd prefer something more similar to the article's new title, such as "Speeding up an Advent of Code solution with Dijkstra's". (At the time of writing, the HN title is "Finding an optimal shortest path algorithm", which doesn't provide much information, and the article's title is "Speeding up my AoC solution by a factor of 2700 with Dijkstra's".)
Thanks! I'll update the article to say this.
> And regarding the title, I am glad you changed it...
Hopefully dang sees this. After a certain amount of time, the title of submissions cannot be changed (I didn't make the most recent changes.)
> [...]
> even if you have the right algorithm, the choice of how you represent auxillary data in the algorithm matters.
I think this is a very useful insight and the core of the piece. It just goes to show how important the layout data is with respect to the algorithm using it.
The ability to visualize the impact of your layout on caching is the fastest pay-off you'll ever have as a programmer, just knowing the sizes of your caches can already make a huge difference.
For instance: if you have a bunch of data and you need to do operations on that data it is much faster to pull in the data piecemeal, do all the operations on it bit-by-bit and then to move on to the next bit of data than to do several sequential passes over the data storing the intermediate results.
The difference in speed can be multiple orders of magnitude.
All steps are of length 1, so you can keep two arrays. In the first one all the things that you are currently visiting and push to the second array the neighbors that you'll visit in the next cycle. Once you are done swap arrays and start over.
But, you just need the sum, not the path. There's no need for a shortest path strategy.
A-star is very insertion heavy. In easy cases with a factor of 5, so using Fib gives significant boost in performance.
[0] https://hackage.haskell.org/package/containers-0.6.5.1/docs/...