DEV Community

M.T.Ramkrushna
M.T.Ramkrushna

Posted on

Topic 13: Shortest Path Algorithms πŸš€

Now we move deeper into graphs.

The key interview question is:

Given a graph, how do I find the minimum-cost path from one node to another?

The correct algorithm depends mainly on edge weights.


1. First: Which Algorithm Should I Use?

Memorize this table:

Graph Algorithm
Unweighted graph BFS
Weights are only 0 and 1 0-1 BFS
Non-negative weights Dijkstra
Negative weights possible Bellman-Ford
All-pairs shortest paths Floyd-Warshall
DAG Topological-order shortest path

Most important for interviews

Unweighted β†’ BFS
Non-negative weighted β†’ Dijkstra
Negative edges β†’ Bellman-Ford
Enter fullscreen mode Exit fullscreen mode

2. Unweighted Shortest Path

You already saw this with BFS.

Example:

0 --- 1 --- 2
      |
      3
Enter fullscreen mode Exit fullscreen mode

Every edge has equal cost.

Starting from 0:

distance[0] = 0
distance[1] = 1
distance[2] = 2
distance[3] = 2
Enter fullscreen mode Exit fullscreen mode

Use BFS:

from collections import deque

def shortest_path(graph, start):
    distance = {start: 0}
    queue = deque([start])

    while queue:
        node = queue.popleft()

        for neighbor in graph[node]:
            if neighbor not in distance:
                distance[neighbor] = distance[node] + 1
                queue.append(neighbor)

    return distance
Enter fullscreen mode Exit fullscreen mode

Complexity

Time:  O(V + E)
Space: O(V)
Enter fullscreen mode Exit fullscreen mode

3. Why BFS Doesn't Work for Weighted Graphs

Consider:

A --10-- B
 \       |
  1      1
   \     |
     C---
Enter fullscreen mode Exit fullscreen mode

There may be a path with more edges but lower total cost.

BFS minimizes:

number of edges
Enter fullscreen mode Exit fullscreen mode

not:

total weight
Enter fullscreen mode Exit fullscreen mode

For weighted graphs, we need another algorithm.


4. Dijkstra's Algorithm ⭐

Dijkstra finds shortest paths when:

All edge weights are non-negative.

Example:

        4
   A -------- B
   |          |
  1|          |2
   |          |
   C -------- D
        3
Enter fullscreen mode Exit fullscreen mode

Starting from A:

A = 0
C = 1
B = 4
D = 4
Enter fullscreen mode Exit fullscreen mode

5. Dijkstra's Core Idea

Maintain the best known distance to every node.

Initially:

start = 0
everything else = infinity
Enter fullscreen mode Exit fullscreen mode

Then repeatedly:

  1. Pick the unprocessed node with the smallest distance.
  2. Examine its neighbors.
  3. Try to improve their distances.

This operation is called relaxation.


6. Relaxation

Suppose:

A --5--> B
Enter fullscreen mode Exit fullscreen mode

and:

distance[A] = 3
Enter fullscreen mode Exit fullscreen mode

Then the path through A costs:

3 + 5 = 8
Enter fullscreen mode Exit fullscreen mode

If:

distance[B] = 10
Enter fullscreen mode Exit fullscreen mode

we improve it:

distance[B] = 8
Enter fullscreen mode Exit fullscreen mode

In code:

new_distance = distance + weight

if new_distance < dist[neighbor]:
    dist[neighbor] = new_distance
Enter fullscreen mode Exit fullscreen mode

This is the heart of Dijkstra.


7. Dijkstra Using heapq

Python's heapq is perfect for Dijkstra.

Graph format:

graph = {
    0: [(1, 4), (2, 1)],
    1: [(3, 1)],
    2: [(1, 2), (3, 5)],
    3: []
}
Enter fullscreen mode Exit fullscreen mode

Each tuple means:

(neighbor, weight)
Enter fullscreen mode Exit fullscreen mode

Implementation:

import heapq

def dijkstra(graph, start):
    dist = {node: float("inf") for node in graph}
    dist[start] = 0

    heap = [(0, start)]

    while heap:
        current_dist, node = heapq.heappop(heap)

        if current_dist > dist[node]:
            continue

        for neighbor, weight in graph[node]:
            new_dist = current_dist + weight

            if new_dist < dist[neighbor]:
                dist[neighbor] = new_dist
                heapq.heappush(heap, (new_dist, neighbor))

    return dist
