Shortest Paths

Find the cheapest route through a weighted graph with BFS, Dijkstra or Bellman-Ford, depending on what the edge weights can be.

How It Works

A shortest-path problem gives you a graph whose edges carry weights (distances, times, prices) and asks for the cheapest total weight from a source to the other nodes. No single algorithm is best. The right one depends on what the weights can be:

  • Every edge costs the same: plain BFS, in O(V + E). The first time it reaches a node is along the fewest edges.
  • Weights are zero or positive: Dijkstra's algorithm with a binary heap, in O((V + E) log V). This is the default for weighted graphs.
  • Some weights are negative, or a path may use at most k edges: Bellman-Ford, in O(V × E). It also detects negative cycles.
  • You need the distance between every pair of nodes, and V is small (a few hundred): Floyd-Warshall, in O(V³).

Dijkstra grows a region of settled nodes outward from the source, always settling the closest unsettled node next. A min-heap keyed by distance finds that node quickly. Settling a node means relaxing its edges: if going through it gives a neighbor a shorter distance, record the new distance and push the neighbor. Why is the closest node safe to settle? Any other route to it has to leave the settled region through some node that's at least as far away, and with no negative edges, the rest of that route can only add to the total.

     +--------4--------+
     |                 v
     0 --1--> 1 --2--> 2 --1--> 3      4
              |                 ^
              +--------5--------+
The template's graph, with weights on the edges. The direct edge 0 -> 2 costs 4, but 0 -> 1 -> 2 costs 3. Nothing leads to node 4.

Dijkstra's Algorithm, Step by Step

Step 1 of 13

graph

pq (closest first)

  • Being expanded, or an edge that shortens a distance
  • Tentative distance, waiting in the heap
  • Settled, with the edge that reached it
  • Stale entry, or unreachable

Every distance starts at inf except the source's: dist[0] = 0. The heap holds one entry, (0, 0), meaning distance 0 to node 0.

All 13 steps as text
  1. Every distance starts at inf except the source's: dist[0] = 0. The heap holds one entry, (0, 0), meaning distance 0 to node 0.
  2. Pop (0, 0). It's the closest entry and it matches dist[0], so node 0 is settled.
  3. Relax 0 -> 1: 0 + 1 = 1 beats inf, so dist[1] = 1 and (1, 1) goes into the heap.
  4. Relax 0 -> 2: 0 + 4 = 4 beats inf, so dist[2] = 4 and (4, 2) goes into the heap.
  5. Pop (1, 1) and settle 1 at 1 along 0 -> 1. Nothing unsettled is closer, so any other route to 1 leaves the settled region through a node at least as far and can only cost more.
  6. Relax 1 -> 2: 1 + 2 = 3 beats 4. Push (3, 2) and leave (4, 2) where it is: it's stale now, and gets skipped when it comes out.
  7. Relax 1 -> 3: 1 + 5 = 6 beats inf, so dist[3] = 6 and (6, 3) goes into the heap.
  8. Pop (3, 2) and settle 2 at 3 along 0 -> 1 -> 2, down from the 4 it was first given.
  9. Relax 2 -> 3: 3 + 1 = 4 beats 6. Push (4, 3) and leave (6, 3) where it is: it's stale now, and gets skipped when it comes out.
  10. Pop (4, 2). It's stale: 4 > dist[2] = 3, because a shorter way in was found after it was pushed. Skip it.
  11. Pop (4, 3) and settle 3 at 4 along 0 -> 1 -> 2 -> 3, down from the 6 it was first given. It has no outgoing edges, so there's nothing to relax.
  12. Pop (6, 3). It's stale: 6 > dist[3] = 4, because a shorter way in was found after it was pushed. Skip it.
  13. The heap is empty, so every reachable node is settled. 4 is still at inf because no edge leads to it, so it's reported as -1: the answer is [0, 1, 3, 4, -1].

That argument is exactly what a negative edge breaks. Take edges A -> B costing 2, A -> C costing 3 and C -> B costing -2. Dijkstra settles B at 2 before it ever expands C, but the route through C costs 1. A version that treats settled nodes as final returns the wrong answer. The lazy version in the template would notice the improvement and push B again, but then nodes can be expanded many times over, and on adversarial inputs the running time can grow exponentially. Once weights can be negative, switch to Bellman-Ford.

Bellman-Ford drops the greedy step and simply relaxes every edge, V - 1 times over. After round i, every distance is at least as good as the best path with i edges or fewer. Without negative cycles, a shortest path never repeats a node, so it has at most V - 1 edges, and V - 1 rounds are enough. If one more round still improves something, there's a negative cycle, and "shortest" stops meaning anything for nodes that cycle can reach. Stopping after k rounds instead gives the cheapest path that uses at most k edges, but only if each round reads the distances from the round before. Relaxing in place can chain several edges in one round, which is why Cheapest Flights Within K Stops below works on a copy.

