mirror of
https://github.com/mfocko/blog.git
synced 2024-11-22 04:53:47 +01:00
algorithms(paths): ‹BF to A*› (#13)
This commit is contained in:
commit
5549494f67
13 changed files with 1705 additions and 0 deletions
573
algorithms/11-paths/2024-01-01-bf-to-astar/01-bf.md
Normal file
573
algorithms/11-paths/2024-01-01-bf-to-astar/01-bf.md
Normal file
|
@ -0,0 +1,573 @@
|
||||||
|
---
|
||||||
|
id: bf
|
||||||
|
slug: /paths/bf-to-astar/bf
|
||||||
|
title: BF
|
||||||
|
description: |
|
||||||
|
Solving the shortest path problem with a naïve approach that turns into
|
||||||
|
something.
|
||||||
|
tags:
|
||||||
|
- cpp
|
||||||
|
- brute force
|
||||||
|
- bellman ford
|
||||||
|
- dynamic programming
|
||||||
|
last_update:
|
||||||
|
date: 2024-01-01
|
||||||
|
---
|
||||||
|
|
||||||
|
## Basic idea
|
||||||
|
|
||||||
|
We will _ease in_ with our own algorithm to find the shortest path. We will
|
||||||
|
start by thinking about the ways we can achieve that. If we didn't have the `*`
|
||||||
|
cells, we could've easily run a BFS[^1] and be done with it. Maybe it is a good
|
||||||
|
place to start, or isn't, there is only one way to find out though.
|
||||||
|
|
||||||
|
_How does the BFS work?_ We know the vertex where we start and we know the
|
||||||
|
vertex we want to find the shortest path to. Given this knowledge we
|
||||||
|
incrementally visit all of our neighbours and we do that over and over until the
|
||||||
|
destination is found[^2]. Could we leverage this somehow?
|
||||||
|
|
||||||
|
## Naïve approach
|
||||||
|
|
||||||
|
Well, we could probably start with all vertices being _unreachable_ (having the
|
||||||
|
highest possible price) and try to improve what we've gotten so far until there
|
||||||
|
are no improvements. That sounds fine, we shall implement this. Since we are
|
||||||
|
going on repeat, we will name this function `bf()` as in _brute-force_, cause it
|
||||||
|
is trying to find it the hard way:
|
||||||
|
```cpp
|
||||||
|
const static std::vector<vertex_t> DIRECTIONS =
|
||||||
|
std::vector{std::make_pair(0, 1), std::make_pair(0, -1),
|
||||||
|
std::make_pair(1, 0), std::make_pair(-1, 0)};
|
||||||
|
|
||||||
|
auto bf(const graph& g, const vertex_t& source, const vertex_t& destination)
|
||||||
|
-> int {
|
||||||
|
// ‹source› must be within the bounds
|
||||||
|
assert(g.has(source));
|
||||||
|
|
||||||
|
// ‹destination› must be within the bounds
|
||||||
|
assert(g.has(destination));
|
||||||
|
|
||||||
|
// we need to initialize the distances
|
||||||
|
std::vector<std::vector<int>> distances(
|
||||||
|
g.height(), std::vector(g.width(), graph::unreachable()));
|
||||||
|
|
||||||
|
// ‹source› destination denotes the beginning where the cost is 0
|
||||||
|
auto [sx, sy] = source;
|
||||||
|
distances[sy][sx] = 0;
|
||||||
|
|
||||||
|
// now we need to improve the paths as long as possible
|
||||||
|
bool improvement_found;
|
||||||
|
do {
|
||||||
|
// reset the flag at the beginning
|
||||||
|
improvement_found = false;
|
||||||
|
|
||||||
|
// go through all of the vertices
|
||||||
|
for (int y = 0; y < g.height(); ++y) {
|
||||||
|
for (int x = 0; x < g.width(); ++x) {
|
||||||
|
// skip the cells we cannot reach
|
||||||
|
if (distances[y][x] == graph::unreachable()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// go through the neighbours
|
||||||
|
auto u = std::make_pair(x, y);
|
||||||
|
for (const auto& [dx, dy] : DIRECTIONS) {
|
||||||
|
auto v = std::make_pair(x + dx, y + dy);
|
||||||
|
auto cost = g.cost(u, v);
|
||||||
|
|
||||||
|
// if we can move to the cell and it's better, relax¹ it
|
||||||
|
if (cost != graph::unreachable() &&
|
||||||
|
distances[y][x] + cost < distances[y + dy][x + dx]) {
|
||||||
|
distances[y + dy][x + dx] = distances[y][x] + cost;
|
||||||
|
improvement_found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (improvement_found);
|
||||||
|
|
||||||
|
return distances[destination.second][destination.first];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
:::info Relaxation
|
||||||
|
|
||||||
|
I have made a brief mention of the relaxation in the comment in the code. You've
|
||||||
|
been probably thought that **relaxation of an edge** means that you found
|
||||||
|
a better solution to the problem.
|
||||||
|
|
||||||
|
In general it is an approximation technique that _reduces_ the problem of
|
||||||
|
finding the path `u → x1 → … → xn → v` to subproblems
|
||||||
|
`u → x1, x1 → x2, …, xn → v` such that the sum of the costs of each step is
|
||||||
|
**minimal**.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Correctness
|
||||||
|
|
||||||
|
_Is our solution correct?_ It appears to be correct… We have rather complicated
|
||||||
|
map and our algorithm has finished in an instant with the following output:
|
||||||
|
```
|
||||||
|
Normal cost: 1
|
||||||
|
Vortex cost: 5
|
||||||
|
Graph:
|
||||||
|
#############
|
||||||
|
#..#..*.*.**#
|
||||||
|
##***.....**#
|
||||||
|
#..########.#
|
||||||
|
#...###...#.#
|
||||||
|
#..#...##.#.#
|
||||||
|
#..#.*.#..#.#
|
||||||
|
#D...#....#.#
|
||||||
|
########*.*.#
|
||||||
|
#S..........#
|
||||||
|
#############
|
||||||
|
Cost: 22
|
||||||
|
```
|
||||||
|
|
||||||
|
If you have a better look at the map, you will realize that the cost `22` is the
|
||||||
|
one path skipping the `*` cells, since they cost more than going around.
|
||||||
|
|
||||||
|
We can play around a bit with it. The `*` cells can be even vortices that pull
|
||||||
|
you in with a negative price and let you _propel_ yourself out :wink: Let's
|
||||||
|
change their cost to `-1` then. Let's check what's the fastest path to the cell.
|
||||||
|
```
|
||||||
|
Normal cost: 1
|
||||||
|
Vortex cost: -1
|
||||||
|
Graph:
|
||||||
|
#############
|
||||||
|
#..#..*.*.**#
|
||||||
|
##***.....**#
|
||||||
|
#..########.#
|
||||||
|
#...###...#.#
|
||||||
|
#..#...##.#.#
|
||||||
|
#..#.*.#..#.#
|
||||||
|
#D...#....#.#
|
||||||
|
########*.*.#
|
||||||
|
#S..........#
|
||||||
|
#############
|
||||||
|
```
|
||||||
|
|
||||||
|
And we're somehow stuck… The issue comes from the fact that _spinning around_ in
|
||||||
|
the vortices allows us to lower the cost infinitely. That's why after each
|
||||||
|
iteration there is still a possibility to lower the cost, hence the algorithm
|
||||||
|
doesn't finish. _What can we do about this?_
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
|
||||||
|
This algorithm is correct as long as there are no negative loops, i.e. ways how
|
||||||
|
to lower the cost infinitely. Therefore we can also just lay a precondition that
|
||||||
|
requires no negative loops to be present.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Fixing the infinite loop
|
||||||
|
|
||||||
|
Our issue lies in the fact that we can endlessly lower the cost. Such thing must
|
||||||
|
surely happen in some kind of a loop. We could probably track the relaxations
|
||||||
|
and once we spot repeating patterns, we know we can safely terminate with _some_
|
||||||
|
results at least.
|
||||||
|
|
||||||
|
This approach will not even work on our 2D map, let alone any graph. Problem is
|
||||||
|
that the _negative loops_ lower the cost in **each** iteration and that results
|
||||||
|
in lowering of the costs to the cells that are reachable from the said loops.
|
||||||
|
That's why this problem is relatively hard to tackle, it's not that easy to spot
|
||||||
|
the repeating patterns algorithmically.
|
||||||
|
|
||||||
|
On the other hand, we can approach this from the different perspective. Let's
|
||||||
|
assume the worst-case scenario (generalized for any graph):
|
||||||
|
> Let $K_n$ be complete graph. Let $P$ be the shortest path from $v_1$ to $v_n$
|
||||||
|
> such that $P$ has $n - 1$ edges, i.e. the shortest path between the two chosen
|
||||||
|
> vertices visits all vertices (not necessarily in order) and has the lowest
|
||||||
|
> cost.
|
||||||
|
>
|
||||||
|
> In such scenario assume the worst-case ordering of the relaxations (only one
|
||||||
|
> _helpful_ relaxation per iteration). In this case, in each iteration we find
|
||||||
|
> the next edge on our path $P$ as the last. This means that we need
|
||||||
|
> $\vert V \vert - 1$ iterations to find the shortest path $P$.
|
||||||
|
>
|
||||||
|
> Because we have laid $P$ as the shortest path from $v_1$ to $v_n$ and it
|
||||||
|
> visits all vertices, its prefixes are the shortest paths from $v_1$ to any
|
||||||
|
> other vertex in our graph.
|
||||||
|
>
|
||||||
|
> Therefore, we can safely assume that any relaxation after $\vert V \vert - 1$
|
||||||
|
> iterations, is the effect of a negative loop in the graph.
|
||||||
|
|
||||||
|
_How can we leverage this?_ We will go through the edges only as many times as
|
||||||
|
cells we have. Let's adjust the code to fix the looping:
|
||||||
|
```cpp
|
||||||
|
auto bf_finite(const graph& g, const vertex_t& source,
|
||||||
|
const vertex_t& destination) -> int {
|
||||||
|
// ‹source› must be within the bounds
|
||||||
|
assert(g.has(source));
|
||||||
|
|
||||||
|
// ‹destination› must be within the bounds
|
||||||
|
assert(g.has(destination));
|
||||||
|
|
||||||
|
// we need to initialize the distances
|
||||||
|
std::vector<std::vector<int>> distances(
|
||||||
|
g.height(), std::vector(g.width(), graph::unreachable()));
|
||||||
|
|
||||||
|
// ‹source› destination denotes the beginning where the cost is 0
|
||||||
|
auto [sx, sy] = source;
|
||||||
|
distances[sy][sx] = 0;
|
||||||
|
|
||||||
|
// now we only iterate as many times as cells that we have
|
||||||
|
for (int i = g.height() * g.width(); i > 0; --i) {
|
||||||
|
// go through all of the vertices
|
||||||
|
for (int y = 0; y < g.height(); ++y) {
|
||||||
|
for (int x = 0; x < g.width(); ++x) {
|
||||||
|
// skip the cells we cannot reach
|
||||||
|
if (distances[y][x] == graph::unreachable()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// go through the neighbours
|
||||||
|
auto u = std::make_pair(x, y);
|
||||||
|
for (const auto& [dx, dy] : DIRECTIONS) {
|
||||||
|
auto v = std::make_pair(x + dx, y + dy);
|
||||||
|
auto cost = g.cost(u, v);
|
||||||
|
|
||||||
|
// if we can move to the cell and it's better, relax¹ it
|
||||||
|
if (cost != graph::unreachable() &&
|
||||||
|
distances[y][x] + cost < distances[y + dy][x + dx]) {
|
||||||
|
distances[y + dy][x + dx] = distances[y][x] + cost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return distances[destination.second][destination.first];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And we get the following result:
|
||||||
|
```
|
||||||
|
Normal cost: 1
|
||||||
|
Vortex cost: -1
|
||||||
|
Graph:
|
||||||
|
#############
|
||||||
|
#..#..*.*.**#
|
||||||
|
##***.....**#
|
||||||
|
#..########.#
|
||||||
|
#...###...#.#
|
||||||
|
#..#...##.#.#
|
||||||
|
#..#.*.#..#.#
|
||||||
|
#D...#....#.#
|
||||||
|
########*.*.#
|
||||||
|
#S..........#
|
||||||
|
#############
|
||||||
|
Cost: -236
|
||||||
|
```
|
||||||
|
|
||||||
|
The negative cost means that there is a way to _propel_ ourselves via some
|
||||||
|
vortices. Let's adjust the cost of _vortices_ back to the original `5` and check
|
||||||
|
whether our modified algorithm works as it did before. And it surely does yield
|
||||||
|
the `22` as before.
|
||||||
|
|
||||||
|
:::tip Refactoring
|
||||||
|
|
||||||
|
You can definitely notice some _deep nesting_ in our code, to counter this
|
||||||
|
phenomenon I will convert the looping over `x` and `y` to one variable that can
|
||||||
|
be decomposed to `x` and `y`. It is a very common practice when working with 2D
|
||||||
|
arrays/lists to represent them as 1D. In our case:
|
||||||
|
|
||||||
|
```
|
||||||
|
i : 0 → width * height - 1
|
||||||
|
x = i % width
|
||||||
|
y = i / width
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Bellman-Ford
|
||||||
|
|
||||||
|
If you have ever attended any Algorithms course that had path-finding in its
|
||||||
|
syllabus, you probably feel like you've seen the algorithm above before[^3]… And
|
||||||
|
yes, the first algorithm I have proposed is a very dumb version of the
|
||||||
|
_Bellman-Ford_ algorithm, it's dumb, because it loops :wink: After our “looping”
|
||||||
|
prevention we got to the point that is almost the _Bellman-Ford_ with the one
|
||||||
|
exception that it doesn't report whether there are any negative cycles, it just
|
||||||
|
ends.
|
||||||
|
|
||||||
|
Let's have a look at a proper implementation of the Bellman-Ford algorithm:
|
||||||
|
```cpp
|
||||||
|
auto bellman_ford(const graph& g, const vertex_t& source)
|
||||||
|
-> std::vector<std::vector<int>> {
|
||||||
|
// ‹source› must be within the bounds
|
||||||
|
assert(g.has(source));
|
||||||
|
|
||||||
|
// we need to initialize the distances
|
||||||
|
std::vector<std::vector<int>> distances(
|
||||||
|
g.height(), std::vector(g.width(), graph::unreachable()));
|
||||||
|
|
||||||
|
// ‹source› destination denotes the beginning where the cost is 0
|
||||||
|
auto [sx, sy] = source;
|
||||||
|
distances[sy][sx] = 0;
|
||||||
|
|
||||||
|
// now we only iterate as many times as cells that we have
|
||||||
|
for (int i = g.height() * g.width(); i > 0; --i) {
|
||||||
|
// go through all of the vertices
|
||||||
|
for (int v = g.height() * g.width() - 1; v >= 0; --v) {
|
||||||
|
int y = v / g.width();
|
||||||
|
int x = v % g.width();
|
||||||
|
|
||||||
|
// skip the cells we cannot reach
|
||||||
|
if (distances[y][x] == graph::unreachable()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// go through the neighbours
|
||||||
|
auto u = std::make_pair(x, y);
|
||||||
|
for (const auto& [dx, dy] : DIRECTIONS) {
|
||||||
|
auto v = std::make_pair(x + dx, y + dy);
|
||||||
|
auto cost = g.cost(u, v);
|
||||||
|
|
||||||
|
// if we can move to the cell and it's better, relax¹ it
|
||||||
|
if (cost != graph::unreachable() &&
|
||||||
|
distances[y][x] + cost < distances[y + dy][x + dx]) {
|
||||||
|
distances[y + dy][x + dx] = distances[y][x] + cost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// now we check for the negative loops
|
||||||
|
bool relaxed = false;
|
||||||
|
for (int v = g.height() * g.width() - 1; !relaxed && v >= 0; --v) {
|
||||||
|
int y = v / g.width();
|
||||||
|
int x = v % g.width();
|
||||||
|
|
||||||
|
// skip the cells we cannot reach
|
||||||
|
if (distances[y][x] == graph::unreachable()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// go through the neighbours
|
||||||
|
auto u = std::make_pair(x, y);
|
||||||
|
for (const auto& [dx, dy] : DIRECTIONS) {
|
||||||
|
auto v = std::make_pair(x + dx, y + dy);
|
||||||
|
auto cost = g.cost(u, v);
|
||||||
|
|
||||||
|
// if we can move to the cell and it's better, relax¹ it
|
||||||
|
if (cost != graph::unreachable() &&
|
||||||
|
distances[y][x] + cost < distances[y + dy][x + dx]) {
|
||||||
|
relaxed = true;
|
||||||
|
std::cerr << "Found a negative loop\n";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return distances;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And if we run it with our negative cost of entering vortices:
|
||||||
|
```
|
||||||
|
[Bellman-Ford] Found a negative loop
|
||||||
|
[Bellman-Ford] Cost: -240
|
||||||
|
```
|
||||||
|
|
||||||
|
### On the Bellman-Ford
|
||||||
|
|
||||||
|
You might be surprised that we have managed to iterate from a brute-force method
|
||||||
|
that mindlessly tries to find a better path until there are no better paths left
|
||||||
|
all the way to the Bellman-Ford algorithm.
|
||||||
|
|
||||||
|
I always say that Bellman-Ford is a _smart_ brute-force. BF is also an algorithm
|
||||||
|
that leverages _dynamic programming_. You might wonder how can it utilize DP if
|
||||||
|
it is “technically” a brute-force technique. Table with the shortest distances
|
||||||
|
is the thing that makes it DP.
|
||||||
|
|
||||||
|
> I might not know the shortest path yet, but I do remember all of other paths,
|
||||||
|
> and I can improve them, if possible.
|
||||||
|
|
||||||
|
That's where the beauty of both _dynamic programming_ and _relaxing_ gets merged
|
||||||
|
together and does its magic.
|
||||||
|
|
||||||
|
Proof of the correctness of the BF is done via induction to the number of
|
||||||
|
iterations. I would suggest to try to prove the correctness yourself and
|
||||||
|
possibly look it up, if necessary.
|
||||||
|
|
||||||
|
Also the correctness of the BF relies on the conclusion we've made when fixing
|
||||||
|
the infinite-loop on our naïve BF solution.
|
||||||
|
|
||||||
|
## Time complexity
|
||||||
|
|
||||||
|
Let's have a short look at the time complexities of the presented algorithms:
|
||||||
|
|
||||||
|
1. naïve approach: given that there are no negative loops, we are bound by the
|
||||||
|
worst-case ordering of the relaxations which results in
|
||||||
|
$$
|
||||||
|
\mathcal{O}(\vert V \vert \cdot \vert E \vert)
|
||||||
|
$$
|
||||||
|
|
||||||
|
2. our naïve approach with the fixed count of iterations instead of the
|
||||||
|
`do-while` loop results in the same worst-case time complexity:
|
||||||
|
$$
|
||||||
|
\Theta(\vert V \vert \cdot \vert E \vert)
|
||||||
|
$$
|
||||||
|
|
||||||
|
3. and finally the well-known Bellman-Ford's algorithm time complexity:
|
||||||
|
$$
|
||||||
|
\Theta(\vert V \vert \cdot \vert E \vert)
|
||||||
|
$$
|
||||||
|
|
||||||
|
## Small refactor
|
||||||
|
|
||||||
|
Since we are literally copy-pasting the body of the loops just for the sake of
|
||||||
|
relaxing, we can factor that part out into a separate function:
|
||||||
|
```cpp
|
||||||
|
static auto _check_vertex(const graph& g,
|
||||||
|
std::vector<std::vector<int>>& distances, int v,
|
||||||
|
bool check_only = false) -> bool {
|
||||||
|
bool improvement_found = false;
|
||||||
|
|
||||||
|
// unpack the vertex coordinates
|
||||||
|
int y = v / g.width();
|
||||||
|
int x = v % g.width();
|
||||||
|
|
||||||
|
// skip the cells we cannot reach
|
||||||
|
if (distances[y][x] == graph::unreachable()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// go through the neighbours
|
||||||
|
auto u = std::make_pair(x, y);
|
||||||
|
for (const auto& [dx, dy] : DIRECTIONS) {
|
||||||
|
auto v = std::make_pair(x + dx, y + dy);
|
||||||
|
auto cost = g.cost(u, v);
|
||||||
|
|
||||||
|
// if we can move to the cell and it's better, relax¹ it
|
||||||
|
if (cost != graph::unreachable() &&
|
||||||
|
distances[y][x] + cost < distances[y + dy][x + dx]) {
|
||||||
|
if (check_only) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
distances[y + dy][x + dx] = distances[y][x] + cost;
|
||||||
|
improvement_found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return improvement_found;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This function can be also used for checking the negative loops at the end of the
|
||||||
|
BF by using the `check_only` parameter to signal that we just want to know if
|
||||||
|
there would be any edge relaxed instead of performing the relaxation itself.
|
||||||
|
|
||||||
|
Then we can also see the differences between the specific versions of our
|
||||||
|
path-finding algorithms in a clear way:
|
||||||
|
```cpp
|
||||||
|
auto bf(const graph& g, const vertex_t& source, const vertex_t& destination)
|
||||||
|
-> int {
|
||||||
|
// ‹source› must be within the bounds
|
||||||
|
assert(g.has(source));
|
||||||
|
|
||||||
|
// ‹destination› must be within the bounds
|
||||||
|
assert(g.has(destination));
|
||||||
|
|
||||||
|
// we need to initialize the distances
|
||||||
|
std::vector<std::vector<int>> distances(
|
||||||
|
g.height(), std::vector(g.width(), graph::unreachable()));
|
||||||
|
|
||||||
|
// ‹source› destination denotes the beginning where the cost is 0
|
||||||
|
auto [sx, sy] = source;
|
||||||
|
distances[sy][sx] = 0;
|
||||||
|
|
||||||
|
// now we need to improve the paths as long as possible
|
||||||
|
bool improvement_found;
|
||||||
|
do {
|
||||||
|
// reset the flag at the beginning
|
||||||
|
improvement_found = false;
|
||||||
|
|
||||||
|
// go through all of the vertices
|
||||||
|
for (int v = g.height() * g.width() - 1; v >= 0; --v) {
|
||||||
|
improvement_found = _check_vertex(g, distances, v) || improvement_found;
|
||||||
|
}
|
||||||
|
} while (improvement_found);
|
||||||
|
|
||||||
|
return distances[destination.second][destination.first];
|
||||||
|
}
|
||||||
|
|
||||||
|
auto bf_finite(const graph& g, const vertex_t& source,
|
||||||
|
const vertex_t& destination) -> int {
|
||||||
|
// ‹source› must be within the bounds
|
||||||
|
assert(g.has(source));
|
||||||
|
|
||||||
|
// ‹destination› must be within the bounds
|
||||||
|
assert(g.has(destination));
|
||||||
|
|
||||||
|
// we need to initialize the distances
|
||||||
|
std::vector<std::vector<int>> distances(
|
||||||
|
g.height(), std::vector(g.width(), graph::unreachable()));
|
||||||
|
|
||||||
|
// ‹source› destination denotes the beginning where the cost is 0
|
||||||
|
auto [sx, sy] = source;
|
||||||
|
distances[sy][sx] = 0;
|
||||||
|
|
||||||
|
// now we only iterate as many times as cells that we have
|
||||||
|
for (int i = g.height() * g.width(); i > 0; --i) {
|
||||||
|
// go through all of the vertices
|
||||||
|
for (int v = g.height() * g.width() - 1; v >= 0; --v) {
|
||||||
|
_check_vertex(g, distances, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return distances[destination.second][destination.first];
|
||||||
|
}
|
||||||
|
|
||||||
|
auto bellman_ford(const graph& g, const vertex_t& source)
|
||||||
|
-> std::vector<std::vector<int>> {
|
||||||
|
// ‹source› must be within the bounds
|
||||||
|
assert(g.has(source));
|
||||||
|
|
||||||
|
// we need to initialize the distances
|
||||||
|
std::vector<std::vector<int>> distances(
|
||||||
|
g.height(), std::vector(g.width(), graph::unreachable()));
|
||||||
|
|
||||||
|
// ‹source› destination denotes the beginning where the cost is 0
|
||||||
|
auto [sx, sy] = source;
|
||||||
|
distances[sy][sx] = 0;
|
||||||
|
|
||||||
|
// now we only iterate as many times as cells that we have
|
||||||
|
for (int i = g.height() * g.width(); i > 0; --i) {
|
||||||
|
// go through all of the vertices
|
||||||
|
for (int v = g.height() * g.width() - 1; v >= 0; --v) {
|
||||||
|
_check_vertex(g, distances, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// now we check for the negative loops
|
||||||
|
for (int v = g.height() * g.width() - 1; v >= 0; --v) {
|
||||||
|
if (_check_vertex(g, distances, v, true)) {
|
||||||
|
std::cerr << "[Bellman-Ford] Found a negative loop\n";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return distances;
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
|
||||||
|
You might've noticed that I've been using abbreviation _BF_ interchangeably for
|
||||||
|
both _Bellman-Ford_ and _brute-force_. If you think about the way Bellman-Ford
|
||||||
|
algorithm works, you should realize that in the worst case it's updating the
|
||||||
|
shortest path till there no shorter path exists, so in a sense, you could really
|
||||||
|
consider it a brute-force algorithm.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
|
||||||
|
[^1]: [Breadth-first search](https://en.wikipedia.org/wiki/Breadth-first_search)
|
||||||
|
[^2]: Of course, there are some technicalities like keeping track of the visited
|
||||||
|
vertices to not taint the shortest path by already visited vertices.
|
||||||
|
[^3]: or at least you should, LOL
|
318
algorithms/11-paths/2024-01-01-bf-to-astar/02-dijkstra.md
Normal file
318
algorithms/11-paths/2024-01-01-bf-to-astar/02-dijkstra.md
Normal file
|
@ -0,0 +1,318 @@
|
||||||
|
---
|
||||||
|
id: dijkstra
|
||||||
|
slug: /paths/bf-to-astar/dijkstra
|
||||||
|
title: Dijkstra's algorithm
|
||||||
|
description: |
|
||||||
|
Moving from Bellman-Ford into the Dijsktra's algorithm.
|
||||||
|
tags:
|
||||||
|
- cpp
|
||||||
|
- dynamic programming
|
||||||
|
- greedy
|
||||||
|
- dijkstra
|
||||||
|
last_update:
|
||||||
|
date: 2024-01-03
|
||||||
|
---
|
||||||
|
|
||||||
|
## Intro
|
||||||
|
|
||||||
|
Let's rewind back to the small argument in the previous post about the fact that
|
||||||
|
we can safely bound the amount of iterations with relaxations being done.
|
||||||
|
|
||||||
|
We have said that assuming the worst-case scenario (bad order of relaxations) we
|
||||||
|
**need** at most $\vert V \vert - 1$ iterations over all edges. We've used that
|
||||||
|
to our advantage to _bound_ the iterations instead of the `do-while` loop that
|
||||||
|
was a risk given the possibility of the infinite loop (when negative loops are
|
||||||
|
present in the graph).
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
|
||||||
|
We could've possibly used both _boolean flag_ to denote that some relaxation has
|
||||||
|
happened and the upper bound of iterations, for some graphs that would result in
|
||||||
|
faster termination.
|
||||||
|
|
||||||
|
Using only the upper bound we try to relax edges even though we can't.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
Now the question arises, could we leverage this somehow in a different way? What
|
||||||
|
if we used it to improve the algorithm instead of just bounding the iterations?
|
||||||
|
Would that be even possible?
|
||||||
|
|
||||||
|
**Yes, it would!** And that's when _Dijkstra's algorithm_ comes in.
|
||||||
|
|
||||||
|
## Dijkstra's algorithm
|
||||||
|
|
||||||
|
I'll start with a well-known meme about Dijkstra's algorithm:
|
||||||
|
![Dijkstra's algorithm meme](/img/algorithms/paths/bf-to-astar/dijkstra-meme.jpg)
|
||||||
|
|
||||||
|
And then follow up on that with the actual backstory from Dijkstra himself:
|
||||||
|
> What is the shortest way to travel from Rotterdam to Groningen, in general:
|
||||||
|
> from given city to given city. It is the algorithm for the shortest path,
|
||||||
|
> which I designed in about twenty minutes. One morning I was shopping in
|
||||||
|
> Amsterdam with my young fiancée, and tired, we sat down on the café terrace to
|
||||||
|
> drink a cup of coffee and I was just thinking about whether I could do this,
|
||||||
|
> and I then designed the algorithm for the shortest path. As I said, it was
|
||||||
|
> a twenty-minute invention. In fact, it was published in '59, three years
|
||||||
|
> later. The publication is still readable, it is, in fact, quite nice. One of
|
||||||
|
> the reasons that it is so nice was that I designed it without pencil and
|
||||||
|
> paper. I learned later that one of the advantages of designing without pencil
|
||||||
|
> and paper is that you are almost forced to avoid all avoidable complexities.
|
||||||
|
> Eventually, that algorithm became to my great amazement, one of the
|
||||||
|
> cornerstones of my fame.
|
||||||
|
>
|
||||||
|
> — Edsger Dijkstra, in an interview with Philip L. Frana, Communications of the
|
||||||
|
> ACM, 2001
|
||||||
|
|
||||||
|
:::caution Precondition
|
||||||
|
|
||||||
|
As our own naïve algorithm, Dijkstra's algorithm has a precondition that places
|
||||||
|
a requirement of _no edges with negative weights_ in the graph. This
|
||||||
|
precondition is required because of the nature of the algorithm that requires
|
||||||
|
monotonically non-decreasing changes in the costs of shortest paths.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Short description
|
||||||
|
|
||||||
|
Let's have a brief look at the pseudocode taken from the Wikipedia:
|
||||||
|
```
|
||||||
|
function Dijkstra(Graph, source):
|
||||||
|
for each vertex v in Graph.Vertices:
|
||||||
|
dist[v] ← INFINITY
|
||||||
|
prev[v] ← UNDEFINED
|
||||||
|
add v to Q
|
||||||
|
dist[source] ← 0
|
||||||
|
|
||||||
|
while Q is not empty:
|
||||||
|
u ← vertex in Q with min dist[u]
|
||||||
|
remove u from Q
|
||||||
|
|
||||||
|
for each neighbor v of u still in Q:
|
||||||
|
alt ← dist[u] + Graph.Edges(u, v)
|
||||||
|
if alt < dist[v]:
|
||||||
|
dist[v] ← alt
|
||||||
|
prev[v] ← u
|
||||||
|
|
||||||
|
return dist[], prev[]
|
||||||
|
```
|
||||||
|
|
||||||
|
Dijkstra's algorithm works in such way that it always tries to find the shortest
|
||||||
|
paths from a vertex to which it already has a shortest path. This may result in
|
||||||
|
finding the shortest path to another vertex, or at least some path, till further
|
||||||
|
relaxation.
|
||||||
|
|
||||||
|
Given that we need to **always** choose the _cheapest_ vertex, we can use a min
|
||||||
|
heap to our advantage which can further improve the time complexity of the
|
||||||
|
algorithm.
|
||||||
|
|
||||||
|
## Used techniques
|
||||||
|
|
||||||
|
This algorithm leverages the _dynamic programming_ technique that has already
|
||||||
|
been mentioned with regards to the _Bellman-Ford_ algorithm and also _greedy_
|
||||||
|
technique. Let's talk about them both!
|
||||||
|
|
||||||
|
_Dynamic programming_ technique comes from the fact that we are continuously
|
||||||
|
building on top of the shortest paths that we have found so far. We slowly build
|
||||||
|
the shortest paths from the given source vertex to all other vertices that are
|
||||||
|
reachable.
|
||||||
|
|
||||||
|
_Greedy_ technique is utilized in such way that Dijkstra's algorithm always
|
||||||
|
improves the shortest paths from the vertex that is the closest, i.e. it tries
|
||||||
|
extending the shortest path to some vertex by appending an edge, such extended
|
||||||
|
path may (or may not) be the shortest path to another vertex.
|
||||||
|
|
||||||
|
:::tip Greedy algorithms
|
||||||
|
|
||||||
|
_Greedy algorithms_ are algorithms that choose the most optimal action
|
||||||
|
**at the moment**.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
The reason why the algorithm requires no edges with negative weights comes from
|
||||||
|
the fact that it's greedy. By laying the requirement of non-negative weights in
|
||||||
|
the graph we are guaranteed that at any given moment of processing outgoing
|
||||||
|
edges from a vertex, we already have a shortest path to the given vertex. This
|
||||||
|
means that either this is the shortest path, or there is some other vertex that
|
||||||
|
may have a higher cost, but the outgoing edge compensates for it.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
Firstly we need to have some priority queue wrappers. C++ itself offers
|
||||||
|
functions that can be used for maintaining max heaps. They also have generalized
|
||||||
|
version with any ordering, in our case we need reverse ordering, because we need
|
||||||
|
the min heap.
|
||||||
|
```cpp
|
||||||
|
using pqueue_item_t = std::pair<int, vertex_t>;
|
||||||
|
using pqueue_t = std::vector<pqueue_item_t>;
|
||||||
|
|
||||||
|
auto pushq(pqueue_t& q, pqueue_item_t v) -> void {
|
||||||
|
q.push_back(v);
|
||||||
|
std::push_heap(q.begin(), q.end(), std::greater<>{});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto popq(pqueue_t& q) -> std::optional<pqueue_item_t> {
|
||||||
|
if (q.empty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pop_heap(q.begin(), q.end(), std::greater<>{});
|
||||||
|
pqueue_item_t top = q.back();
|
||||||
|
q.pop_back();
|
||||||
|
|
||||||
|
return std::make_optional(top);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And now we can finally move to the actual implementation of the Dijkstra's
|
||||||
|
algorithm:
|
||||||
|
```cpp
|
||||||
|
auto dijkstra(const graph& g, const vertex_t& source)
|
||||||
|
-> std::vector<std::vector<int>> {
|
||||||
|
// make sure that ‹source› exists
|
||||||
|
assert(g.has(source));
|
||||||
|
|
||||||
|
// initialize the distances
|
||||||
|
std::vector<std::vector<int>> distances(
|
||||||
|
g.height(), std::vector(g.width(), graph::unreachable()));
|
||||||
|
|
||||||
|
// initialize the visited
|
||||||
|
std::vector<std::vector<bool>> visited(g.height(),
|
||||||
|
std::vector(g.width(), false));
|
||||||
|
|
||||||
|
// ‹source› destination denotes the beginning where the cost is 0
|
||||||
|
auto [sx, sy] = source;
|
||||||
|
distances[sy][sx] = 0;
|
||||||
|
|
||||||
|
pqueue_t priority_queue{std::make_pair(0, source)};
|
||||||
|
std::optional<pqueue_item_t> item{};
|
||||||
|
while ((item = popq(priority_queue))) {
|
||||||
|
auto [cost, u] = *item;
|
||||||
|
auto [x, y] = u;
|
||||||
|
|
||||||
|
// we have already found the shortest path
|
||||||
|
if (visited[y][x]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
visited[y][x] = true;
|
||||||
|
|
||||||
|
for (const auto& [dx, dy] : DIRECTIONS) {
|
||||||
|
auto v = std::make_pair(x + dx, y + dy);
|
||||||
|
auto cost = g.cost(u, v);
|
||||||
|
|
||||||
|
// if we can move to the cell and it's better, relax¹ it and update queue
|
||||||
|
if (cost != graph::unreachable() &&
|
||||||
|
distances[y][x] + cost < distances[y + dy][x + dx]) {
|
||||||
|
distances[y + dy][x + dx] = distances[y][x] + cost;
|
||||||
|
pushq(priority_queue, std::make_pair(distances[y + dy][x + dx], v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return distances;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Time complexity
|
||||||
|
|
||||||
|
The time complexity of Dijkstra's algorithm differs based on the backing data
|
||||||
|
structure.
|
||||||
|
|
||||||
|
The original implementation doesn't leverage the heap which results in
|
||||||
|
repetitive _look up_ of the “closest” vertex, hence we get the following
|
||||||
|
worst-case time complexity in the _Bachmann-Landau_ notation:
|
||||||
|
$$
|
||||||
|
\Theta(\vert V \vert^2)
|
||||||
|
$$
|
||||||
|
|
||||||
|
If we turn our attention to the backing data structure, we always want the
|
||||||
|
“cheapest” vertex, that's why we can use the min heap, given that we use
|
||||||
|
Fibonacci heap we can achieve the following amortized time complexity:
|
||||||
|
$$
|
||||||
|
\mathcal{O}(\vert E \vert + \vert V \vert \cdot \log{\vert V \vert})
|
||||||
|
$$
|
||||||
|
|
||||||
|
:::tip Fibonacci heap
|
||||||
|
|
||||||
|
Fibonacci heap is known as the heap that provides $\Theta(1)$ **amortized**
|
||||||
|
insertion and $\mathcal{O}(\log{n})$ **amortized** removal of the top (either
|
||||||
|
min or max).
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Running the Dijkstra
|
||||||
|
|
||||||
|
Let's run our code:
|
||||||
|
```
|
||||||
|
Normal cost: 1
|
||||||
|
Vortex cost: 5
|
||||||
|
Graph:
|
||||||
|
#############
|
||||||
|
#..#..*.*.**#
|
||||||
|
##***.....**#
|
||||||
|
#..########.#
|
||||||
|
#...###...#.#
|
||||||
|
#..#...##.#.#
|
||||||
|
#..#.*.#..#.#
|
||||||
|
#D...#....#.#
|
||||||
|
########*.*.#
|
||||||
|
#S..........#
|
||||||
|
#############
|
||||||
|
[Finite BF] Cost: 22
|
||||||
|
[Bellman-Ford] Cost: 22
|
||||||
|
[Dijkstra] Cost: 22
|
||||||
|
```
|
||||||
|
|
||||||
|
OK, so it seems to be working just fine. Now the question arises:
|
||||||
|
|
||||||
|
> What happens when we have negative weights in our graph?
|
||||||
|
|
||||||
|
## Busting the myth about looping Dijkstra
|
||||||
|
|
||||||
|
One of the very common misconception about Dijkstra's algorithm is that it loops
|
||||||
|
infinitely when you have negative weights or loops in the graph. Well, if we use
|
||||||
|
our _propelling vortices_, not only we have the negative weights, but also the
|
||||||
|
negative loops. Let's run our code! Our first naïve approach was actually
|
||||||
|
looping:
|
||||||
|
```
|
||||||
|
Normal cost: 1
|
||||||
|
Vortex cost: -1
|
||||||
|
Graph:
|
||||||
|
#############
|
||||||
|
#..#..*.*.**#
|
||||||
|
##***.....**#
|
||||||
|
#..########.#
|
||||||
|
#...###...#.#
|
||||||
|
#..#...##.#.#
|
||||||
|
#..#.*.#..#.#
|
||||||
|
#D...#....#.#
|
||||||
|
########*.*.#
|
||||||
|
#S..........#
|
||||||
|
#############
|
||||||
|
[Finite BF] Cost: -240
|
||||||
|
[Bellman-Ford] Found a negative loop
|
||||||
|
[Bellman-Ford] Cost: -240
|
||||||
|
[Dijkstra] Cost: 14
|
||||||
|
```
|
||||||
|
|
||||||
|
Well, it definitely doesn't loop. How much does `14` make sense is a different
|
||||||
|
matter.
|
||||||
|
|
||||||
|
:::info Variations
|
||||||
|
|
||||||
|
There are multiple variations of the Dijkstra's algorithm. You **can** implement
|
||||||
|
it in such way that with negative weights or loops it loops infinitely, but it
|
||||||
|
can be countered. In our case we keep the track of the vertices that already got
|
||||||
|
a shortest path established via the `visited`, that's how even multiple entries
|
||||||
|
for one vertex in the heap are not an issue.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Now we have an algorithm for finding the shortest path that is faster than our
|
||||||
|
original naïve brute-force or Bellman-Ford. However we need to keep in mind its
|
||||||
|
requirement of no negative weights for correct functioning.
|
||||||
|
|
||||||
|
You can also see how we used our thought process of figuring out the worst-case
|
||||||
|
time complexity for the naïve or Bellman-Ford algorithm to improve the original
|
||||||
|
path-finding algorithms.
|
224
algorithms/11-paths/2024-01-01-bf-to-astar/03-astar.md
Normal file
224
algorithms/11-paths/2024-01-01-bf-to-astar/03-astar.md
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
---
|
||||||
|
id: astar
|
||||||
|
slug: /paths/bf-to-astar/astar
|
||||||
|
title: A* algorithm
|
||||||
|
description: |
|
||||||
|
Moving from Dijkstra's algorithm into the A* algorithm.
|
||||||
|
tags:
|
||||||
|
- cpp
|
||||||
|
- dynamic programming
|
||||||
|
- astar
|
||||||
|
last_update:
|
||||||
|
date: 2024-01-03
|
||||||
|
---
|
||||||
|
|
||||||
|
## Intro
|
||||||
|
|
||||||
|
Let's start by the recap of what we've achieved so far:
|
||||||
|
1. We have implemented a naïve brute-force algorithm that tries to relax paths
|
||||||
|
as long as there are any paths to be relaxed.
|
||||||
|
2. Then we have fixed an issue caused by negative loops that can result in
|
||||||
|
a non-terminating run of our brute-force method. At this moment we have made
|
||||||
|
some small arguments why are bounding is enough and doesn't prevent any
|
||||||
|
shortest path to _not be_ discovered.
|
||||||
|
3. Finally we have converted our bounded brute-force algorithm into the
|
||||||
|
Bellman-Ford algorithm.
|
||||||
|
4. We have mentioned the worst-case time complexity of our bounded naïve
|
||||||
|
approach and also the Bellman-Ford algorithm. Our worst-case depended on the
|
||||||
|
fact that we assumed the worst possible ordering of the relaxations. However
|
||||||
|
we could also try to relax in the most ideal ordering which could result in a
|
||||||
|
faster algorithm and that's how we got to the Dijkstra's algorithm.
|
||||||
|
|
||||||
|
Now the question is, could we improve the Dijkstra's algorithm to get even
|
||||||
|
better results? And the answer is _maybe_!
|
||||||
|
|
||||||
|
Dijkstra's algorithm chooses the next cheapest vertex for relaxing. This is good
|
||||||
|
as long as there is no additional information. However, imagine a roadmap of
|
||||||
|
some country. If you're in the middle of the map and you want to go south, it
|
||||||
|
doesn't make much sense for you to go to the north (in the opposite direction),
|
||||||
|
but a little bit might make sense, so that you can switch to highway and go much
|
||||||
|
faster.
|
||||||
|
|
||||||
|
The important question here is how to _influence_ the algorithm, so that it does
|
||||||
|
choose the path that _makes more sense_ rather than the one that costs the
|
||||||
|
least.
|
||||||
|
|
||||||
|
## A* description
|
||||||
|
|
||||||
|
The _A* algorithm_ can be considered a modification of Dijkstra's algorithm. The
|
||||||
|
cost is still the same, we cannot change it, right? However when we pick the
|
||||||
|
vertices from the heap, we can influence the order by some _heuristic_. In this
|
||||||
|
case, we introduce a function that can suggest how feasible the vertex is.
|
||||||
|
|
||||||
|
## Roadmap heuristic
|
||||||
|
|
||||||
|
Let's have a look at the heuristic we could use for the roadmap example. There
|
||||||
|
are roads (the edges) and towns (the vertices). Cost could be an average time to
|
||||||
|
travel the road. What heuristic could we use to influence our algorithm to
|
||||||
|
choose a better ordering of the vertices when relaxing?
|
||||||
|
|
||||||
|
In the former example we've said that it doesn't make much sense to go in the
|
||||||
|
opposite direction than our goal is… We could choose the distance from our goal
|
||||||
|
as the heuristic, e.g. right now we're 100 km away from our goal, using this
|
||||||
|
road makes us 50 km away and using the other road we will be 200 km away.
|
||||||
|
|
||||||
|
## Heuristic for our map
|
||||||
|
|
||||||
|
Our map is a bit simpler, but we can use a very similar principle. We will use
|
||||||
|
the _Manhattan distance_, which is defined in a following way:
|
||||||
|
$$
|
||||||
|
\vert x_a - x_b \vert + \vert y_a - y_b \vert
|
||||||
|
$$
|
||||||
|
|
||||||
|
Since we cannot move in diagonals, it makes sense to maintain the distance in
|
||||||
|
the actual steps from the goal.
|
||||||
|
|
||||||
|
## Passing the heuristic
|
||||||
|
|
||||||
|
In our case, when we're using C++, we can just template the function that will
|
||||||
|
calculate the shortest path and pass the heuristic as a parameter.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
Actual implementation is very easy once we have the Dijkstra's algorithm:
|
||||||
|
```cpp
|
||||||
|
auto astar(const graph& g, const vertex_t& source, const auto& h)
|
||||||
|
-> std::vector<std::vector<int>> {
|
||||||
|
// make sure that ‹source› exists
|
||||||
|
assert(g.has(source));
|
||||||
|
|
||||||
|
// initialize the distances
|
||||||
|
std::vector<std::vector<int>> distances(
|
||||||
|
g.height(), std::vector(g.width(), graph::unreachable()));
|
||||||
|
|
||||||
|
// initialize the visited
|
||||||
|
std::vector<std::vector<bool>> visited(g.height(),
|
||||||
|
std::vector(g.width(), false));
|
||||||
|
|
||||||
|
// ‹source› destination denotes the beginning where the cost is 0
|
||||||
|
auto [sx, sy] = source;
|
||||||
|
distances[sy][sx] = 0;
|
||||||
|
|
||||||
|
pqueue_t priority_queue{std::make_pair(0 + h(source), source)};
|
||||||
|
std::optional<pqueue_item_t> item{};
|
||||||
|
while ((item = popq(priority_queue))) {
|
||||||
|
auto [cost, u] = *item;
|
||||||
|
auto [x, y] = u;
|
||||||
|
|
||||||
|
// we have already found the shortest path
|
||||||
|
if (visited[y][x]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
visited[y][x] = true;
|
||||||
|
|
||||||
|
for (const auto& [dx, dy] : DIRECTIONS) {
|
||||||
|
auto v = std::make_pair(x + dx, y + dy);
|
||||||
|
auto cost = g.cost(u, v);
|
||||||
|
|
||||||
|
// if we can move to the cell and it's better, relax¹ it and update queue
|
||||||
|
if (cost != graph::unreachable() &&
|
||||||
|
distances[y][x] + cost < distances[y + dy][x + dx]) {
|
||||||
|
distances[y + dy][x + dx] = distances[y][x] + cost;
|
||||||
|
pushq(priority_queue,
|
||||||
|
std::make_pair(distances[y + dy][x + dx] + h(v), v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return distances;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running on our map
|
||||||
|
|
||||||
|
For this algorithm I will also show the example of a call:
|
||||||
|
```cpp
|
||||||
|
distances = astar(g, std::make_pair(1, 9), [](const auto& u) {
|
||||||
|
auto [x, y] = u;
|
||||||
|
return std::abs(1 - x) + std::abs(7 - y);
|
||||||
|
});
|
||||||
|
std::cout << "[A*] Cost: " << distances[7][1] << "\n";
|
||||||
|
```
|
||||||
|
|
||||||
|
First argument to the function is the graph itself. Second argument is the
|
||||||
|
source vertex where we start. And finally the lambda returns
|
||||||
|
_Manhattan distance_ to the goal.
|
||||||
|
|
||||||
|
And we get the following result:
|
||||||
|
```
|
||||||
|
Normal cost: 1
|
||||||
|
Vortex cost: 5
|
||||||
|
Graph:
|
||||||
|
#############
|
||||||
|
#..#..*.*.**#
|
||||||
|
##***.....**#
|
||||||
|
#..########.#
|
||||||
|
#...###...#.#
|
||||||
|
#..#...##.#.#
|
||||||
|
#..#.*.#..#.#
|
||||||
|
#D...#....#.#
|
||||||
|
########*.*.#
|
||||||
|
#S..........#
|
||||||
|
#############
|
||||||
|
[Finite BF] Cost: 22
|
||||||
|
[Bellman-Ford] Cost: 22
|
||||||
|
[Dijkstra] Cost: 22
|
||||||
|
[A*] Cost: 22
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comparison
|
||||||
|
|
||||||
|
Now you may wonder how does it compare to the previous algorithms. Supposedly it
|
||||||
|
should be faster. Let's add counters and debugging output when we update
|
||||||
|
distance to our goal. And now if we run our code, we get the following output:
|
||||||
|
```
|
||||||
|
Normal cost: 1
|
||||||
|
Vortex cost: 5
|
||||||
|
Graph:
|
||||||
|
#############
|
||||||
|
#..#..*.*.**#
|
||||||
|
##***.....**#
|
||||||
|
#..########.#
|
||||||
|
#...###...#.#
|
||||||
|
#..#...##.#.#
|
||||||
|
#..#.*.#..#.#
|
||||||
|
#D...#....#.#
|
||||||
|
########*.*.#
|
||||||
|
#S..........#
|
||||||
|
#############
|
||||||
|
Relaxing path to goal in 40. relaxation
|
||||||
|
Relaxing path to goal in 68. relaxation
|
||||||
|
Relaxing path to goal in 89. relaxation
|
||||||
|
[Finite BF] Cost: 22
|
||||||
|
Relaxing path to goal in 40. relaxation
|
||||||
|
Relaxing path to goal in 68. relaxation
|
||||||
|
Relaxing path to goal in 89. relaxation
|
||||||
|
[Bellman-Ford] Cost: 22
|
||||||
|
Relaxing path to goal in 41. iteration
|
||||||
|
[Dijkstra] Cost: 22
|
||||||
|
Relaxing path to goal in 31. iteration
|
||||||
|
[A*] Cost: 22
|
||||||
|
```
|
||||||
|
|
||||||
|
From the output we can easily deduce that for both brute-force and Bellman-Ford,
|
||||||
|
which are in our case identical, we actually relax three times and for the last
|
||||||
|
time in the 89th iteration.
|
||||||
|
|
||||||
|
Dijkstra's algorithm manages to find the shortest path to our goal already in
|
||||||
|
the 41st iteration.
|
||||||
|
|
||||||
|
And finally after introducing some heuristic, we could find the shortest path
|
||||||
|
in the 31st iteration.
|
||||||
|
|
||||||
|
:::danger
|
||||||
|
|
||||||
|
Please keep in mind that choosing bad heuristic can actually lead to worse
|
||||||
|
results than using no heuristic at all.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
And there we have it. We have made our way from the brute-force algorithm all
|
||||||
|
the way to more optimal ones. Hopefully we could notice how the small
|
||||||
|
improvements of the already existing algorithms made them much better.
|
171
algorithms/11-paths/2024-01-01-bf-to-astar/index.md
Normal file
171
algorithms/11-paths/2024-01-01-bf-to-astar/index.md
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
---
|
||||||
|
id: index
|
||||||
|
slug: /paths/bf-to-astar
|
||||||
|
title: From BF to A*
|
||||||
|
description: |
|
||||||
|
Figuring out shortest-path problem from the BF to the A* algorithm.
|
||||||
|
tags:
|
||||||
|
- cpp
|
||||||
|
- brute force
|
||||||
|
- bellman ford
|
||||||
|
- dynamic programming
|
||||||
|
- dijkstra
|
||||||
|
- greedy
|
||||||
|
- a star
|
||||||
|
last_update:
|
||||||
|
date: 2024-01-01
|
||||||
|
---
|
||||||
|
|
||||||
|
## Intro
|
||||||
|
|
||||||
|
We will delve into the details and ideas of the most common path-finding
|
||||||
|
algorithms. For the purpose of demonstrating some “features” of the improved
|
||||||
|
algorithms, we will use a 2D map with some rules that will allow us to show cons
|
||||||
|
and pros of the shown algorithms.
|
||||||
|
|
||||||
|
Let's have a look at the example map:
|
||||||
|
```
|
||||||
|
#############
|
||||||
|
#..#..*.*.**#
|
||||||
|
##***.....**#
|
||||||
|
#..########.#
|
||||||
|
#...###...#.#
|
||||||
|
#..#...##.#.#
|
||||||
|
#..#.*.#..#.#
|
||||||
|
#....#....#.#
|
||||||
|
########*.*.#
|
||||||
|
#...........#
|
||||||
|
#############
|
||||||
|
```
|
||||||
|
|
||||||
|
We can see three different kinds of cells:
|
||||||
|
1. `#` which represent walls, that cannot be entered at all
|
||||||
|
2. `*` which represent vortices that can be entered at the cost of 5 coins
|
||||||
|
3. `.` which represent normal cells that can be entered for 1 coin (which is the
|
||||||
|
base price of moving around the map)
|
||||||
|
|
||||||
|
Let's dissect a specific position on the map to get a better grasp of the rules:
|
||||||
|
```
|
||||||
|
.
|
||||||
|
#S*
|
||||||
|
.
|
||||||
|
```
|
||||||
|
We are standing in the cell marked with `S` and we have the following options
|
||||||
|
* move to the north (`.`) with the cost of 1 coin,
|
||||||
|
* move to the west (`#`) **is not** allowed because of the wall,
|
||||||
|
* move to the east (`*`) is allowed with the cost of 5 coins, and finally
|
||||||
|
* move to the south (`.`) with the cost of 1 coin.
|
||||||
|
|
||||||
|
:::info
|
||||||
|
|
||||||
|
Further on I will follow the same scheme for marking cells with an addition of
|
||||||
|
`D` to denote the _destination_ to which we will be finding the shortest path.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Boilerplate
|
||||||
|
|
||||||
|
For working with this map I have prepared a basic structure for the graph in C++
|
||||||
|
that will abstract some of the internal workings of our map, namely:
|
||||||
|
* remembers the costs of moving around
|
||||||
|
* provides a simple function that returns price for moving **directly** between
|
||||||
|
two positions on the map
|
||||||
|
* allows us to print the map out, just in case we'd need some adjustments to be
|
||||||
|
made
|
||||||
|
|
||||||
|
We can see the `graph` header here:
|
||||||
|
```cpp
|
||||||
|
#ifndef _GRAPH_HPP
|
||||||
|
#define _GRAPH_HPP
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
#include <ostream>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using vertex_t = std::pair<int, int>;
|
||||||
|
|
||||||
|
struct graph {
|
||||||
|
graph(const std::vector<std::vector<char>>& map)
|
||||||
|
: map(map),
|
||||||
|
_height(static_cast<int>(map.size())),
|
||||||
|
_width(map.empty() ? 0 : static_cast<int>(map[0].size())) {}
|
||||||
|
|
||||||
|
static auto unreachable() -> int { return UNREACHABLE; }
|
||||||
|
static auto normal_cost() -> int { return NORMAL_COST; }
|
||||||
|
static auto vortex_cost() -> int { return VORTEX_COST; }
|
||||||
|
|
||||||
|
auto cost(const vertex_t& u, const vertex_t& v) const -> int {
|
||||||
|
auto [ux, uy] = u;
|
||||||
|
auto [vx, vy] = v;
|
||||||
|
|
||||||
|
auto hd = std::abs(ux - vx) + std::abs(uy - vy);
|
||||||
|
switch (hd) {
|
||||||
|
// ‹u = v›; staying on the same cell
|
||||||
|
case 0:
|
||||||
|
return 0;
|
||||||
|
// ‹u› and ‹v› are neighbours
|
||||||
|
case 1:
|
||||||
|
break;
|
||||||
|
// ‹u› and ‹v› are not neighbouring cells
|
||||||
|
default:
|
||||||
|
return UNREACHABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// boundary check
|
||||||
|
if (vy < 0 || vy >= _height || vx < 0 || vx >= _width) {
|
||||||
|
return UNREACHABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (map[vy][vx]) {
|
||||||
|
case '#':
|
||||||
|
return UNREACHABLE;
|
||||||
|
case '*':
|
||||||
|
return VORTEX_COST;
|
||||||
|
default:
|
||||||
|
return NORMAL_COST;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto width() const -> int { return _width; }
|
||||||
|
auto height() const -> int { return _height; }
|
||||||
|
auto has(const vertex_t& v) const -> bool {
|
||||||
|
auto [x, y] = v;
|
||||||
|
return (0 <= y && y < _height) && (0 <= x && x < _width);
|
||||||
|
}
|
||||||
|
|
||||||
|
friend std::ostream& operator<<(std::ostream& os, const graph& g);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<std::vector<char>> map;
|
||||||
|
int _height, _width;
|
||||||
|
|
||||||
|
const static int UNREACHABLE = std::numeric_limits<int>::max();
|
||||||
|
// XXX: modify here to change the price of entering the vortex
|
||||||
|
const static int VORTEX_COST = 5;
|
||||||
|
const static int NORMAL_COST = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::ostream& operator<<(std::ostream& os, const graph& g) {
|
||||||
|
for (const auto& row : g.map) {
|
||||||
|
for (const char cell : row) {
|
||||||
|
os << cell;
|
||||||
|
}
|
||||||
|
os << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* _GRAPH_HPP */
|
||||||
|
```
|
||||||
|
|
||||||
|
:::info Source code
|
||||||
|
|
||||||
|
You can find all the source code referenced in this series
|
||||||
|
[here](pathname:///files/algorithms/paths/bf-to-astar.tar.gz).
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
Let's finally start with some algorithms!
|
0
static/files/algorithms/paths/bf-to-astar/.archive
Normal file
0
static/files/algorithms/paths/bf-to-astar/.archive
Normal file
59
static/files/algorithms/paths/bf-to-astar/astar.hpp
Normal file
59
static/files/algorithms/paths/bf-to-astar/astar.hpp
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
#ifndef _ASTAR_HPP
|
||||||
|
#define _ASTAR_HPP
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cassert>
|
||||||
|
#include <functional>
|
||||||
|
#include <optional>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "graph.hpp"
|
||||||
|
|
||||||
|
auto astar(const graph& g, const vertex_t& source, const auto& h)
|
||||||
|
-> std::vector<std::vector<int>> {
|
||||||
|
// make sure that ‹source› exists
|
||||||
|
assert(g.has(source));
|
||||||
|
|
||||||
|
// initialize the distances
|
||||||
|
std::vector<std::vector<int>> distances(
|
||||||
|
g.height(), std::vector(g.width(), graph::unreachable()));
|
||||||
|
|
||||||
|
// initialize the visited
|
||||||
|
std::vector<std::vector<bool>> visited(g.height(),
|
||||||
|
std::vector(g.width(), false));
|
||||||
|
|
||||||
|
// ‹source› destination denotes the beginning where the cost is 0
|
||||||
|
auto [sx, sy] = source;
|
||||||
|
distances[sy][sx] = 0;
|
||||||
|
|
||||||
|
pqueue_t priority_queue{std::make_pair(0 + h(source), source)};
|
||||||
|
std::optional<pqueue_item_t> item{};
|
||||||
|
while ((item = popq(priority_queue))) {
|
||||||
|
auto [cost, u] = *item;
|
||||||
|
auto [x, y] = u;
|
||||||
|
|
||||||
|
// we have already found the shortest path
|
||||||
|
if (visited[y][x]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
visited[y][x] = true;
|
||||||
|
|
||||||
|
for (const auto& [dx, dy] : DIRECTIONS) {
|
||||||
|
auto v = std::make_pair(x + dx, y + dy);
|
||||||
|
auto cost = g.cost(u, v);
|
||||||
|
|
||||||
|
// if we can move to the cell and it's better, relax¹ it and update queue
|
||||||
|
if (cost != graph::unreachable() &&
|
||||||
|
distances[y][x] + cost < distances[y + dy][x + dx]) {
|
||||||
|
distances[y + dy][x + dx] = distances[y][x] + cost;
|
||||||
|
pushq(priority_queue,
|
||||||
|
std::make_pair(distances[y + dy][x + dx] + h(v), v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return distances;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* _ASTAR_HPP */
|
136
static/files/algorithms/paths/bf-to-astar/bf.hpp
Normal file
136
static/files/algorithms/paths/bf-to-astar/bf.hpp
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
#ifndef _BF_HPP
|
||||||
|
#define _BF_HPP
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <iostream>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "graph.hpp"
|
||||||
|
|
||||||
|
static auto _check_vertex(const graph& g,
|
||||||
|
std::vector<std::vector<int>>& distances, int v,
|
||||||
|
bool check_only = false) -> bool {
|
||||||
|
bool improvement_found = false;
|
||||||
|
|
||||||
|
// unpack the vertex coordinates
|
||||||
|
int y = v / g.width();
|
||||||
|
int x = v % g.width();
|
||||||
|
|
||||||
|
// skip the cells we cannot reach
|
||||||
|
if (distances[y][x] == graph::unreachable()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// go through the neighbours
|
||||||
|
auto u = std::make_pair(x, y);
|
||||||
|
for (const auto& [dx, dy] : DIRECTIONS) {
|
||||||
|
auto v = std::make_pair(x + dx, y + dy);
|
||||||
|
auto cost = g.cost(u, v);
|
||||||
|
|
||||||
|
// if we can move to the cell and it's better, relax¹ it
|
||||||
|
if (cost != graph::unreachable() &&
|
||||||
|
distances[y][x] + cost < distances[y + dy][x + dx]) {
|
||||||
|
if (check_only) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
distances[y + dy][x + dx] = distances[y][x] + cost;
|
||||||
|
improvement_found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return improvement_found;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto bf(const graph& g, const vertex_t& source, const vertex_t& destination)
|
||||||
|
-> int {
|
||||||
|
// ‹source› must be within the bounds
|
||||||
|
assert(g.has(source));
|
||||||
|
|
||||||
|
// ‹destination› must be within the bounds
|
||||||
|
assert(g.has(destination));
|
||||||
|
|
||||||
|
// we need to initialize the distances
|
||||||
|
std::vector<std::vector<int>> distances(
|
||||||
|
g.height(), std::vector(g.width(), graph::unreachable()));
|
||||||
|
|
||||||
|
// ‹source› destination denotes the beginning where the cost is 0
|
||||||
|
auto [sx, sy] = source;
|
||||||
|
distances[sy][sx] = 0;
|
||||||
|
|
||||||
|
// now we need to improve the paths as long as possible
|
||||||
|
bool improvement_found;
|
||||||
|
do {
|
||||||
|
// reset the flag at the beginning
|
||||||
|
improvement_found = false;
|
||||||
|
|
||||||
|
// go through all of the vertices
|
||||||
|
for (int v = g.height() * g.width() - 1; v >= 0; --v) {
|
||||||
|
improvement_found = _check_vertex(g, distances, v) || improvement_found;
|
||||||
|
}
|
||||||
|
} while (improvement_found);
|
||||||
|
|
||||||
|
return distances[destination.second][destination.first];
|
||||||
|
}
|
||||||
|
|
||||||
|
auto bf_finite(const graph& g, const vertex_t& source,
|
||||||
|
const vertex_t& destination) -> int {
|
||||||
|
// ‹source› must be within the bounds
|
||||||
|
assert(g.has(source));
|
||||||
|
|
||||||
|
// ‹destination› must be within the bounds
|
||||||
|
assert(g.has(destination));
|
||||||
|
|
||||||
|
// we need to initialize the distances
|
||||||
|
std::vector<std::vector<int>> distances(
|
||||||
|
g.height(), std::vector(g.width(), graph::unreachable()));
|
||||||
|
|
||||||
|
// ‹source› destination denotes the beginning where the cost is 0
|
||||||
|
auto [sx, sy] = source;
|
||||||
|
distances[sy][sx] = 0;
|
||||||
|
|
||||||
|
// now we only iterate as many times as cells that we have
|
||||||
|
for (int i = g.height() * g.width(); i > 0; --i) {
|
||||||
|
// go through all of the vertices
|
||||||
|
for (int v = g.height() * g.width() - 1; v >= 0; --v) {
|
||||||
|
_check_vertex(g, distances, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return distances[destination.second][destination.first];
|
||||||
|
}
|
||||||
|
|
||||||
|
auto bellman_ford(const graph& g, const vertex_t& source)
|
||||||
|
-> std::vector<std::vector<int>> {
|
||||||
|
// ‹source› must be within the bounds
|
||||||
|
assert(g.has(source));
|
||||||
|
|
||||||
|
// we need to initialize the distances
|
||||||
|
std::vector<std::vector<int>> distances(
|
||||||
|
g.height(), std::vector(g.width(), graph::unreachable()));
|
||||||
|
|
||||||
|
// ‹source› destination denotes the beginning where the cost is 0
|
||||||
|
auto [sx, sy] = source;
|
||||||
|
distances[sy][sx] = 0;
|
||||||
|
|
||||||
|
// now we only iterate as many times as cells that we have
|
||||||
|
for (int i = g.height() * g.width(); i > 0; --i) {
|
||||||
|
// go through all of the vertices
|
||||||
|
for (int v = g.height() * g.width() - 1; v >= 0; --v) {
|
||||||
|
_check_vertex(g, distances, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// now we check for the negative loops
|
||||||
|
for (int v = g.height() * g.width() - 1; v >= 0; --v) {
|
||||||
|
if (_check_vertex(g, distances, v, true)) {
|
||||||
|
std::cerr << "[Bellman-Ford] Found a negative loop\n";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return distances;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* _BF_HPP */
|
58
static/files/algorithms/paths/bf-to-astar/dijkstra.hpp
Normal file
58
static/files/algorithms/paths/bf-to-astar/dijkstra.hpp
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
#ifndef _DIJKSTRA_HPP
|
||||||
|
#define _DIJKSTRA_HPP
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cassert>
|
||||||
|
#include <functional>
|
||||||
|
#include <optional>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "graph.hpp"
|
||||||
|
|
||||||
|
auto dijkstra(const graph& g, const vertex_t& source)
|
||||||
|
-> std::vector<std::vector<int>> {
|
||||||
|
// make sure that ‹source› exists
|
||||||
|
assert(g.has(source));
|
||||||
|
|
||||||
|
// initialize the distances
|
||||||
|
std::vector<std::vector<int>> distances(
|
||||||
|
g.height(), std::vector(g.width(), graph::unreachable()));
|
||||||
|
|
||||||
|
// initialize the visited
|
||||||
|
std::vector<std::vector<bool>> visited(g.height(),
|
||||||
|
std::vector(g.width(), false));
|
||||||
|
|
||||||
|
// ‹source› destination denotes the beginning where the cost is 0
|
||||||
|
auto [sx, sy] = source;
|
||||||
|
distances[sy][sx] = 0;
|
||||||
|
|
||||||
|
pqueue_t priority_queue{std::make_pair(0, source)};
|
||||||
|
std::optional<pqueue_item_t> item{};
|
||||||
|
while ((item = popq(priority_queue))) {
|
||||||
|
auto [cost, u] = *item;
|
||||||
|
auto [x, y] = u;
|
||||||
|
|
||||||
|
// we have already found the shortest path
|
||||||
|
if (visited[y][x]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
visited[y][x] = true;
|
||||||
|
|
||||||
|
for (const auto& [dx, dy] : DIRECTIONS) {
|
||||||
|
auto v = std::make_pair(x + dx, y + dy);
|
||||||
|
auto cost = g.cost(u, v);
|
||||||
|
|
||||||
|
// if we can move to the cell and it's better, relax¹ it and update queue
|
||||||
|
if (cost != graph::unreachable() &&
|
||||||
|
distances[y][x] + cost < distances[y + dy][x + dx]) {
|
||||||
|
distances[y + dy][x + dx] = distances[y][x] + cost;
|
||||||
|
pushq(priority_queue, std::make_pair(distances[y + dy][x + dx], v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return distances;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* _DIJKSTRA_HPP */
|
108
static/files/algorithms/paths/bf-to-astar/graph.hpp
Normal file
108
static/files/algorithms/paths/bf-to-astar/graph.hpp
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
#ifndef _GRAPH_HPP
|
||||||
|
#define _GRAPH_HPP
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
#include <ostream>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using vertex_t = std::pair<int, int>;
|
||||||
|
|
||||||
|
struct graph {
|
||||||
|
graph(const std::vector<std::vector<char>>& map)
|
||||||
|
: map(map),
|
||||||
|
_height(static_cast<int>(map.size())),
|
||||||
|
_width(map.empty() ? 0 : static_cast<int>(map[0].size())) {}
|
||||||
|
|
||||||
|
static auto unreachable() -> int { return UNREACHABLE; }
|
||||||
|
static auto normal_cost() -> int { return NORMAL_COST; }
|
||||||
|
static auto vortex_cost() -> int { return VORTEX_COST; }
|
||||||
|
|
||||||
|
auto cost(const vertex_t& u, const vertex_t& v) const -> int {
|
||||||
|
auto [ux, uy] = u;
|
||||||
|
auto [vx, vy] = v;
|
||||||
|
|
||||||
|
auto md = std::abs(ux - vx) + std::abs(uy - vy);
|
||||||
|
switch (md) {
|
||||||
|
// ‹u = v›; staying on the same cell
|
||||||
|
case 0:
|
||||||
|
return 0;
|
||||||
|
// ‹u› and ‹v› are neighbours
|
||||||
|
case 1:
|
||||||
|
break;
|
||||||
|
// ‹u› and ‹v› are not neighbouring cells
|
||||||
|
default:
|
||||||
|
return UNREACHABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// boundary check
|
||||||
|
if (vy < 0 || vy >= _height || vx < 0 || vx >= _width) {
|
||||||
|
return UNREACHABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (map[vy][vx]) {
|
||||||
|
case '#':
|
||||||
|
return UNREACHABLE;
|
||||||
|
case '*':
|
||||||
|
return VORTEX_COST;
|
||||||
|
default:
|
||||||
|
return NORMAL_COST;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto width() const -> int { return _width; }
|
||||||
|
auto height() const -> int { return _height; }
|
||||||
|
auto has(const vertex_t& v) const -> bool {
|
||||||
|
auto [x, y] = v;
|
||||||
|
return (0 <= y && y < _height) && (0 <= x && x < _width);
|
||||||
|
}
|
||||||
|
|
||||||
|
friend std::ostream& operator<<(std::ostream& os, const graph& g);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<std::vector<char>> map;
|
||||||
|
int _height, _width;
|
||||||
|
|
||||||
|
const static int UNREACHABLE = std::numeric_limits<int>::max();
|
||||||
|
// XXX: modify here to change the price of entering the vortex
|
||||||
|
const static int VORTEX_COST = 5;
|
||||||
|
const static int NORMAL_COST = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::ostream& operator<<(std::ostream& os, const graph& g) {
|
||||||
|
for (const auto& row : g.map) {
|
||||||
|
for (const char cell : row) {
|
||||||
|
os << cell;
|
||||||
|
}
|
||||||
|
os << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
const static std::vector<vertex_t> DIRECTIONS =
|
||||||
|
std::vector{std::make_pair(0, 1), std::make_pair(0, -1),
|
||||||
|
std::make_pair(1, 0), std::make_pair(-1, 0)};
|
||||||
|
|
||||||
|
using pqueue_item_t = std::pair<int, vertex_t>;
|
||||||
|
using pqueue_t = std::vector<pqueue_item_t>;
|
||||||
|
|
||||||
|
auto pushq(pqueue_t& q, pqueue_item_t v) -> void {
|
||||||
|
q.push_back(v);
|
||||||
|
std::push_heap(q.begin(), q.end(), std::greater<>{});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto popq(pqueue_t& q) -> std::optional<pqueue_item_t> {
|
||||||
|
if (q.empty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pop_heap(q.begin(), q.end(), std::greater<>{});
|
||||||
|
pqueue_item_t top = q.back();
|
||||||
|
q.pop_back();
|
||||||
|
|
||||||
|
return std::make_optional(top);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* _GRAPH_HPP */
|
50
static/files/algorithms/paths/bf-to-astar/main.cpp
Normal file
50
static/files/algorithms/paths/bf-to-astar/main.cpp
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "astar.hpp"
|
||||||
|
#include "bf.hpp"
|
||||||
|
#include "dijkstra.hpp"
|
||||||
|
#include "graph.hpp"
|
||||||
|
|
||||||
|
auto line_to_vector(const std::string& l) -> std::vector<char> {
|
||||||
|
return std::vector(l.begin(), l.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto main() -> int {
|
||||||
|
graph g{std::vector{
|
||||||
|
line_to_vector(std::string("#############")),
|
||||||
|
line_to_vector(std::string("#..#..*.*.**#")),
|
||||||
|
line_to_vector(std::string("##***.....**#")),
|
||||||
|
line_to_vector(std::string("#..########.#")),
|
||||||
|
line_to_vector(std::string("#...###...#.#")),
|
||||||
|
line_to_vector(std::string("#..#...##.#.#")),
|
||||||
|
line_to_vector(std::string("#..#.*.#..#.#")),
|
||||||
|
line_to_vector(std::string("#....#....#.#")),
|
||||||
|
line_to_vector(std::string("########*.*.#")),
|
||||||
|
line_to_vector(std::string("#...........#")),
|
||||||
|
line_to_vector(std::string("#############")),
|
||||||
|
}};
|
||||||
|
std::cout << "Normal cost: " << g.normal_cost() << "\n";
|
||||||
|
std::cout << "Vortex cost: " << g.vortex_cost() << "\n";
|
||||||
|
std::cout << "Graph:\n" << g;
|
||||||
|
|
||||||
|
// finding the distances from the bottom left corner to the 2 rows above
|
||||||
|
auto cost = bf_finite(g, std::make_pair(1, 9), std::make_pair(1, 7));
|
||||||
|
std::cout << "[Finite BF] Cost: " << cost << "\n";
|
||||||
|
|
||||||
|
auto distances = bellman_ford(g, std::make_pair(1, 9));
|
||||||
|
std::cout << "[Bellman-Ford] Cost: " << distances[7][1] << "\n";
|
||||||
|
|
||||||
|
distances = dijkstra(g, std::make_pair(1, 9));
|
||||||
|
std::cout << "[Dijkstra] Cost: " << distances[7][1] << "\n";
|
||||||
|
|
||||||
|
distances = astar(g, std::make_pair(1, 9), [](const auto& u) {
|
||||||
|
auto [x, y] = u;
|
||||||
|
return std::abs(1 - x) + std::abs(7 - y);
|
||||||
|
});
|
||||||
|
std::cout << "[A*] Cost: " << distances[7][1] << "\n";
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
8
static/files/algorithms/paths/bf-to-astar/makefile
Normal file
8
static/files/algorithms/paths/bf-to-astar/makefile
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
CXX=c++
|
||||||
|
CXXFLAGS=-std=c++20 -Wall -Wextra -g
|
||||||
|
|
||||||
|
all: format
|
||||||
|
$(CXX) $(CXXFLAGS) main.cpp -o main
|
||||||
|
|
||||||
|
format:
|
||||||
|
clang-format -i -style=google *.hpp *.cpp
|
BIN
static/img/algorithms/paths/bf-to-astar/bellman-ford-meme.jpg
Normal file
BIN
static/img/algorithms/paths/bf-to-astar/bellman-ford-meme.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
BIN
static/img/algorithms/paths/bf-to-astar/dijkstra-meme.jpg
Normal file
BIN
static/img/algorithms/paths/bf-to-astar/dijkstra-meme.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 84 KiB |
Loading…
Reference in a new issue