Enter fullscreen mode Exit fullscreen mode

8. The Most Important Dijkstra Line

This line:

if current_dist > dist[node]:
    continue
Enter fullscreen mode Exit fullscreen mode

is extremely important.

Why?

heapq doesn't provide a direct decrease-key operation.

Instead, we may push multiple entries for the same node.

Example:

(10, B)
(7, B)
(5, B)
Enter fullscreen mode Exit fullscreen mode

When (5, B) is processed, the old entries become stale.

So:

if current_dist > dist[node]:
    continue
Enter fullscreen mode Exit fullscreen mode

ignores them.


9. Dijkstra Complexity

Using an adjacency list + binary heap:

Time:  O((V + E) log V)
Enter fullscreen mode Exit fullscreen mode

Usually simplified to:

O(E log V)
Enter fullscreen mode Exit fullscreen mode

Space:

O(V + E)
Enter fullscreen mode Exit fullscreen mode

10. Dijkstra Cannot Handle Negative Weights

Example:

A --2--> B
A --5--> C
C --(-10)--> B
Enter fullscreen mode Exit fullscreen mode

The path:

A β†’ C β†’ B
Enter fullscreen mode Exit fullscreen mode

has cost:

5 + (-10) = -5
Enter fullscreen mode Exit fullscreen mode

Negative edges can break Dijkstra's greedy assumption.

So:

Negative edge?
        ↓
Don't use Dijkstra
Enter fullscreen mode Exit fullscreen mode

Use Bellman-Ford when appropriate.


11. Bellman-Ford

Bellman-Ford can handle:

  • Positive edges
  • Zero edges
  • Negative edges

It can also detect negative cycles reachable from the source.


Basic Idea

Relax every edge repeatedly.

For V vertices, perform:

V - 1
Enter fullscreen mode Exit fullscreen mode

passes.

Why V - 1?

A shortest simple path can contain at most V - 1 edges.


12. Bellman-Ford Implementation

Suppose:

edges = [
    (0, 1, 4),
    (0, 2, 5),
    (1, 2, -3)
]
Enter fullscreen mode Exit fullscreen mode

Each edge is:

(source, destination, weight)
Enter fullscreen mode Exit fullscreen mode

Implementation:

def bellman_ford(n, edges, source):
    dist = [float("inf")] * n
    dist[source] = 0

    for _ in range(n - 1):
        changed = False

        for u, v, weight in edges:
            if dist[u] == float("inf"):
                continue

            new_dist = dist[u] + weight

            if new_dist < dist[v]:
                dist[v] = new_dist
                changed = True

        if not changed:
            break

    # Check for negative cycle
    for u, v, weight in edges:
        if dist[u] == float("inf"):
            continue

        if dist[u] + weight < dist[v]:
            return None  # Negative cycle

    return dist
Enter fullscreen mode Exit fullscreen mode

13. Bellman-Ford Complexity

Time:  O(VE)
Space: O(V)
Enter fullscreen mode Exit fullscreen mode

This is slower than Dijkstra for many normal weighted-graph problems.

So don't automatically use Bellman-Ford.

Use it when negative edges are relevant.


14. 0-1 BFS

What if every edge weight is either:

0 or 1
Enter fullscreen mode Exit fullscreen mode

Example:

A --0--> B
B --1--> C
Enter fullscreen mode Exit fullscreen mode

Instead of Dijkstra, we can use 0-1 BFS.

It uses a deque.

Rule

For an edge with weight 0:

appendleft()
Enter fullscreen mode Exit fullscreen mode

For an edge with weight 1:

append()
Enter fullscreen mode Exit fullscreen mode

Template

from collections import deque

def zero_one_bfs(graph, start, n):
    dist = [float("inf")] * n
    dist[start] = 0

    queue = deque([start])

    while queue:
        node = queue.popleft()

        for neighbor, weight in graph[node]:
            new_dist = dist[node] + weight

            if new_dist < dist[neighbor]:
                dist[neighbor] = new_dist

                if weight == 0:
                    queue.appendleft(neighbor)
                else:
                    queue.append(neighbor)

    return dist