When to Reach for It

  • Edges carry a cost, time or distance, and you want the minimum total from a source.
  • The question says "cheapest route", "minimum time for every node to receive it" or "path with the least effort".
  • A path may use at most k edges, stops or steps. That points to Bellman-Ford rounds, or BFS over (node, steps) states.
  • Some weights can be negative, or the problem is about negative cycles (currency arbitrage, for instance). Dijkstra is out, so use Bellman-Ford.
  • Moves on a grid cost different amounts. The grid is an implicit graph, so run Dijkstra over its cells.

Complexity

OperationTimeSpace
BFS (every edge costs the same)O(V + E)O(V + E)
Dijkstra with a binary heapO((V + E) log V)O(V + E)
Bellman-FordO(V × E)O(V)
Bellman-Ford capped at k edges (copy per round)O(k × (V + E))O(V)
Floyd-Warshall (all pairs)O(V³)O(V²)

The Template

Dijkstra's Algorithm

Build an adjacency list of [neighbor, weight] pairs and set every distance to infinity except the source's, which is 0. The heap holds [distance, node] pairs, so each pop returns the closest node that hasn't been settled. Relaxing its edges pushes every neighbor whose distance just improved. At the end, any distance still at infinity belongs to a node the source can't reach, reported as -1.

Most heaps can't lower the priority of an entry that's already inside, so the template uses lazy deletion: when a distance improves, push a fresh entry and leave the old one where it is. When the old entry is popped later, its distance is bigger than dist[node], which marks it as stale, and it's skipped. The heap can grow to about E entries, so each operation costs O(log E). Without duplicate edges E is at most V², so log E is at most 2 log V and the total stays O((V + E) log V).

pop      dist after           heap after
(0, 0)   [0, 1, 4, -, -]      (1, 1) (4, 2)
(1, 1)   [0, 1, 3, 6, -]      (3, 2) (4, 2) (6, 3)
(3, 2)   [0, 1, 3, 4, -]      (4, 2) (4, 3) (6, 3)
(4, 2)   stale, skip          (4, 3) (6, 3)
(4, 3)   [0, 1, 3, 4, -]      (6, 3)
(6, 3)   stale, skip          empty
Dijkstra from node 0. Entries are (distance, node), and - means not reached yet. Nodes 2 and 3 are each pushed twice, and their older entries come out stale.

Time O((V + E) log V)Space O(V + E)

import heapq
import math


def dijkstra(n: int, edges: list[list[int]], source: int) -> list[int]:
    """Shortest distance from `source` to every node of a directed graph with
    non-negative weights, where each edge is [u, v, w]. -1 if unreachable."""
    graph: list[list[tuple[int, int]]] = [[] for _ in range(n)]
    for u, v, w in edges:
        graph[u].append((v, w))
    dist = [math.inf] * n
    dist[source] = 0
    pq = [(0, source)]  # (distance, node), closest first.
    while pq:
        d, node = heapq.heappop(pq)
        if d > dist[node]:
            continue  # Stale entry: node was already settled closer.
        for neighbor, w in graph[node]:
            if d + w < dist[neighbor]:
                dist[neighbor] = d + w  # A shorter way in, so queue neighbor again.
                heapq.heappush(pq, (dist[neighbor], neighbor))
    return [-1 if d == math.inf else int(d) for d in dist]


if __name__ == "__main__":
    edges = [[0, 1, 1], [0, 2, 4], [1, 2, 2], [1, 3, 5], [2, 3, 1]]
    print(dijkstra(5, edges, 0))  # [0, 1, 3, 4, -1]
    print(dijkstra(5, edges, 1))  # [-1, 0, 2, 3, -1]
// Shortest distance from source to every node of a directed graph with
// non-negative weights, where each edge is [u, v, w]. -1 if unreachable.
export function dijkstra(n: number, edges: number[][], source: number): number[] {
  const graph: [number, number][][] = Array.from({ length: n }, () => []);
  for (const [u, v, w] of edges) graph[u].push([v, w]);
  const dist = new Array<number>(n).fill(Infinity);
  dist[source] = 0;
  const pq: [number, number][] = [[0, source]]; // [distance, node], closest first.
  while (pq.length > 0) {
    const [d, node] = heapPop(pq);
    if (d > dist[node]) continue; // Stale entry: node was already settled closer.
    for (const [neighbor, w] of graph[node]) {
      if (d + w < dist[neighbor]) {
        dist[neighbor] = d + w; // A shorter way in, so queue neighbor again.
        heapPush(pq, [dist[neighbor], neighbor]);
      }
    }
  }
  return dist.map((d) => (d === Infinity ? -1 : d));
}

// TypeScript has no built-in priority queue, so here's a small binary
// min-heap of [distance, node] pairs, ordered by distance.
function heapPush(heap: [number, number][], item: [number, number]): void {
  heap.push(item);
  let i = heap.length - 1;
  while (i > 0) {
    const parent = (i - 1) >> 1;
    if (heap[parent][0] <= heap[i][0]) break;
    [heap[parent], heap[i]] = [heap[i], heap[parent]];
    i = parent;
  }
}

