787. Cheapest Flights Within K Stops

Find the cheapest one-way flight path from src to dst using at most k stops. The trick is a snapshot-based Bellman-Ford run exactly k+1 times — the snapshot prevents chaining more than one edge per round, which is the whole constraint.

MediumBellman-FordDynamic ProgrammingBFSTypeScript

PROBLEM What we're solving

You have n cities and a list of directed flights, each with a cost. Find the cheapest path from src to dst using at most k intermediate stops (so at most k+1 edges). Return -1 if no such path exists.

Worked example. n=3, flights [[0,1,100],[1,2,100],[0,2,500]], src=0, dst=2, k=1. Two paths reach city 2: 0→2 costs 500; 0→1→2 costs 200 and uses 1 stop (within limit). Answer: 200.

KEY IDEA Bellman-Ford + snapshot enforces the stop limit

Insight → Run Bellman-Ford exactly k+1 times. After round r, dist[v] holds the cheapest cost reachable in exactly at most r edges — but only if you copy dist into a snapshot before each round and relax using the snapshot. Without the copy, a single round could chain multiple edges (following the path 0→1→2 in one pass), silently violating the k-stop budget.

RECIPE k+1 Bellman-Ford rounds, snapshot each time

  • 0 · Init. Set dist[src] = 0 and dist[i] = ∞for all other cities. This represents "0 edges used, only src reachable."
  • 1 · Loop k+1 times. Each iteration adds one more allowed edge, so after k+1 rounds the budget is exhausted.
  • 2 · Snapshot. Copy dist into prev at the start of each round. All reads in this round use prev, all writes go to dist. This is the critical step.
  • 3 · Relax every edge. For each flight [from, to, cost]: if prev[from] + cost < dist[to], update dist[to].
  • 4 · Answer. Return dist[dst] or -1 if still .
Classic confusion → forgetting the snapshot is the single most common bug. If you write dist[from] + cost (live array) instead of prev[from] + cost (snapshot), a single round can chain edges across the whole graph — so you get the cheapest path ignoring the stop limit, not with it. Always take the snapshot.

COST Complexity & alternatives

Dijkstra ignoring k
O(E log V)
Wrong: can't enforce the stop count without state explosion.
Bellman-Ford (k+1 rounds)
O(k · E)
O(n) space; straightforward loop with snapshot copy.

Space note

Only two arrays (dist and prev) of length n are needed — O(n) space. A BFS/Dijkstra with (city, stops) state pairs would use O(n·k) space.

Pattern transfer → any shortest-path problem where you must count edges or hops (not just cost) reaches for this Bellman-Ford trick. See also: Minimum Cost to Reach Destination in Time (LC 1928), Path With Minimum Effort (LC 1631), and Reachable Nodes in Subdivided Graph(LC 882). The "BFS layer by layer" approach (one round = one hop) is the same idea under a different name.

RUN IT Bellman-Ford k+1 rounds with snapshot

step 0 / 11
STARTBellman-Ford, 2 rounds. Init dist[0] = 0; everything else . Each round uses a snapshot so we can't chain relaxations.
1function findCheapestPrice(
2 n: number,
3 flights: number[][],
4 src: number,
5 dst: number,
6 k: number
7): number {
8 const INF = Infinity;
9 // dist[i] = cheapest cost to reach city i using at most (round) edges so far
10 let dist: number[] = Array(n).fill(INF);
11 dist[src] = 0;
12
13 for (let round = 0; round < k + 1; round++) {
14 // Snapshot: each round can only use prices from the PREVIOUS round.
15 // Without this copy, a chain of updates within one round would let us
16 // traverse more than 1 edge per round — violating the k-stop constraint.
17 const prev = [...dist];
18
19 for (const [from, to, cost] of flights) {
20 if (prev[from] === INF) continue; // city not yet reachable
21 if (prev[from] + cost < dist[to]) {
22 dist[to] = prev[from] + cost;
23 }
24 }
25 }
26
27 return dist[dst] === INF ? -1 : dist[dst];
28}
dist[]0012
State
round
[0, ∞, ∞]
dist[]
prev[]
0
src
2
dst
1
k
source citybeing relaxeddestination reachedunreachable / skipped
slowfast

