- Published on
Shortest Path Algorithms
- Authors
- Name
- Ramtin Tajbakhsh
- Dijkstra's Algorithm
- Practice Problems
- Further Reading
- Floyd-Warshall Algorithm
- Practice Problems
- Further Reading
- Bellman-Ford Algorithm
- Practice Problems
- Further Reading
Dijkstra's Algorithm
Dijkstra's algorithm is the most famous algorithm to find the shortest path between the source node and all the other nodes in a graph. This algorithm assumes non-negative weights and uses a greedy method to find the shortest path. It's generally pretty efficient and it's usually the go-to method in DSA problems unless negative weights or negative cycles exist in the graph.
Here is a breakdown of the algorithm for a graph with n
nodes:
- Create Distance Array: Create an array
dist
wheredist[i]
is the tentative shortest path from source to the node in the graph - Initialize Distance Array: Initialize
dist[source]
to zero anddist[i]
to infinity for all other nodes. Here infinity means that either a path doesn't exist or it hasn't been computed yet. - Maintain a Set of Visited Nodes: By maintaining a set of visited nodes, we can stop the algorithm once we have visited all the nodes we can access from source
- Process Each Node: In each iteration, we choose the node that has the smallest distance in the
dist
array. Then, we consider all the unvisited neighbors of the chosen node and calculate their distance from the current node. If the newly calculated distance is less thandist[neighbor]
, we update the distance to that node. - Mark Current Node as Visited: After the current is processed, we mark it as visited.
- Termination: We continue this process until all the nodes have been visited or no other node is accessible from source It's important to note that since in every iteration we choose the node with the smallest distance, using a
priority_queue
would greatly improve the efficiency of the algorithm.
Here is the implementation of this algorithm in C++ with priority_queue
:
/*
Input:
number of nodes (n)
adjacency list of the graph with weights
source node
Output:
Vector where output[i] is the shortest path from source to node i
*/
vector<int> DijkstraShortestPath(int n, vector<vector<pair<int, int>>> adj, int source)
{
// Initialize dist array with INT_MAX
vector<int> dist(n, INT_MAX);
// Initialize MinHeap. Pairs are {distance, vertex}
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
dist[source] = 0; // Distance from source to source is 0 by definition
pq.push_make(make_pair(0, source));
while (!pq.empty()) {
int current = pq.top().second;
pq.pop();
for (const auto& [neighbor, weight]: adj(current)) {
int newDist = dist[current] + weight;
if (newDist < dist[neighbor]) { // If the new distance is shorter
dist[neighbor] = newDist;
pq.push(make_pair(newDist, neighbor));
}
}
}
return dist;
}
Time Complexity: where is the number of vertices and is the number of edges
Practice Problems
Further Reading
Floyd-Warshall Algorithm
Floyd-Warshall is an algorithm to find the shortest path from every node to every other node so it is an all-pairs shortest path algorithm. In this algorithm, we initialize a 2D array dist
where dist[i][j]
is the shortest path between the nodes and at each phase.
The algorithm have phases. After each phase , dist[i][j]
contains the shortest path between the and node if we are only allowed to pass through the nodes . If there is no path between two nodes, the value of dist
for them is going to be a very large number, indicating that a path doesn't exist. Also, by definition, the distance between each node and itself is 0.
Floyd-Warshall algorithm works on weighted graphs with both positive and negative weights. However, it won't produce the correct output if the graph contains a negative cycle.
Here is the implementation of the Floyd-Warshall algorithm:
/*
Input:
number of nodes (n)
adjacency list of the graph with weights
Output:
Vector where output[i][j] is the shortest path from node i to node j
*/
vector<vector<int, int>> FloydWarshall(int n, vector<vector<pair<int, int>>> adj) {
// Initialize dist array with infinity (INT_MAX)
vector<vector<int, int>> dist(n, vector<int>(n, INT_MAX));
for (int i = 0; i < n; ++i) {
dist[i][i] = 0; // distance between node to itself
for (int j = 0; j < adj[i].size(); ++j) {
dist[i][adj[i][j].first] = adj[i][j].second; // the initial weights
}
}
for (int k = 0; k < n; ++k) { // the k phases
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
// This condition is needed to account for negative weights
if (dist[i][k] < INT_MAX && dist[k][j] < INT_MAX)
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
}
}
return dist;
}
The time complexity of this algorithm is obviously or since is the number of vertices in the input graph.
Practice Problems
- Design Graph With Shortest Path Calculator (Can also be solved with Dijkstra)
- Greg and Graph
Further Reading
Bellman-Ford Algorithm
Bellman-Ford is another single-source shortest path algorithm, like Dijkstra, but it works on graphs with negative weights and it can be used to detect negative cycles as well.
This algorithm uses the fact that in a graph with nodes, the shortest path between any two nodes cannot be longer than edges. So the algorithm iterates through the edges times and every time it checks if the previously calculated shortest paths can be improved if we include another edge.
To detect negative cycles, we can do the iteration one last time ( time) and if any of the shortest paths improve, it means there is a negative cycle in the graph.
Here is a sample implementation of this algorithm in C++. Note that for the Bellman-Ford algorithm, it is much simpler to have a list of edges instead of the adjacency list:
/*
Input:
number of nodes (n)
a list of all the edges in this format: [from, to, weight]
source node
Output:
Vector where output[i] is the shortest path from the source node to node i
*/
vector<int> BellmanFord(int n, vector<vector<int>>& edges, int source) {
vector<int> dist(n, INT_MAX);
dist[source] = 0; // Distance from source to itself
for (int i = 0; i < n-1; ++i) {
for (auto& edge: edges) {
if (dist[edge[0]] < INT_MAX) {
dist[edge[1]] = min(dist[edge[1]], dist[edge[0]] + dist[edge[2]]);
}
}
}
// One last iteration to detect negative cycles
for (auto& edge: edges) {
if (dist[edge[0]] < INT_MAX) {
if (dist[edge[0]] + dist[edge[2]] < dist[edge[1]])
cout << "Negative Cycle Detected";
}
}
// dist contains the shortest path from the source to all the other nodes
return dist;
}
The time complexity of this algorithm is where is the number of vertices and is the number of edges in the input graph.