Dijkstra's algorithm is a classic pathfinding algorithm used in graph theory to find the shortest path from a source node to all other nodes in a graph. In this article, we’ll explore the algorithm, its proof of correctness, and provide an implementation in JavaScript.
What is Dijkstra's Algorithm?
Dijkstra's algorithm is a greedy algorithm designed to find the shortest paths from a single source node in a weighted graph with non-negative edge weights. It was proposed by Edsger W. Dijkstra in 1956 and remains one of the most widely used algorithms in computer science.
Input and Output
- Input: A graph , where is the set of vertices, is the set of edges, and a source node .
- Output: The shortest path distances from to all other nodes in .
Core Concepts
- Relaxation: The process of updating the shortest known distance to a node.
- Priority Queue: Efficiently fetches the node with the smallest tentative distance.
- Greedy Approach: Processes nodes in non-decreasing order of their shortest distances.
The Algorithm
-
Initialize distances:
Use a priority queue to store nodes based on their distances.
Repeatedly extract the node with the smallest distance and relax its neighbors.
Relaxation - Mathematical Explanation
- Initialization:
where is the source node, and represents any other node.
-
Relaxation Step: for each edge
with weight
:
If
, update:
Why It Works: Relaxation ensures that we always find the shortest path to a node by progressively updating the distance when a shorter path is found.
Priority Queue - Mathematical Explanation
-
Queue Operation:
The priority queue always dequeues the node
with the smallest tentative distance:
Why It Works: By processing the node with the smallest , we guarantee the shortest path from the source to .
Proof of Correctness
We prove the correctness of Dijkstra’s algorithm using strong induction.
What is Strong Induction?
Strong induction is a variant of mathematical induction where, to prove a statement , we assume the truth of to prove . This differs from regular induction, which assumes only to prove . Explore it in greater detail in my other post.
Correctness of Dijkstra's Algorithm (Inductive Proof)
Base Case:
The source node is initialized with , which is correct.Inductive Hypothesis:
Assume all nodes processed so far have the correct shortest path distances.Inductive Step:
The next node is dequeued from the priority queue. Since is the smallest remaining distance, and all previous nodes have correct distances, is also correct.
JavaScript Implementation
Prerequisites (Priority Queue):
// Simplified Queue using Sorting
// Use Binary Heap (good)
// or Binomial Heap (better) or Pairing Heap (best)
class PriorityQueue {
constructor() {
this.queue = [];
}
enqueue(node, priority) {
this.queue.push({ node, priority });
this.queue.sort((a, b) => a.priority - b.priority);
}
dequeue() {
return this.queue.shift();
}
isEmpty() {
return this.queue.length === 0;
}
}
Here’s a JavaScript implementation of Dijkstra’s algorithm using a priority queue:
function dijkstra(graph, start) {
const distances = {}; // Hold the shortest distance from the start node to all other nodes
const previous = {}; // Stores the previous node for each node in the shortest path (used to reconstruct the path later).
const pq = new PriorityQueue(); // Used to efficiently retrieve the node with the smallest tentative distance.
// Initialize distances and previous
for (let node in graph) {
distances[node] = Infinity; // Start with infinite distances
previous[node] = null; // No previous nodes at the start
}
distances[start] = 0; // Distance to the start node is 0
pq.enqueue(start, 0);
while (!pq.isEmpty()) {
const { node } = pq.dequeue(); // Get the node with the smallest tentative distance
for (let neighbor in graph[node]) {
const distance = graph[node][neighbor]; // The edge weight
// Relaxation Step
const newDist = distances[node] + distance;
if (newDist < distances[neighbor]) {
distances[neighbor] = newDist; // Update the shortest distance to the neighbor
previous[neighbor] = node; // Update the previous node
pq.enqueue(neighbor, newDist); // Enqueue the neighbor with the updated distance
}
}
}
return { distances, previous };
}
// Example usage
const graph = {
A: { B: 1, C: 4 },
B: { A: 1, C: 2, D: 5 },
C: { A: 4, B: 2, D: 1 },
D: { B: 5, C: 1 }
};
const result = dijkstra(graph, 'A'); // Start node 'A'
console.log(result);
Reconstruct Path
function reconstructPath(previous, start, end) {
let path = [];
let currentNode = end;
while (currentNode !== start) {
path.push(currentNode);
currentNode = previous[currentNode];
}
path.push(start);
return path.reverse(); // Reverse the path to get the correct order
}
// Example usage:
const path = reconstructPath(previous, startNode, "D");
console.log("Shortest path from A to D:", path);
Example Walkthrough
Graph Representation
- Nodes:
-
Edges:
Step-by-Step Execution
-
Initialize distances:
-
Process A:
- Relax edges:
- Relax edges:
-
Process B:
- Relax edges:
- Relax edges:
-
Process C:
- Relax edge:
- Relax edge:
-
Process D:
- No further updates.
Final Distances and Path
Optimizations and Time Complexity
Comparing the time complexities of Dijkstra's algorithm with different priority queue implementations:
Priority Queue Type | Insert (M) | Extract Min | Decrease Key | Overall Time Complexity |
---|---|---|---|---|
Simple Array | O(1) | O(V) | O(V) | O(V^2) |
Binary Heap | O(log V) | O(log V) | O(log V) | O((V + E) log V) |
Binomial Heap | O(log V) | O(log V) | O(log V) | O((V + E) log V) |
Fibonacci Heap | O(1) | O(log V) | O(1) | O(V log V + E) |
Pairing Heap | O(1) | O(log V) | O(log V) | O(V log V + E) (practical) |
Key Points:
-
Simple Array:
- Inefficient for large graphs due to linear search for extract-min.
-
Binary Heap:
- Standard and commonly used due to its balance of simplicity and efficiency.
-
Binomial Heap:
- Slightly better theoretical guarantees but more complex to implement.
-
Fibonacci Heap:
- Best theoretical performance with amortized decrease-key, but harder to implement.
-
Pairing Heap:
- Simple and performs close to Fibonacci heap in practice.
Conclusion
Dijkstra’s algorithm is a powerful and efficient method for finding shortest paths in graphs with non-negative weights. While it has limitations (e.g., cannot handle negative edge weights), it’s widely used in networking, routing, and other applications.
- Relaxation ensures shortest distances by iteratively updating paths.
- Priority Queue guarantees we always process the closest node, maintaining correctness.
- Correctness is proven via induction: Once a node's distance is finalized, it's guaranteed to be the shortest path.
Here are some detailed resources where you can explore Dijkstra's algorithm along with rigorous proofs and examples:
Additionally, Wikipedia offers a great overview of the topic.
Citations:
[1] https://www.fuhuthu.com/CPSC420F2019/dijkstra.pdf
Feel free to share your thoughts or improvements in the comments!
Top comments (0)