Enter fullscreen mode Exit fullscreen mode

Complexity:

O(V + E)
Enter fullscreen mode Exit fullscreen mode

15. Floyd-Warshall

So far we've mostly found:

shortest paths from one source.

What if we want:

shortest paths between every pair of vertices?

Use Floyd-Warshall.

It uses dynamic programming.


Core idea

Let:

dist[i][j]
Enter fullscreen mode Exit fullscreen mode

represent the shortest known distance from i to j.

For every intermediate node k:

dist[i][j] = min(
    dist[i][j],
    dist[i][k] + dist[k][j]
)
Enter fullscreen mode Exit fullscreen mode

16. Floyd-Warshall Implementation

def floyd_warshall(dist):
    n = len(dist)

    for k in range(n):
        for i in range(n):
            for j in range(n):
                dist[i][j] = min(
                    dist[i][j],
                    dist[i][k] + dist[k][j]
                )

    return dist
Enter fullscreen mode Exit fullscreen mode

Complexity:

Time:  O(VΒ³)
Space: O(VΒ²)
Enter fullscreen mode Exit fullscreen mode

Therefore, it is usually appropriate for relatively small graphs.


17. Negative Cycle Detection with Floyd-Warshall

After running the algorithm:

for i in range(n):
    if dist[i][i] < 0:
        print("Negative cycle exists")
Enter fullscreen mode Exit fullscreen mode

Why?

Normally:

dist[i][i] = 0
Enter fullscreen mode Exit fullscreen mode

If it becomes negative, there is a negative-cost cycle reachable from i.


18. Dijkstra: Reconstruct the Actual Path

Sometimes the question doesn't ask only:

What is the shortest distance?
Enter fullscreen mode Exit fullscreen mode

It asks:

What is the shortest path?

Maintain a parent array/dictionary.

import heapq

def dijkstra_with_path(graph, start, target):
    dist = {node: float("inf") for node in graph}
    parent = {node: None for node in graph}

    dist[start] = 0
    heap = [(0, start)]

    while heap:
        current_dist, node = heapq.heappop(heap)

        if current_dist > dist[node]:
            continue

        if node == target:
            break

        for neighbor, weight in graph[node]:
            new_dist = current_dist + weight

            if new_dist < dist[neighbor]:
                dist[neighbor] = new_dist
                parent[neighbor] = node

                heapq.heappush(
                    heap,
                    (new_dist, neighbor)
                )

    if dist[target] == float("inf"):
        return None

    path = []
    current = target

    while current is not None:
        path.append(current)
        current = parent[current]

    path.reverse()

    return dist[target], path
Enter fullscreen mode Exit fullscreen mode

For example:

A β†’ C β†’ D
Enter fullscreen mode Exit fullscreen mode

could return:

(7, ["A", "C", "D"])
Enter fullscreen mode Exit fullscreen mode

19. Important Problem: Network Delay Time

This is a classic Dijkstra problem.

You have:

times = [
    [2, 1, 1],
    [2, 3, 1],
    [3, 4, 1]
]
Enter fullscreen mode Exit fullscreen mode

Meaning:

2 β†’ 1 with cost 1
2 β†’ 3 with cost 1
3 β†’ 4 with cost 1
Enter fullscreen mode Exit fullscreen mode

Starting from node 2:

2 β†’ 1 = 1
2 β†’ 3 = 1
2 β†’ 3 β†’ 4 = 2
Enter fullscreen mode Exit fullscreen mode

So the time needed for everyone to receive the signal is:

max(1, 1, 2) = 2
Enter fullscreen mode Exit fullscreen mode

The pattern is:

Dijkstra
    ↓
shortest distance to every node
    ↓
take maximum distance
Enter fullscreen mode Exit fullscreen mode

20. Important Problem: Cheapest Flights Within K Stops

This problem is trickier.

You have a limit:

maximum K stops
Enter fullscreen mode Exit fullscreen mode

A normal Dijkstra implementation based only on the minimum cost per node can be insufficient because reaching a node cheaply with too many stops may not be a valid state.

The state becomes something like:

(cost, node, stops)
Enter fullscreen mode Exit fullscreen mode

This teaches an important interview lesson:

Sometimes the graph state is more than just the node.

For example:

(node, remaining_steps)
(node, fuel)
(node, number_of_stops)
(node, time)
Enter fullscreen mode Exit fullscreen mode

