0% found this document useful (0 votes)
12 views7 pages

DSA-Chapter-5.2-2024

Uploaded by

roinieva22
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
12 views7 pages

DSA-Chapter-5.2-2024

Uploaded by

roinieva22
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 7

Data Structures and Algorithms

Chapter 5.2: Dijkstra’s Algorithm

In the previous chapter, we discussed about BFS on graphs, and we found that BFS always
finds the path with the least number of edges from a node to another node. However, it
disregards the weights of the edges and therefore may not actually find the shortest path for
weighted graphs.

For example, if the edge weights represented the length of a road between cities,
imagine this scenario:
• You want to go from city A to city D.
• There’s a road connecting city A to city B, a road connecting city B to city C, and a
road connecting city C to city D, and all of those are mostly straight roads, with a
total of 40 km.
• There’s a road directly from city A to city D, but it’s full of curves and turns, with a
length of 60 km.

BFS would find the road from city A to city D the shortest path because there’s only one
edge(road) between A to D. However, it isn’t actually the shortest because of the edge
weights (road length) between the cities.
Imagine another scenario where the edge weights represented the time it takes to travel
between cities:
• You want to go from city A to city D.
• There’s a road connecting city A -> city C -> city D, with a total of 15 km, but very
heavy traffic, taking 1 hour to travel.
• There’s a road connecting city A -> city B -> city D, with a total of 20 km, with very
light traffic, taking only 30 minutes to travel.
The number of nodes between A and D are the same, but the weights (time it takes to travel)
are different.

