visit
It turns out that well-known algorithms like BFS, , , and are essentially variations of the same algorithm.
You can find all the working code for these algorithms on my GitHub repository . I recommend experimenting with the code while reading this article since practical experience enhances learning more than just theoretical understanding.
( 0 ) - ( 1 ) - ( 2 ) - ( 3 ) - ( 4 )
| | | | |
( 5 ) - ( 6 ) - ( 7 ) - ( 8 ) - ( 9 )
| | | | |
( 10 ) - ( 11 ) - ( 12 ) - ( 13 ) - ( 14 )
| | | | |
( 15 ) - ( 16 ) - ( 17 ) - ( 18 ) - ( 19 )
| | | | |
( 20 ) - ( 21 ) - ( 22 ) - ( 23 ) - ( 24 )
In this article, I do not cover the basic concepts, hoping that you are already familiar with them. If the terminology mentioned above seems daunting to you, you should probably learn the basics as well. However, playing around with these code examples can still be exciting.
(0, 0) - (0, 1) - (0, 2) - (0, 3) - (0, 4)
| | | | |
(1, 0) - (1, 1) - (1, 2) - (1, 3) - (1, 4)
| | | | |
(2, 0) - (2, 1) - (2, 2) - (2, 3) - (2, 4)
| | | | |
(3, 0) - (3, 1) - (3, 2) - (3, 3) - (3, 4)
| | | | |
(4, 0) - (4, 1) - (4, 2) - (4, 3) - (4, 4)
(0, 0) -1- (0, 1) -1- (0, 2) -1- (0, 3) -2- (0, 4)
| | | | |
2 1 1 2 2
| | | | |
(1, 0) -2- (1, 1) -1- (1, 2) -2- (1, 3) -1- (1, 4)
| | | | |
2 1 1 1 1
| | | | |
(2, 0) -1- (2, 1) -1- (2, 2) -1- (2, 3) -2- (2, 4)
| | | | |
2 1 1 1 2
| | | | |
(3, 0) -2- (3, 1) -2- (3, 2) -1- (3, 3) -2- (3, 4)
| | | | |
2 1 1 2 2
| | | | |
(4, 0) -2- (4, 1) -1- (4, 2) -2- (4, 3) -2- (4, 4)
class GraphNode
{
public:
int X;
int Y;
};
class Graph
{
public:
vector<vector<pair<int, int>>> Edges;
vector<GraphNode> Nodes;
};
int toNode = graph.Edges[fromNode][neighbourIndex].first;
int weight = graph.Edges[fromNode][neighbourIndex].second;
class pathFindingBase
{
public:
virtual void insert(int node) = 0;
virtual int getFirst() = 0;
virtual bool isEmpty() = 0;
};
const int INF = 1000000;
const int WHITE = 0;
const int GREY = 1;
const int BLACK = 2;
/// <summary>
/// Universal algorithm to apply Path search using BFS, DFS, Dijkstra, A-Star.
/// </summary>
vector<int> FindPath(Graph& graph, int start, int finish, int finishX, int finishY)
{
int verticesNumber = graph.Nodes.size();
vector<int> nodeColor(verticesNumber, WHITE); // All the nodes are White colored initially
vector<int> shortestPath(verticesNumber, INF); // Current shortest path found from Start to i is some large/INFinite number from the beginning.
vector<int> previousVertex(verticesNumber, -1); // Index of the vertex/node that is predecessor of i-th vertex in a shortest path to it.
// We should use pointers here because we want to pass the pointer to a data-structure
// so it may receive all the updates automatically on every step.
shared_ptr<vector<int>> ptrShortestPath = make_shared<vector<int>>(shortestPath);
shared_ptr<Graph> ptrGraph = make_shared<Graph>(graph);
//////////////////////////////////////////////////
// TODO
// UNCOMMENT DATA STRUCTURE YOU WANT TO USE:
//dfsStack customQueue;
//bfsQueue customQueue;
//dijkstraQueue customQueue(ptrShortestPath);
//aStarQueue customQueue(finishX, finishY, ptrGraph, ptrShortestPath);
// END OF TODO
/////////////////////////////////////////////////
customQueue.insert(start);
nodeColor[start] = BLACK;
ptrShortestPath->at(start) = 0;
while (!customQueue.isEmpty()) // Traverse nodes starting from start node.
{
int current = customQueue.getFirst();
if (current == finish) // If we found finish node, then let's print full path.
{
vector<int> path;
int cur = finish;
path.push_back(cur);
while (previousVertex[cur] != -1) // Recover path node by node.
{
cur = previousVertex[cur];
path.push_back(cur);
}
reverse(path.begin(), path.end()); // Since we are at the finish node, reverse list to be at start.
return path;
}
for (int neighbourIndex = 0; neighbourIndex < graph.Edges[current].size(); neighbourIndex++)
{
int to = graph.Edges[current][neighbourIndex].first;
int weight = graph.Edges[current][neighbourIndex].second;
if (nodeColor[to] == WHITE) // If node is not yet visited.
{
nodeColor[to] = GREY; // Mark node as "in progress".
customQueue.insert(to);
previousVertex[to] = current;
ptrShortestPath->at(to) = ptrShortestPath->at(current) + weight; // Calculate cost of moving to this node.
}
else // Select the most optimal route.
{
if (ptrShortestPath->at(to) > ptrShortestPath->at(current) + weight)
{
ptrShortestPath->at(to) = ptrShortestPath->at(current) + weight;
}
}
}
nodeColor[current] = BLACK;
}
return {};
}
#include <queue>
#include "pathFindingBase.h"
class bfsQueue : public pathFindingBase
{
private:
queue<int> _queue;
public:
virtual void insert(int node)
{
_queue.push(node);
}
virtual int getFirst()
{
int value = _queue.front();
_queue.pop();
return value;
}
virtual bool isEmpty()
{
return _queue.empty();
}
};
In reality, we could simply replace the custom queue interface here with the standard C++ queue provided by the STL (Standard Template Library). However, the goal here is universality. Now, you only need to uncomment the line in the main method and run this algorithm://bfsQueue customQueue; // UNCOMMENT TO USE BFS
(0, 0) - (0, 1) - (0, 2) - (0, 3) - (0, 4)
|
(1, 4)
|
(2, 4)
|
(3, 4)
|
(4, 4)
If we consider weights, the final cost of this path will be 11. However, remember that neither BFS nor DFS consider weights. Instead, they traverse all nodes in the graph, hoping to find the desired node sooner or later.
#include <stack>
#include "pathFindingBase.h"
class dfsStack : public pathFindingBase
{
private:
stack<int> _queue;
public:
virtual void insert(int node)
{
_queue.push(node);
}
virtual int getFirst()
{
int value = _queue.top();
_queue.pop();
return value;
}
virtual bool isEmpty()
{
return _queue.empty();
}
};
(0, 0)
|
(1, 0)
|
(2, 0)
|
(3, 0)
|
(4, 0) - (4, 1) - (4, 2) - (4, 3) - (4, 4)
#include <queue>
#include "pathFindingBase.h"
class dijkstraQueue : public pathFindingBase
{
private:
vector<int> _queue;
shared_ptr<vector<int>> _shortestPaths;
public:
dijkstraQueue(shared_ptr<vector<int>> shortestPaths) : _shortestPaths(shortestPaths) { }
virtual void insert(int node)
{
_queue.push_back(node);
}
virtual int getFirst()
{
int minimum = INF;
int minimumNode = -1;
for (int i = 0; i < _queue.size(); i++)
{
int to = _queue[i];
int newDistance = _shortestPaths->at(to);
if (minimum > newDistance) // Greedy selection: select node with minimum distance on every step
{
minimum = newDistance;
minimumNode = to;
}
}
if (minimumNode != -1)
{
remove(_queue.begin(), _queue.end(), minimumNode);
}
return minimumNode;
}
virtual bool isEmpty()
{
return _queue.empty();
}
};
(0, 0) -1- (0, 1)
|
1
|
(1, 1) -1- (1, 2)
|
1
|
(2, 2) -1- (2, 3)
|
1
|
(3, 3) -1- (3, 4)
|
1
|
(4, 4)
The A-Star algorithm is particularly suited for cases where a path is sought in an Euclidean space with coordinates, such as maps. This is why it is widely used in games. It not only utilizes a "blind" greedy search based on minimal weights but also considers the Euclidean distance to the goal. As a result, it is usually much more efficient than Dijkstra's algorithm in practical scenarios (refer to for more details). :
class aStarQueue : public pathFindingBase
{
private:
vector<int> _queue;
shared_ptr<vector<int>> _shortestPaths;
shared_ptr<Graph> _graph;
int _finishX;
int _finishY;
/// <summary>
/// Euclidian distance from node start to specified node id.
/// </summary>
int calcEuristic(int id)
{
return sqrt(
pow(abs(
_finishX > _graph->Nodes[id].X ?
_finishX - _graph->Nodes[id].X :
_graph->Nodes[id].X - _finishX), 2) +
pow(abs(
_finishY > _graph->Nodes[id].Y ?
_finishY - _graph->Nodes[id].Y :
_graph->Nodes[id].Y - _finishY), 2));
}
public:
aStarQueue(int finishX, int finishY, shared_ptr<Graph> graph, shared_ptr<vector<int>> shortestPaths)
:
_shortestPaths(shortestPaths),
_graph(graph)
{
_finishX = finishX;
_finishY = finishY;
}
virtual void insert(int node)
{
_queue.push_back(node);
}
virtual int getFirst()
{
int minimum = INF;
int minimumNode = -1;
for (int i = 0; i < _queue.size(); i++)
{
int to = _queue[i];
int newDistance = _shortestPaths->at(to);
int euristic = calcEuristic(to);
if (minimum > newDistance + euristic)
{
minimum = newDistance + euristic;
minimumNode = to;
}
}
if (minimumNode != -1)
{
_queue.erase(remove(_queue.begin(), _queue.end(), minimumNode), _queue.end());
}
return minimumNode;
}
virtual bool isEmpty()
{
return _queue.empty();
}
};
Also published .