21. Shortest Path Recognition

This is extremely important.

Question says:

"Minimum number of edges"

Use:

BFS
Enter fullscreen mode Exit fullscreen mode

Says:

"Minimum cost" with non-negative weights

Use:

Dijkstra
Enter fullscreen mode Exit fullscreen mode

Says:

"Weights are 0 or 1"

Consider:

0-1 BFS
Enter fullscreen mode Exit fullscreen mode

Says:

"Negative edge weights"

Use:

Bellman-Ford
Enter fullscreen mode Exit fullscreen mode

Says:

"Shortest distance between every pair"

Consider:

Floyd-Warshall
Enter fullscreen mode Exit fullscreen mode

Says:

"DAG"

Consider:

Topological-order shortest path
Enter fullscreen mode Exit fullscreen mode

22. Dijkstra Template to Memorize ⭐

This is probably the most important code from today's topic:

import heapq

def dijkstra(graph, start):
    dist = {node: float("inf") for node in graph}
    dist[start] = 0

    heap = [(0, start)]

    while heap:
        current_dist, node = heapq.heappop(heap)

        if current_dist > dist[node]:
            continue

        for neighbor, weight in graph[node]:
            new_dist = current_dist + weight

            if new_dist < dist[neighbor]:
                dist[neighbor] = new_dist
                heapq.heappush(
                    heap,
                    (new_dist, neighbor)
                )

    return dist
Enter fullscreen mode Exit fullscreen mode

Understand these three lines especially:

current_dist, node = heapq.heappop(heap)
Enter fullscreen mode Exit fullscreen mode
new_dist = current_dist + weight
Enter fullscreen mode Exit fullscreen mode
if new_dist < dist[neighbor]:
Enter fullscreen mode Exit fullscreen mode

That's the core of Dijkstra.


23. Algorithm Comparison

Algorithm Weights Main use Time
BFS Unweighted Shortest path O(V+E)
0-1 BFS 0/1 Shortest path O(V+E)
Dijkstra Non-negative Single-source shortest path O((V+E)log V)
Bellman-Ford Can be negative Single-source + negative cycle O(VE)
Floyd-Warshall Can be negative All-pairs shortest path O(VΒ³)

24. Common Interview Mistakes

❌ Using BFS on weighted edges

BFS minimizes the number of edges, not total cost.

❌ Using Dijkstra with negative weights

Dijkstra requires non-negative edge weights.

❌ Forgetting stale heap entries

Always consider:

if current_dist > dist[node]:
    continue
Enter fullscreen mode Exit fullscreen mode

❌ Confusing directed and undirected graphs

For an undirected edge:

graph[u].append((v, weight))
graph[v].append((u, weight))
Enter fullscreen mode Exit fullscreen mode

For directed:

graph[u].append((v, weight))
Enter fullscreen mode Exit fullscreen mode

❌ Returning only distance when path is required

Maintain:

parent[neighbor] = node
Enter fullscreen mode Exit fullscreen mode

and reconstruct the path afterward.


25. Interview Practice

Easy

  1. Shortest path in an unweighted graph
  2. Network Delay Time
  3. Path With Minimum Effort

Medium

  1. Cheapest Flights Within K Stops
  2. Swim in Rising Water
  3. Minimum Cost to Connect Points
  4. 0-1 Matrix
  5. Minimum Obstacle Removal to Reach Corner

Advanced

  1. Bellman-Ford implementation
  2. Floyd-Warshall
  3. Cheapest Flights with state optimization
  4. Shortest Path Visiting All Nodes

🧠 Final Cheat Sheet

             SHORTEST PATH
                   β”‚
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚           β”‚           β”‚
   Unweighted    Weighted    All pairs
       β”‚           β”‚           β”‚
      BFS          β”‚      Floyd-Warshall
                   β”‚
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”
          β”‚        β”‚        β”‚
        0/1     Non-negative Negative
          β”‚        β”‚        β”‚
       0-1 BFS  Dijkstra  Bellman-Ford
Enter fullscreen mode Exit fullscreen mode

Next topic: Union-Find (Disjoint Set Union), Minimum Spanning Trees, Kruskal's and Prim's algorithms β€” another major interview/exam section.

Top comments (0)