In both scenarios, BFS would be able to give us neither the shortest (in distance) nor quickest
path. Fortunately, we can use a different algorithm that can handle those.
Dijkstra's Shortest Path
This algorithm is named after the Dutch computer scientist Edsger Dijkstra. It calculates the
shortest path from one node to all other nodes in a graph. This means that we can also use
it to find a specific node.
How does the Algorithm work?
1. We start at the node (let's call it the "source node") at which you are at. This node
has a current distance of 0, while all other nodes have a current distance of
infinity. The reason for this is that we know the distance from the source to itself
is 0, but we don't yet know the shortest distances to any other nodes.
2. We then visit all the nearest neighbors of the current node. For each one, we
calculate the distance to that node traveling through the current node. If this
distance is less than the previously known distance to that node, update the
shortest distance.
3. Once we have visited all neighbors of the current node and updated their
distances, we mark the current node as "visited." A visited node will not be
checked ever again; its distance recorded now is final and minimal.
4. The next current node to move to is the unvisited node with the lowest known
distance from the source node. If all nodes have been visited, the algorithm is
finished.
5. We repeat steps 2-4 until all the nodes in the graph have been visited.

Let’s try to implement it:


#include <iostream>
#include <vector>
#include <limits>
#include <algorithm>

const int inf = std::numeric_limits<int>::max();

struct GraphEdge {
int to;
int weight;
};

using WeightedAdjacencyList = std::vector<std::vector<GraphEdge>>;


using Visited = std::vector<bool>;
using Distances = std::vector<int>;
using PreviousNodes = std::vector<int>;
using Path = std::vector<int>;

bool hasUnvisited(const Visited& visited, const Distances& distances) {


for (int i = 0; i < visited.size(); ++i) {
if (!visited[i] && distances[i] < inf) {
return true;
}
}
return false;
}

int getLowestUnvisited(const Visited& visited, const Distances& distances) {


int idx = -1;
int lowestDistance = inf;
for (int i = 0; i < visited.size(); ++i) {
if (visited[i]) {
continue;
}
if (lowestDistance > distances[i]) {
lowestDistance = distances[i];
idx = i;
}
}
return idx;
}

Path dijkstra_list(int source, int target, const WeightedAdjacencyList& adjacen


cyList) {
Visited visited(adjacencyList.size(), false);
PreviousNodes previous(adjacencyList.size(), -1);
Distances distances(adjacencyList.size(), inf);
distances[source] = 0;

while (hasUnvisited(visited, distances)) {


int curr = getLowestUnvisited(visited, distances);
visited[curr] = true;

const std::vector<GraphEdge>& adjs = adjacencyList[curr];


for (const GraphEdge& edge : adjs) {
if (visited[edge.to]) {
continue;
}
int dist = distances[curr] + edge.weight;
if (dist < distances[edge.to]) {
distances[edge.to] = dist;
previous[edge.to] = curr;
}
}
}

Path path;
int curr = target;
while (previous[curr] != -1) {
path.push_back(curr);
curr = previous[curr];
}
path.push_back(source);
std::reverse(path.begin(), path.end());

return path;
}

int main() {
WeightedAdjacencyList graph = {
{ {1, 7}, {2, 9}, {5, 14} },
{ {0, 7}, {2, 10}, {3, 15} },
{ {0, 9}, {1, 10}, {3, 11}, {5, 2} },
{ {1, 15}, {2, 11}, {4, 6} },
{ {3, 6}, {5, 9} },
{ {0, 14}, {2, 2}, {4, 9} }
};

int source = 0;
int target = 4;

Path shortestPath = dijkstra_list(source, target, graph);

std::cout << "Shortest path from node " << source << " to node " << target
<< " is:\n";
for (int node : shortestPath) {
std::cout << node << ' ';
}
std::cout << '\n';

return 0;
}
The example graph :

Output:
Shortest path from node 0 to node 4 is:
0 2 5 4

What’s the running time of the Dijkstra’s algorithm implemented above?


The while loop condition hasUnvisited() is checked for as long as there are unvisited nodes
with finite distances. This loop can run in the worst case for V iterations, where V is the
number of vertices in the graph. O(V).
Inside the while loop, getLowestUnvisited() is called. This function iterates over all vertices to
find the unvisited vertex with the lowest distance. It runs in O(V) time.
The nested for loop inside the while loop iterates over the adjacency list of the current vertex.
In the worst case, it will iterate over E edges over the course of the entire algorithm, where E is
the total number of edges in the graph. O(E).
Therefore, the total time complexity is:
𝑂(𝑉 2 + 𝐸)
Since E can be at most V^2 in a dense graph, the time complexity can also be expressed as
O(V^2) for dense graphs.

A dense graph is a graph in which the number of edges is close to the maximum number
of edges.

The implementation of Dijkstra’s algorithm above is not optimized. The getLowestUnvisited()


function performs a linear search through all the vertices (that’s why it’s O(V)).

Instead of using a linear search on a vector to find the MINIMUM distance, what can we do to
optimize the algorithm?
Based on our previous topics, what data structure can we use for this problem?
.
.
.
.
.
.
.
.
.
.

We can use a MIN HEAP!

Remember, in a min heap, removing the minimum element is an O(1) operation (because it’s
the root). Once the minimum element is removed, we heapify to ensure that the new root is
the next smallest element, which takes O(log n) time, where n is the number of elements in
the heap.
If we use a min heap for storing the vertices where each vertex contains a pair of values: the
identifier/the index, and its distance from the starting vertex, finding the minimum distance
will result in a O(log V) time complexity.
The overall time complexity for Dijkstra's algorithm using a min-heap is:

O (( V + E) log V)

which is much faster than our implementation using a vector for keeping track of the
distances.

If using a min heap was better, why didn’t I implement it in the first place?
I want you to understand that we can write our programs in different ways, and it would solve
the problem, but if we understand how these data structures work, we can optimize them to be
faster and/or take up less space and be able to scale to larger data or more complex problems.
While the result of an algorithm might be the same regardless of optimization, the
efficiency gains in terms of speed, resource usage, and scalability are critical for
practical applications
Dijkstra's algorithm is both efficient and powerful, as it can handle graphs with weighted edges.
However, it also has its limitations. Specifically, it does not work with graphs where edges
have negative weights.

Negative weights in graphs are less common than positive weights, but they certainly do exist
and are used in various applications. Negative weights can represent costs, penalties, or losses
in certain situations.

For example, in a graph of financial transactions, positive weights might represent profits,
while negative weights could represent expenses.

Another example, in a graph for an energy system, positive weights might represent
energy gain, while negative weights could represent energy loss.

There are various common algorithms used for graphs with negative weights, like Bellman-Ford,
Floyd-Warshall, and Johnson's Algorithm, but we won’t discuss it in this course. It is useful to
know that such algorithms exist in case you need them.

You might also like