function heapPop(heap: [number, number][]): [number, number] {
  const top = heap[0];
  const last = heap.pop()!;
  if (heap.length > 0) {
    heap[0] = last; // Move the last leaf to the root, then sift it down.
    let i = 0;
    while (2 * i + 1 < heap.length) {
      let child = 2 * i + 1;
      if (child + 1 < heap.length && heap[child + 1][0] < heap[child][0]) child++;
      if (heap[i][0] <= heap[child][0]) break;
      [heap[i], heap[child]] = [heap[child], heap[i]];
      i = child;
    }
  }
  return top;
}

const edges = [[0, 1, 1], [0, 2, 4], [1, 2, 2], [1, 3, 5], [2, 3, 1]];
console.log(JSON.stringify(dijkstra(5, edges, 0))); // [0,1,3,4,-1]
console.log(JSON.stringify(dijkstra(5, edges, 1))); // [-1,0,2,3,-1]
package main

import (
	"container/heap"
	"fmt"
	"math"
)

// dijkstra returns the shortest distance from source to every node of a
// directed graph with non-negative weights, where each edge is [u, v, w].
// Unreachable nodes get -1.
func dijkstra(n int, edges [][]int, source int) []int {
	graph := make([][][2]int, n)
	for _, e := range edges {
		u, v, w := e[0], e[1], e[2]
		graph[u] = append(graph[u], [2]int{v, w})
	}
	dist := make([]int, n)
	for i := range dist {
		dist[i] = math.MaxInt
	}
	dist[source] = 0
	pq := &minHeap{{0, source}} // [distance, node], closest first.
	for pq.Len() > 0 {
		top := heap.Pop(pq).([2]int)
		d, node := top[0], top[1]
		if d > dist[node] {
			continue // Stale entry: node was already settled closer.
		}
		for _, e := range graph[node] {
			neighbor, w := e[0], e[1]
			if d+w < dist[neighbor] {
				dist[neighbor] = d + w // A shorter way in, so queue neighbor again.
				heap.Push(pq, [2]int{dist[neighbor], neighbor})
			}
		}
	}
	for i := range dist {
		if dist[i] == math.MaxInt {
			dist[i] = -1
		}
	}
	return dist
}

// minHeap holds [distance, node] pairs for container/heap, smallest distance
// first.
type minHeap [][2]int