TYPESCRIPT The solution, annotated

findCheapestPrice.ts
function findCheapestPrice(
  n: number,
  flights: number[][],
  src: number,
  dst: number,
  k: number
): number {
  const INF = Infinity;
  // dist[i] = cheapest cost to reach city i using at most (round) edges so far
  let dist: number[] = Array(n).fill(INF);
  dist[src] = 0;

  for (let round = 0; round < k + 1; round++) {
    // Snapshot: each round can only use prices from the PREVIOUS round.
    // Without this copy, a chain of updates within one round would let us
    // traverse more than 1 edge per round — violating the k-stop constraint.
    const prev = [...dist];

    for (const [from, to, cost] of flights) {
      if (prev[from] === INF) continue;            // city not yet reachable
      if (prev[from] + cost < dist[to]) {
        dist[to] = prev[from] + cost;
      }
    }
  }

  return dist[dst] === INF ? -1 : dist[dst];
}

Reading it block by block

Lines 8–10 — initialise distances. dist[src] = 0; every other city starts at Infinity. After zero rounds only the source is reachable with zero cost.
Line 12 — loop k+1 times. We allow at most k intermediate stops, meaning at most k+1 edges. One loop iteration = one additional edge allowed.
Line 17 — snapshot. const prev = [...dist] freezes the state from the previous round. Every read in this loop uses prev, not dist. Without this, a single iteration could walk an arbitrarily long path, breaking the constraint.
Lines 19–22 — relax all edges. For each flight, check whether going through from (at last round's cost) gives a cheaper route to to.prev[from] === INF means from was unreachable; skip it to avoid arithmetic on Infinity.
Line 26 — return. After k+1 relaxation passes, dist[dst] is the minimum cost reachable within the budget, or -1 if still Infinity.
Complexity → O(k · E) time — k+1 rounds each scanning all E edges. Space is O(n) for the two distance arrays (no graph adjacency structure needed beyond the edge list).

INTERVIEWFollow-ups they'll ask

  • "Can you reconstruct the actual path?" Keep a parent array and update it whenever you relax an edge. Then trace back from dst to src.
  • "What if k is very large (no stop limit)?" Switch to standard Dijkstra — O(E log V) — since the stop count is no longer the binding constraint and Dijkstra is faster than O(n · E) Bellman-Ford.
  • "What if there are negative-cost edges?" Bellman-Ford handles negative weights correctly (unlike Dijkstra). The snapshot still enforces the stop count. Negative cycles within the budget would require an extra check.
  • "BFS alternative?" Yes — BFS layer-by-layer where each layer represents one hop gives the same result. It uses O(n·k) state but is often more intuitive to explain.
  • "What if multiple queries share the same graph?" Precompute a full n × n Bellman-Ford DP table (dp[stops][city]) and answer each query in O(1).

OPTIMAL Bellman-Ford

findCheapestPrice.ts
function findCheapestPrice(
  n: number,
  flights: number[][],
  src: number,
  dst: number,
  k: number
): number {
  const INF = Infinity;
  // dist[i] = cheapest cost to reach city i using at most (round) edges so far
  let dist: number[] = Array(n).fill(INF);
  dist[src] = 0;

  for (let round = 0; round < k + 1; round++) {
    // Snapshot: each round can only use prices from the PREVIOUS round.
    // Without this copy, a chain of updates within one round would let us
    // traverse more than 1 edge per round — violating the k-stop constraint.
    const prev = [...dist];

    for (const [from, to, cost] of flights) {
      if (prev[from] === INF) continue;            // city not yet reachable
      if (prev[from] + cost < dist[to]) {
        dist[to] = prev[from] + cost;
      }
    }
  }

  return dist[dst] === INF ? -1 : dist[dst];
}
Complexity → O(k · E) time — k+1 rounds each scanning all E edges. Space is O(n) for the two distance arrays (no graph adjacency structure needed beyond the edge list).

ALT 1 Brute force — DFS every path within the stop budget

O(nᵏ) time · O(n + E) space

Build an adjacency list and recursively explore every flight out of the current city, decrementing the remaining-stops budget, and keep the cheapest total cost that reaches dst before the budget runs out.

approach-2.ts
function findCheapestPrice(
  n: number,
  flights: number[][],
  src: number,
  dst: number,
  k: number
): number {
  const adj: [number, number][][] = Array.from({ length: n }, () => []);
  for (const [from, to, cost] of flights) adj[from].push([to, cost]);

  let best = Infinity;

  // stops = number of intermediate cities still allowed
  function dfs(city: number, stops: number, cost: number): void {
    if (cost >= best) return;          // prune: already worse than known best
    if (city === dst) { best = cost; return; }
    if (stops < 0) return;             // out of budget
    for (const [next, price] of adj[city]) {
      dfs(next, stops - 1, cost + price);
    }
  }

  dfs(src, k, 0);
  return best === Infinity ? -1 : best;
}
Note → With no per-city memo the recursion fans out along every path, which is exponential in k and revisits the same (city, stops) states over and over. Bellman-Ford relaxes all edges k + 1 rounds for O(k · E) instead.

MNEMONIC The one-liner

"Snapshot before you relax — one round, one hop, k+1 rounds total."

TRIGGERS When you see ___ → reach for ___

"cheapest path with at most k edges/stops"Bellman-Ford k+1 rounds + snapshot
edge count / hop count is the budgetBFS by layers or snapshot Bellman-Ford
shortest path on directed weighted graphBellman-Ford (negative ok) or Dijkstra (non-negative)
"return -1 if unreachable within constraint"dist[dst] === INF ? -1 : dist[dst]

SKELETON The reusable shape

skeleton.ts
let dist: number[] = Array(n).fill(Infinity);
dist[src] = 0;

for (let round = 0; round < k + 1; round++) {
  const prev = [...dist];                      // snapshot — key step
  for (const [from, to, cost] of flights) {
    if (prev[from] === Infinity) continue;
    if (prev[from] + cost < dist[to]) {
      dist[to] = prev[from] + cost;
    }
  }
}

return dist[dst] === Infinity ? -1 : dist[dst];

FLASHCARDS Tap to flip

Why take a snapshot at the start of each round?
Without it, a single round can chain multiple edges (0→1→2 in one pass), silently exceeding the stop budget.
tap to flip
1 / 8
Answer to reveal explanations. No penalty for retries.Score: 0 / 9
QUESTION 01
Given n=3, flights [[0,1,100],[1,2,100],[0,2,500]], src=0, dst=2, k=1, what is the answer?
QUESTION 02
Why must you copy dist into a snapshot at the start of each Bellman-Ford round?
QUESTION 03
What is the time complexity of this algorithm?
QUESTION 04
Same graph as Q1 but k=0. What is returned?
QUESTION 05
After r completed rounds (with snapshot), dist[v] represents:
QUESTION 06
What is the space complexity?
QUESTION 07
Which alternative approach is conceptually identical to snapshot Bellman-Ford?
QUESTION 08
#787 · Cheapest Flights Within K StopsBellman-Ford relaxed exactly k+1 times using a snapshot of distances from the previous round, so a single round cannot chain more than one hop. The answer is the snapshot distance to dst after k+1 relaxations.Which algorithmic approach does this primarily use?
QUESTION 09
#787 · Cheapest Flights Within K StopsBellman-Ford relaxed exactly k+1 times using a snapshot of distances from the previous round, so a single round cannot chain more than one hop. The answer is the snapshot distance to dst after k+1 relaxations.Which implementation correctly solves it?