Dijkstra & Bellman-Ford - CSU083 | Shoolini University

Dijkstra & Bellman-Ford

1. Prerequisites

Before understanding Dijkstra and Bellman-Ford algorithms, you should be familiar with the following concepts:

2. What is it?

2.1 Dijkstra's Algorithm

Dijkstra’s algorithm finds the shortest path from a source node to all other nodes in a weighted graph with non-negative edges.

2.2 Bellman-Ford Algorithm

Bellman-Ford also finds the shortest path from a source node but works with graphs containing negative weights.

3. Why does this algorithm exist?

4. When should you use it?

5. How does it compare to alternatives?

5.1 Strengths

5.2 Weaknesses

6. Basic Implementation (Code)

6.1 Dijkstra's Algorithm

Dijkstra’s algorithm finds the shortest path from a single source to all other nodes using a greedy approach and a priority queue.


import heapq

def dijkstra(graph, start):
    n = len(graph)
    dist = {node: float('inf') for node in graph}
    dist[start] = 0
    pq = [(0, start)]  # (distance, node)
    
    while pq:
        current_dist, current_node = heapq.heappop(pq)
        
        if current_dist > dist[current_node]:
            continue
        
        for neighbor, weight in graph[current_node].items():
            distance = current_dist + weight
            
            if distance < dist[neighbor]:
                dist[neighbor] = distance
                heapq.heappush(pq, (distance, neighbor))
    
    return dist

6.2 Bellman-Ford Algorithm

Bellman-Ford handles graphs with negative weights and detects negative cycles using a dynamic programming approach.


def bellman_ford(graph, start):
    n = len(graph)
    dist = {node: float('inf') for node in graph}
    dist[start] = 0

    # Relax edges (V-1) times
    for _ in range(n - 1):
        for u in graph:
            for v, weight in graph[u].items():
                if dist[u] + weight < dist[v]:
                    dist[v] = dist[u] + weight

    # Check for negative weight cycle
    for u in graph:
        for v, weight in graph[u].items():
            if dist[u] + weight < dist[v]:
                raise ValueError("Graph contains a negative weight cycle")

    return dist

7. Dry Run (Step-by-Step Execution)

7.1 Sample Graph

We use a simple directed weighted graph:


graph = {
    'A': {'B': 4, 'C': 1},
    'B': {'D': 2},
    'C': {'B': 2, 'D': 5},
    'D': {}
}

7.2 Dry Run for Dijkstra

Step Current Distances
Processing A {'A': 0, 'B': inf, 'C': inf, 'D': inf}
Updating B {'A': 0, 'B': 4, 'C': inf, 'D': inf}
Updating C {'A': 0, 'B': 4, 'C': 1, 'D': inf}
Processing C {'A': 0, 'B': 4, 'C': 1, 'D': inf}
Updating B {'A': 0, 'B': 3, 'C': 1, 'D': inf}
Updating D {'A': 0, 'B': 3, 'C': 1, 'D': 6}
Processing B {'A': 0, 'B': 3, 'C': 1, 'D': 6}
Updating D {'A': 0, 'B': 3, 'C': 1, 'D': 5}
Processing D {'A': 0, 'B': 3, 'C': 1, 'D': 5}

7.3 Dry Run for Bellman-Ford

Iteration Updated Node Current Distances
1 - {'A': 0, 'B': inf, 'C': inf, 'D': inf}
1 B {'A': 0, 'B': 4, 'C': inf, 'D': inf}
1 C {'A': 0, 'B': 4, 'C': 1, 'D': inf}
1 D {'A': 0, 'B': 4, 'C': 1, 'D': 6}
1 B {'A': 0, 'B': 3, 'C': 1, 'D': 6}
2 - {'A': 0, 'B': 3, 'C': 1, 'D': 6}
2 D {'A': 0, 'B': 3, 'C': 1, 'D': 5}
3 - {'A': 0, 'B': 3, 'C': 1, 'D': 5}

8. Time Complexity Analysis

8.1 Dijkstra’s Algorithm

Dijkstra’s algorithm uses a priority queue (min-heap) to efficiently find the shortest path. The complexity depends on how the graph is represented:

8.2 Bellman-Ford Algorithm

Bellman-Ford iterates over all edges $V-1$ times, making it slower than Dijkstra.

9. Space Complexity Analysis

9.1 Dijkstra’s Algorithm

9.2 Bellman-Ford Algorithm

10. Trade-offs: Dijkstra vs Bellman-Ford

10.1 When to Use Dijkstra?

10.2 When to Use Bellman-Ford?

10.3 Key Differences

Feature Dijkstra Bellman-Ford
Handles Negative Weights? No Yes
Detects Negative Cycles? No Yes
Time Complexity $O((V+E) \log V)$ $O(VE)$
Best for Large Graphs? Yes No
Works for Sparse Graphs? Yes Yes
Algorithm Type Greedy Dynamic Programming

11. Optimizations

11.1 Optimizations for Dijkstra’s Algorithm

12.2 Optimizations for Bellman-Ford Algorithm

13. Variants of Dijkstra & Bellman-Ford

13.1 Variants of Dijkstra

13.2 Variants of Bellman-Ford