func (h minHeap) Len() int           { return len(h) }
func (h minHeap) Less(i, j int) bool { return h[i][0] < h[j][0] }
func (h minHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
func (h *minHeap) Push(x any)        { *h = append(*h, x.([2]int)) }
func (h *minHeap) Pop() any {
	old := *h
	top := old[len(old)-1]
	*h = old[:len(old)-1]
	return top
}

func main() {
	edges := [][]int{{0, 1, 1}, {0, 2, 4}, {1, 2, 2}, {1, 3, 5}, {2, 3, 1}}
	fmt.Println(dijkstra(5, edges, 0)) // [0 1 3 4 -1]
	fmt.Println(dijkstra(5, edges, 1)) // [-1 0 2 3 -1]
}
using System;
using System.Collections.Generic;

public class Solution
{
    // Shortest distance from source to every node of a directed graph with
    // non-negative weights, where each edge is [u, v, w]. -1 if unreachable.
    public static int[] Dijkstra(int n, int[][] edges, int source)
    {
        var graph = new List<List<(int, int)>>();
        for (int i = 0; i < n; i++) graph.Add(new List<(int, int)>());
        foreach (var edge in edges)
        {
            int u = edge[0], v = edge[1], w = edge[2];
            graph[u].Add((v, w));
        }
        var dist = new int[n];
        Array.Fill(dist, int.MaxValue);
        dist[source] = 0;
        var pq = new PriorityQueue<int, int>(); // Nodes, closest distance first.
        pq.Enqueue(source, 0);
        while (pq.TryDequeue(out int node, out int d))
        {
            if (d > dist[node]) continue; // Stale entry: node was already settled closer.
            foreach (var (neighbor, w) in graph[node])
            {
                if (d + w < dist[neighbor])
                {
                    dist[neighbor] = d + w; // A shorter way in, so queue neighbor again.
                    pq.Enqueue(neighbor, dist[neighbor]);
                }
            }
        }
        for (int i = 0; i < n; i++)
        {
            if (dist[i] == int.MaxValue) dist[i] = -1;
        }
        return dist;
    }

    public static void Main()
    {
        int[][] edges = [[0, 1, 1], [0, 2, 4], [1, 2, 2], [1, 3, 5], [2, 3, 1]];
        Console.WriteLine(Format(Dijkstra(5, edges, 0))); // [0, 1, 3, 4, -1]
        Console.WriteLine(Format(Dijkstra(5, edges, 1))); // [-1, 0, 2, 3, -1]
    }

    static string Format(int[] values) => "[" + string.Join(", ", values) + "]";
}
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.PriorityQueue;

public class Solution {
    // Shortest distance from source to every node of a directed graph with
    // non-negative weights, where each edge is [u, v, w]. -1 if unreachable.
    static int[] dijkstra(int n, int[][] edges, int source) {
        List<List<int[]>> graph = new ArrayList<>();
        for (int i = 0; i < n; i++) graph.add(new ArrayList<>());
        for (int[] edge : edges) {
            int u = edge[0], v = edge[1], w = edge[2];
            graph.get(u).add(new int[] {v, w});
        }
        int[] dist = new int[n];
        Arrays.fill(dist, Integer.MAX_VALUE);
        dist[source] = 0;
        // [distance, node], closest first.
        PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
        pq.add(new int[] {0, source});
        while (!pq.isEmpty()) {
            int[] top = pq.poll();
            int d = top[0], node = top[1];
            if (d > dist[node]) continue; // Stale entry: node was already settled closer.
            for (int[] e : graph.get(node)) {
                int neighbor = e[0], w = e[1];
                if (d + w < dist[neighbor]) {
                    dist[neighbor] = d + w; // A shorter way in, so queue neighbor again.
                    pq.add(new int[] {dist[neighbor], neighbor});
                }
            }
        }
        for (int i = 0; i < n; i++) {
            if (dist[i] == Integer.MAX_VALUE) dist[i] = -1;
        }
        return dist;
    }

    public static void main(String[] args) {
        int[][] edges = {{0, 1, 1}, {0, 2, 4}, {1, 2, 2}, {1, 3, 5}, {2, 3, 1}};
        System.out.println(Arrays.toString(dijkstra(5, edges, 0))); // [0, 1, 3, 4, -1]
        System.out.println(Arrays.toString(dijkstra(5, edges, 1))); // [-1, 0, 2, 3, -1]
    }
}

Worked Problems

Each solution stays folded until you open it, so you can try the problem first. Every one is a complete program in Python, TypeScript, Go, C# and Java, run and checked against its expected output.

Network Delay Time

MEDIUMLeetCode 743

Problem

A network has n nodes labeled 1 to n. Each entry [u, v, w] in times is a one-way link: a signal sent from u reaches v after w time units. You send a signal from node k. Return how long it takes until every node has received it, or -1 if some node never does.

Example: times = [[2, 1, 1], [2, 3, 1], [3, 4, 1]], n = 4, k = 2 returns 2. Nodes 1 and 3 hear the signal at time 1, and node 4 hears it from node 3 at time 2.

Show Solution

Approach

The signal travels down every link at once, so each node first hears it at its shortest-path distance from k. The answer is the largest of those distances, and one run of Dijkstra computes all of them. Times are never negative, so Dijkstra applies.

Two details. The nodes are numbered from 1, so the arrays get n + 1 slots and slot 0 is never used. And if any node's distance is still infinite at the end, no path reaches it, so the answer is -1.

The last demo shows why the first route you find isn't good enough. The direct link 1 -> 2 takes 4, but 1 -> 3 -> 2 takes 1 + 2 = 3. Dijkstra settles node 3 at time 1, lowers node 2 from 4 to 3 through it, and later skips node 2's stale entry at 4. With n nodes and E links, that's O((n + E) log n).

Time O((n + E) log n)Space O(n + E)

import heapq
import math


def network_delay_time(times: list[list[int]], n: int, k: int) -> int:
    """Time for a signal sent from node `k` to reach all `n` nodes (labeled 1..n),
    where each [u, v, w] in `times` is a directed edge that takes w to cross.
    -1 if some node never hears it."""
    graph: list[list[tuple[int, int]]] = [[] for _ in range(n + 1)]  # Index 0 unused.
    for u, v, w in times:
        graph[u].append((v, w))
    dist = [math.inf] * (n + 1)
    dist[k] = 0
    pq = [(0, k)]  # (time, node), earliest first.
    while pq:
        d, node = heapq.heappop(pq)
        if d > dist[node]:
            continue  # Stale entry: node was already settled sooner.
        for neighbor, w in graph[node]:
            if d + w < dist[neighbor]:
                dist[neighbor] = d + w
                heapq.heappush(pq, (dist[neighbor], neighbor))
    # Everyone has the signal once the last node to hear it does.
    slowest = 0
    for node in range(1, n + 1):
        slowest = max(slowest, dist[node])
    return -1 if slowest == math.inf else int(slowest)


if __name__ == "__main__":
    print(network_delay_time([[2, 1, 1], [2, 3, 1], [3, 4, 1]], 4, 2))  # 2
    print(network_delay_time([[1, 2, 1]], 2, 1))  # 1
    print(network_delay_time([[1, 2, 1]], 2, 2))  # -1
    print(network_delay_time([[1, 2, 4], [1, 3, 1], [3, 2, 2]], 3, 1))  # 3
// Time for a signal sent from node k to reach all n nodes (labeled 1..n),
// where each [u, v, w] in times is a directed edge that takes w to cross.
// -1 if some node never hears it.
export function networkDelayTime(times: number[][], n: number, k: number): number {
  const graph: [number, number][][] = Array.from({ length: n + 1 }, () => []); // Index 0 unused.
  for (const [u, v, w] of times) graph[u].push([v, w]);
  const dist = new Array<number>(n + 1).fill(Infinity);
  dist[k] = 0;
  const pq: [number, number][] = [[0, k]]; // [time, node], earliest first.
  while (pq.length > 0) {
    const [d, node] = heapPop(pq);
    if (d > dist[node]) continue; // Stale entry: node was already settled sooner.
    for (const [neighbor, w] of graph[node]) {
      if (d + w < dist[neighbor]) {
        dist[neighbor] = d + w;
        heapPush(pq, [dist[neighbor], neighbor]);
      }
    }
  }
  // Everyone has the signal once the last node to hear it does.
  let slowest = 0;
  for (let node = 1; node <= n; node++) slowest = Math.max(slowest, dist[node]);
  return slowest === Infinity ? -1 : slowest;
}

// TypeScript has no built-in priority queue, so here's a small binary
// min-heap of [time, node] pairs, ordered by time.
function heapPush(heap: [number, number][], item: [number, number]): void {
  heap.push(item);
  let i = heap.length - 1;
  while (i > 0) {
    const parent = (i - 1) >> 1;
    if (heap[parent][0] <= heap[i][0]) break;
    [heap[parent], heap[i]] = [heap[i], heap[parent]];
    i = parent;
  }
}

function heapPop(heap: [number, number][]): [number, number] {
  const top = heap[0];
  const last = heap.pop()!;
  if (heap.length > 0) {
    heap[0] = last; // Move the last leaf to the root, then sift it down.
    let i = 0;
    while (2 * i + 1 < heap.length) {
      let child = 2 * i + 1;
      if (child + 1 < heap.length && heap[child + 1][0] < heap[child][0]) child++;
      if (heap[i][0] <= heap[child][0]) break;
      [heap[i], heap[child]] = [heap[child], heap[i]];
      i = child;
    }
  }
  return top;
}

console.log(networkDelayTime([[2, 1, 1], [2, 3, 1], [3, 4, 1]], 4, 2)); // 2
console.log(networkDelayTime([[1, 2, 1]], 2, 1)); // 1
console.log(networkDelayTime([[1, 2, 1]], 2, 2)); // -1
console.log(networkDelayTime([[1, 2, 4], [1, 3, 1], [3, 2, 2]], 3, 1)); // 3
package main

import (
	"container/heap"
	"fmt"
	"math"
)

// networkDelayTime returns the time for a signal sent from node k to reach all
// n nodes (labeled 1..n), where each [u, v, w] in times is a directed edge
// that takes w to cross. It returns -1 if some node never hears it.
func networkDelayTime(times [][]int, n int, k int) int {
	graph := make([][][2]int, n+1) // Index 0 unused.
	for _, t := range times {
		u, v, w := t[0], t[1], t[2]
		graph[u] = append(graph[u], [2]int{v, w})
	}
	dist := make([]int, n+1)
	for i := range dist {
		dist[i] = math.MaxInt
	}
	dist[k] = 0
	pq := &minHeap{{0, k}} // [time, node], earliest first.
	for pq.Len() > 0 {
		top := heap.Pop(pq).([2]int)
		d, node := top[0], top[1]
		if d > dist[node] {
			continue // Stale entry: node was already settled sooner.
		}
		for _, e := range graph[node] {
			neighbor, w := e[0], e[1]
			if d+w < dist[neighbor] {
				dist[neighbor] = d + w
				heap.Push(pq, [2]int{dist[neighbor], neighbor})
			}
		}
	}
	// Everyone has the signal once the last node to hear it does.
	slowest := 0
	for node := 1; node <= n; node++ {
		slowest = max(slowest, dist[node])
	}
	if slowest == math.MaxInt {
		return -1
	}
	return slowest
}

// minHeap holds [time, node] pairs for container/heap, earliest first.
type minHeap [][2]int

func (h minHeap) Len() int           { return len(h) }
func (h minHeap) Less(i, j int) bool { return h[i][0] < h[j][0] }
func (h minHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
func (h *minHeap) Push(x any)        { *h = append(*h, x.([2]int)) }
func (h *minHeap) Pop() any {
	old := *h
	top := old[len(old)-1]
	*h = old[:len(old)-1]
	return top
}

func main() {
	fmt.Println(networkDelayTime([][]int{{2, 1, 1}, {2, 3, 1}, {3, 4, 1}}, 4, 2)) // 2
	fmt.Println(networkDelayTime([][]int{{1, 2, 1}}, 2, 1))                       // 1
	fmt.Println(networkDelayTime([][]int{{1, 2, 1}}, 2, 2))                       // -1
	fmt.Println(networkDelayTime([][]int{{1, 2, 4}, {1, 3, 1}, {3, 2, 2}}, 3, 1)) // 3
}
using System;
using System.Collections.Generic;

public class Solution
{
    // Time for a signal sent from node k to reach all n nodes (labeled 1..n),
    // where each [u, v, w] in times is a directed edge that takes w to cross.
    // -1 if some node never hears it.
    public static int NetworkDelayTime(int[][] times, int n, int k)
    {
        var graph = new List<List<(int, int)>>(); // Index 0 unused.
        for (int i = 0; i <= n; i++) graph.Add(new List<(int, int)>());
        foreach (var t in times)
        {
            int u = t[0], v = t[1], w = t[2];
            graph[u].Add((v, w));
        }
        var dist = new int[n + 1];
        Array.Fill(dist, int.MaxValue);
        dist[k] = 0;
        var pq = new PriorityQueue<int, int>(); // Nodes, earliest time first.
        pq.Enqueue(k, 0);
        while (pq.TryDequeue(out int node, out int d))
        {
            if (d > dist[node]) continue; // Stale entry: node was already settled sooner.
            foreach (var (neighbor, w) in graph[node])
            {
                if (d + w < dist[neighbor])
                {
                    dist[neighbor] = d + w;
                    pq.Enqueue(neighbor, dist[neighbor]);
                }
            }
        }
        // Everyone has the signal once the last node to hear it does.
        int slowest = 0;
        for (int node = 1; node <= n; node++) slowest = Math.Max(slowest, dist[node]);
        return slowest == int.MaxValue ? -1 : slowest;
    }

    public static void Main()
    {
        Console.WriteLine(NetworkDelayTime([[2, 1, 1], [2, 3, 1], [3, 4, 1]], 4, 2)); // 2
        Console.WriteLine(NetworkDelayTime([[1, 2, 1]], 2, 1)); // 1
        Console.WriteLine(NetworkDelayTime([[1, 2, 1]], 2, 2)); // -1
        Console.WriteLine(NetworkDelayTime([[1, 2, 4], [1, 3, 1], [3, 2, 2]], 3, 1)); // 3
    }
}
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.PriorityQueue;

public class Solution {
    // Time for a signal sent from node k to reach all n nodes (labeled 1..n),
    // where each [u, v, w] in times is a directed edge that takes w to cross.
    // -1 if some node never hears it.
    static int networkDelayTime(int[][] times, int n, int k) {
        List<List<int[]>> graph = new ArrayList<>(); // Index 0 unused.
        for (int i = 0; i <= n; i++) graph.add(new ArrayList<>());
        for (int[] t : times) {
            int u = t[0], v = t[1], w = t[2];
            graph.get(u).add(new int[] {v, w});
        }
        int[] dist = new int[n + 1];
        Arrays.fill(dist, Integer.MAX_VALUE);
        dist[k] = 0;
        // [time, node], earliest first.
        PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
        pq.add(new int[] {0, k});
        while (!pq.isEmpty()) {
            int[] top = pq.poll();
            int d = top[0], node = top[1];
            if (d > dist[node]) continue; // Stale entry: node was already settled sooner.
            for (int[] e : graph.get(node)) {
                int neighbor = e[0], w = e[1];
                if (d + w < dist[neighbor]) {
                    dist[neighbor] = d + w;
                    pq.add(new int[] {dist[neighbor], neighbor});
                }
            }
        }
        // Everyone has the signal once the last node to hear it does.
        int slowest = 0;
        for (int node = 1; node <= n; node++) slowest = Math.max(slowest, dist[node]);
        return slowest == Integer.MAX_VALUE ? -1 : slowest;
    }

    public static void main(String[] args) {
        System.out.println(networkDelayTime(new int[][] {{2, 1, 1}, {2, 3, 1}, {3, 4, 1}}, 4, 2)); // 2
        System.out.println(networkDelayTime(new int[][] {{1, 2, 1}}, 2, 1)); // 1
        System.out.println(networkDelayTime(new int[][] {{1, 2, 1}}, 2, 2)); // -1
        System.out.println(networkDelayTime(new int[][] {{1, 2, 4}, {1, 3, 1}, {3, 2, 2}}, 3, 1)); // 3
    }
}

Cheapest Flights Within K Stops

MEDIUMLeetCode 787

Problem

There are n cities and a list of one-way flights, each written as [from, to, price]. Find the cheapest way to get from src to dst with at most k stops along the way, or return -1 if no route fits. A stop is a city you land in between, so k stops means at most k + 1 flights.

Example: n = 3, flights = [[0, 1, 100], [1, 2, 100], [0, 2, 500]], src = 0, dst = 2. With k = 1 the answer is 200, flying 0 to 1 to 2. With k = 0 you can't stop in city 1, so the answer is the direct flight at 500.

Show Solution

Approach

A cap on the number of edges is exactly what Bellman-Ford's rounds count. Start with every price at infinity except src, which is 0, then run k + 1 rounds. Each round tries every flight once, extending the cheapest routes found so far by one more flight. After round i, prices[v] is the cheapest way to reach v in at most i flights, so after k + 1 rounds, prices[dst] is the answer.

The important detail is that each round reads last round's prices and writes into a copy. If you update in place, a price lowered earlier in a round can be used again later in the same round, so one round can chain several flights. Here's that bug on the example with k = 0: the single round relaxes 0 -> 1 and sets city 1 to 100, then relaxes 1 -> 2 from that brand-new 100 and sets city 2 to 200. That route makes one stop, which k = 0 forbids. With the copy, 1 -> 2 still sees city 1 at infinity during round 1, and the answer stays 500.

Why not plain Dijkstra? It keeps only the cheapest price for each city, however many flights that price took. In the first demo, the cheapest way to city 3 is 0 to 1 to 2 to 3 for 400, but that's two stops. With k = 1 the answer is 0 to 1 to 3 for 700. Dijkstra can work if each heap entry also tracks the flights used so far, but the k + 1 rounds here are simpler: each one copies n prices and scans all E flights.

Time O((k + 1) × (n + E))Space O(n)

import math


def find_cheapest_price(n: int, flights: list[list[int]], src: int, dst: int, k: int) -> int:
    """Cheapest price from `src` to `dst` using at most `k` stops (so at most k + 1
    flights), where each flight is [u, v, price]. -1 if no such route exists."""
    prices = [math.inf] * n
    prices[src] = 0
    # After round i, prices[v] is the cheapest way to reach v in at most i flights.
    for _ in range(k + 1):
        # Read last round's prices and write into a copy. Updating in place would let
        # one round chain several flights and slip past the stop limit.
        next_prices = prices.copy()
        for u, v, price in flights:
            # Only fly out of cities an earlier round already reached.
            if prices[u] != math.inf and prices[u] + price < next_prices[v]:
                next_prices[v] = prices[u] + price
        prices = next_prices
    return -1 if prices[dst] == math.inf else int(prices[dst])


if __name__ == "__main__":
    print(find_cheapest_price(4, [[0, 1, 100], [1, 2, 100], [2, 0, 100], [1, 3, 600], [2, 3, 200]], 0, 3, 1))  # 700
    print(find_cheapest_price(3, [[0, 1, 100], [1, 2, 100], [0, 2, 500]], 0, 2, 1))  # 200
    print(find_cheapest_price(3, [[0, 1, 100], [1, 2, 100], [0, 2, 500]], 0, 2, 0))  # 500
    print(find_cheapest_price(3, [[0, 1, 100], [1, 2, 100]], 0, 2, 0))  # -1
// Cheapest price from src to dst using at most k stops (so at most k + 1
// flights), where each flight is [u, v, price]. -1 if no such route exists.
export function findCheapestPrice(n: number, flights: number[][], src: number, dst: number, k: number): number {
  let prices = new Array<number>(n).fill(Infinity);
  prices[src] = 0;
  // After round i, prices[v] is the cheapest way to reach v in at most i flights.
  for (let round = 1; round <= k + 1; round++) {
    // Read last round's prices and write into a copy. Updating in place would let
    // one round chain several flights and slip past the stop limit.
    const nextPrices = [...prices];
    for (const [u, v, price] of flights) {
      // Only fly out of cities an earlier round already reached.
      if (prices[u] !== Infinity && prices[u] + price < nextPrices[v]) {
        nextPrices[v] = prices[u] + price;
      }
    }
    prices = nextPrices;
  }
  return prices[dst] === Infinity ? -1 : prices[dst];
}

console.log(findCheapestPrice(4, [[0, 1, 100], [1, 2, 100], [2, 0, 100], [1, 3, 600], [2, 3, 200]], 0, 3, 1)); // 700
console.log(findCheapestPrice(3, [[0, 1, 100], [1, 2, 100], [0, 2, 500]], 0, 2, 1)); // 200
console.log(findCheapestPrice(3, [[0, 1, 100], [1, 2, 100], [0, 2, 500]], 0, 2, 0)); // 500
console.log(findCheapestPrice(3, [[0, 1, 100], [1, 2, 100]], 0, 2, 0)); // -1
package main

import (
	"fmt"
	"math"
	"slices"
)

// findCheapestPrice returns the cheapest price from src to dst using at most
// k stops (so at most k + 1 flights), where each flight is [u, v, price]. It
// returns -1 if no such route exists.
func findCheapestPrice(n int, flights [][]int, src int, dst int, k int) int {
	prices := make([]int, n)
	for i := range prices {
		prices[i] = math.MaxInt
	}
	prices[src] = 0
	// After round i, prices[v] is the cheapest way to reach v in at most i flights.
	for round := 1; round <= k+1; round++ {
		// Read last round's prices and write into a copy. Updating in place would let
		// one round chain several flights and slip past the stop limit.
		nextPrices := slices.Clone(prices)
		for _, f := range flights {
			u, v, price := f[0], f[1], f[2]
			// Only fly out of cities an earlier round already reached.
			if prices[u] != math.MaxInt && prices[u]+price < nextPrices[v] {
				nextPrices[v] = prices[u] + price
			}
		}
		prices = nextPrices
	}
	if prices[dst] == math.MaxInt {
		return -1
	}
	return prices[dst]
}

func main() {
	fmt.Println(findCheapestPrice(4, [][]int{{0, 1, 100}, {1, 2, 100}, {2, 0, 100}, {1, 3, 600}, {2, 3, 200}}, 0, 3, 1)) // 700
	fmt.Println(findCheapestPrice(3, [][]int{{0, 1, 100}, {1, 2, 100}, {0, 2, 500}}, 0, 2, 1))                           // 200
	fmt.Println(findCheapestPrice(3, [][]int{{0, 1, 100}, {1, 2, 100}, {0, 2, 500}}, 0, 2, 0))                           // 500
	fmt.Println(findCheapestPrice(3, [][]int{{0, 1, 100}, {1, 2, 100}}, 0, 2, 0))                                        // -1
}
using System;

public class Solution
{
    // Cheapest price from src to dst using at most k stops (so at most k + 1
    // flights), where each flight is [u, v, price]. -1 if no such route exists.
    public static int FindCheapestPrice(int n, int[][] flights, int src, int dst, int k)
    {
        var prices = new int[n];
        Array.Fill(prices, int.MaxValue);
        prices[src] = 0;
        // After round i, prices[v] is the cheapest way to reach v in at most i flights.
        for (int round = 1; round <= k + 1; round++)
        {
            // Read last round's prices and write into a copy. Updating in place would let
            // one round chain several flights and slip past the stop limit.
            var nextPrices = (int[])prices.Clone();
            foreach (var flight in flights)
            {
                int u = flight[0], v = flight[1], price = flight[2];
                // Only fly out of cities an earlier round already reached.
                if (prices[u] != int.MaxValue && prices[u] + price < nextPrices[v])
                {
                    nextPrices[v] = prices[u] + price;
                }
            }
            prices = nextPrices;
        }
        return prices[dst] == int.MaxValue ? -1 : prices[dst];
    }

    public static void Main()
    {
        Console.WriteLine(FindCheapestPrice(4, [[0, 1, 100], [1, 2, 100], [2, 0, 100], [1, 3, 600], [2, 3, 200]], 0, 3, 1)); // 700
        Console.WriteLine(FindCheapestPrice(3, [[0, 1, 100], [1, 2, 100], [0, 2, 500]], 0, 2, 1)); // 200
        Console.WriteLine(FindCheapestPrice(3, [[0, 1, 100], [1, 2, 100], [0, 2, 500]], 0, 2, 0)); // 500
        Console.WriteLine(FindCheapestPrice(3, [[0, 1, 100], [1, 2, 100]], 0, 2, 0)); // -1
    }
}
import java.util.Arrays;

public class Solution {
    // Cheapest price from src to dst using at most k stops (so at most k + 1
    // flights), where each flight is [u, v, price]. -1 if no such route exists.
    static int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
        int[] prices = new int[n];
        Arrays.fill(prices, Integer.MAX_VALUE);
        prices[src] = 0;
        // After round i, prices[v] is the cheapest way to reach v in at most i flights.
        for (int round = 1; round <= k + 1; round++) {
            // Read last round's prices and write into a copy. Updating in place would let
            // one round chain several flights and slip past the stop limit.
            int[] nextPrices = prices.clone();
            for (int[] flight : flights) {
                int u = flight[0], v = flight[1], price = flight[2];
                // Only fly out of cities an earlier round already reached.
                if (prices[u] != Integer.MAX_VALUE && prices[u] + price < nextPrices[v]) {
                    nextPrices[v] = prices[u] + price;
                }
            }
            prices = nextPrices;
        }
        return prices[dst] == Integer.MAX_VALUE ? -1 : prices[dst];
    }

    public static void main(String[] args) {
        System.out.println(findCheapestPrice(4, new int[][] {{0, 1, 100}, {1, 2, 100}, {2, 0, 100}, {1, 3, 600}, {2, 3, 200}}, 0, 3, 1)); // 700
        System.out.println(findCheapestPrice(3, new int[][] {{0, 1, 100}, {1, 2, 100}, {0, 2, 500}}, 0, 2, 1)); // 200
        System.out.println(findCheapestPrice(3, new int[][] {{0, 1, 100}, {1, 2, 100}, {0, 2, 500}}, 0, 2, 0)); // 500
        System.out.println(findCheapestPrice(3, new int[][] {{0, 1, 100}, {1, 2, 100}}, 0, 2, 0)); // -1
    }
}

Common Pitfalls

  • Running Dijkstra on a graph with negative edges. It settles nodes too early and returns wrong distances, or, in the lazy version, can take exponential time. Use Bellman-Ford.
  • Treating a node as final when it's pushed, the way BFS marks nodes. In Dijkstra a node's first push often isn't its shortest distance. Only its first pop is.
  • Dropping the stale-entry check. The answers stay correct, but a node gets expanded again for every outdated entry, which wastes time on dense graphs.
  • Adding a weight to an infinity sentinel like Integer.MAX_VALUE. It overflows to a large negative number that looks like a great path, so check for infinity before adding.
  • Relaxing in place when the number of edges is capped, which lets one round chain several edges. Also count carefully: k stops means k + 1 flights, so run k + 1 rounds.

Practice

More problems to try on your own.