14. Iterative vs. Recursive Implementations

14.1 Dijkstra: Iterative vs. Recursive

Dijkstra’s algorithm is typically implemented iteratively using a priority queue because recursion would require excessive stack space.

14.2 Bellman-Ford: Iterative vs. Recursive


# Recursive Bellman-Ford implementation
def bellman_ford_recursive(graph, dist, edges, depth):
    if depth == 0:
        return dist
    
    for u, v, weight in edges:
        if dist[u] + weight < dist[v]:
            dist[v] = dist[u] + weight
            
    return bellman_ford_recursive(graph, dist, edges, depth - 1)

Why Iterative is Preferred: The recursive approach suffers from deep recursion and stack overflow issues for large graphs.

15. Edge Cases & Common Pitfalls

15.1 Common Pitfalls in Dijkstra’s Algorithm

15.2 Common Pitfalls in Bellman-Ford Algorithm

16. Test Cases for Verification

16.1 Test Cases for Dijkstra


def test_dijkstra():
    graph = {
        'A': {'B': 4, 'C': 1},
        'B': {'D': 2},
        'C': {'B': 2, 'D': 5},
        'D': {}
    }
    
    # Test normal case
    assert dijkstra(graph, 'A') == {'A': 0, 'B': 3, 'C': 1, 'D': 5}

    # Test disconnected graph
    graph['E'] = {}  # Isolated node
    assert dijkstra(graph, 'A')['E'] == float('inf')

    # Test single-node graph
    assert dijkstra({'A': {}}, 'A') == {'A': 0}

test_dijkstra()

16.2 Test Cases for Bellman-Ford


def test_bellman_ford():
    graph = {
        'A': {'B': 4, 'C': 1},
        'B': {'D': 2},
        'C': {'B': 2, 'D': 5},
        'D': {}
    }
    
    # Test normal case
    assert bellman_ford(graph, 'A') == {'A': 0, 'B': 3, 'C': 1, 'D': 5}

    # Test negative weights
    graph['C']['B'] = -2
    assert bellman_ford(graph, 'A') == {'A': 0, 'B': -1, 'C': 1, 'D': 5}

    # Test negative weight cycle
    graph['B']['A'] = -5
    try:
        bellman_ford(graph, 'A')
        assert False, "Should detect negative weight cycle"
    except ValueError:
        pass

test_bellman_ford()

17. Real-World Failure Scenarios

17.1 Dijkstra’s Failures

17.2 Bellman-Ford Failures

18. Real-World Applications & Industry Use Cases

18.1 Applications of Dijkstra’s Algorithm

18.2 Applications of Bellman-Ford Algorithm

19. Open-Source Implementations

Several open-source projects use these algorithms:

20. Practical Project: Finding Optimal Delivery Routes

20.1 Problem Statement

Given a set of delivery locations and road distances, find the shortest path from the warehouse to each location.

20.2 Python Implementation Using Dijkstra


import heapq

def dijkstra(graph, start):
    dist = {node: float('inf') for node in graph}
    dist[start] = 0
    pq = [(0, start)]
    
    while pq:
        current_dist, current_node = heapq.heappop(pq)
        
        if current_dist > dist[current_node]:
            continue
        
        for neighbor, weight in graph[current_node].items():
            distance = current_dist + weight
            if distance < dist[neighbor]:
                dist[neighbor] = distance
                heapq.heappush(pq, (distance, neighbor))
    
    return dist

# Sample city graph (warehouse at 'A')
graph = {
    'A': {'B': 2, 'C': 5},
    'B': {'C': 1, 'D': 3},
    'C': {'D': 2},
    'D': {'E': 4},
    'E': {}
}

# Compute shortest delivery routes
shortest_paths = dijkstra(graph, 'A')
print(shortest_paths)

20.3 Practical Use Case

20.4 Next Steps

21. Competitive Programming Assignments

Solve the following problems to strengthen your understanding of Dijkstra’s and Bellman-Ford algorithms:

21.1 Basic Problems

21.2 Intermediate Problems

21.3 Advanced Problems

22. System Design Integration

22.1 Case Study: Ride-Sharing Service (Uber, Ola, Lyft)

Implement Dijkstra's algorithm to optimize ride allocations and estimated time of arrival (ETA).

22.2 Problem Statement

A ride-sharing company wants to allocate drivers to customers based on the shortest route. Given a city graph where nodes represent locations and edges represent road distances, find the nearest available driver.

22.3 Solution Approach

22.4 Implementation Idea (Python)


import heapq

def find_nearest_driver(graph, customer_location, drivers):
    shortest_paths = dijkstra(graph, customer_location)
    nearest_driver = min(drivers, key=lambda d: shortest_paths.get(d, float('inf')))
    return nearest_driver

# Sample Graph (Road Network)
graph = {
    'A': {'B': 3, 'C': 1},
    'B': {'D': 2},
    'C': {'B': 1, 'D': 4},
    'D': {}
}

# Available Drivers
drivers = ['B', 'D']

# Find Nearest Driver for Customer at 'A'
print(find_nearest_driver(graph, 'A', drivers))

23. Practice Under Time Constraints

23.1 Time-Constrained Challenges

23.2 Resources for Speed Practice