DSAPrep
MediumAdvanced Graphs

Cheapest Flights Within K Stops

There are n cities connected by some number of flights. You are given an array flights where flights[i] = [from_i, to_i, price_i] indicates a flight from city from_i to city to_i with cost price_i.

You are also given three integers src, dst, and k. Return the cheapest price from src to dst with at most k stops. If there is no such route, return -1.

Example 1

Input: n = 4, flights = [[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]], src = 0, dst = 3, k = 1
Output: 700
Explanation: The cheapest path with at most 1 stop is 0 -> 1 -> 3, costing 100 + 600 = 700. The path 0 -> 1 -> 2 -> 3 is cheaper (300) but uses 2 stops, exceeding k = 1.

Example 2

Input: n = 3, flights = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 1
Output: 200
Explanation: The path 0 -> 1 -> 2 uses 1 stop and costs 100 + 100 = 200.

Example 3

Input: n = 3, flights = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 0
Output: 500
Explanation: With 0 stops allowed, only the direct flight 0 -> 2 is valid, costing 500.

Constraints

  • 2 <= n <= 100
  • 0 <= flights.length <= n * (n - 1) / 2
  • flights[i].length == 3
  • 0 <= from_i, to_i < n
  • from_i != to_i
  • 1 <= price_i <= 10^4
  • There will not be any multiple flights between two cities
  • 0 <= src, dst, k < n
  • src != dst
View original on LeetCode β†—

Plain Dijkstra doesn’t directly apply here because the cheapest path might use more stops than a k-stop-respecting one β€” the stop limit changes which paths are even legal, so the state we search over must be (city, stops used so far), not just city. This is the same β€œresource-constrained shortest path” shape as Bellman-Ford’s round-by-round relaxation, which turns out to be exactly the right tool.

DFS with Memoization on (node, stops remaining)

Time O(V + E Β· k)Space O(V Β· k)

Explore every path from src via DFS, tracking how many stops remain, and memoize on (current city, stops remaining) since the cheapest cost to reach dst from a given city with a given stop budget doesn’t depend on how you got there.

from collections import defaultdict
class Solution:
def findCheapestPrice(self, n: int, flights: list[list[int]], src: int, dst: int, k: int) -> int:
graph = defaultdict(list)
for u, v, w in flights:
graph[u].append((v, w))
memo = {}
def dfs(node: int, stops: int) -> float:
if node == dst:
return 0
if stops < 0:
return float('inf')
if (node, stops) in memo:
return memo[(node, stops)]
best = float('inf')
for nxt, price in graph[node]:
cost = dfs(nxt, stops - 1)
if cost != float('inf'):
best = min(best, price + cost)
memo[(node, stops)] = best
return best
result = dfs(src, k)
return -1 if result == float('inf') else result

Each of the O(V Β· k) distinct (node, stops) states is computed once and reused; computing a state costs O(deg(node)), so total work is O(V Β· k + E Β· k) β‰ˆ O(E Β· k) time in dense graphs, with O(V Β· k) space for the memo table plus recursion stack.

Bellman-Ford, Limited to k + 1 Rounds

OptimalTime O(k Β· E)Space O(V)

Bellman-Ford’s relaxation naturally models β€œat most k stops”: each round of relaxing every edge extends the reachable path length by exactly one edge. So running only k + 1 rounds (allowing up to k + 1 edges = at most k intermediate stops) directly enforces the stop limit, without needing to track (node, stops) pairs explicitly β€” the round number is the stop count.

The one subtlety: relax using a snapshot of the previous round’s distances, not the array being updated in place. Otherwise a single round could chain multiple edge relaxations together, silently using more β€œhops” than that round is supposed to represent.

class Solution:
def findCheapestPrice(self, n: int, flights: list[list[int]], src: int, dst: int, k: int) -> int:
dist = [float('inf')] * n
dist[src] = 0
for _ in range(k + 1): # k stops = k + 1 edges
new_dist = dist[:] # snapshot: don't let this round chain onto itself
for u, v, price in flights:
if dist[u] != float('inf') and dist[u] + price < new_dist[v]:
new_dist[v] = dist[u] + price
dist = new_dist
return dist[dst] if dist[dst] != float('inf') else -1

Tracing n=4, flights=[[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]], src=0, dst=3, k=1 (2 rounds = k + 1): watch the frozen snapshot. Every relaxation reads only those slate values, while each update lands in the separate building row below β€” so one round can never chain two edges into the same path, which is the whole reason 0β†’1β†’2β†’3 cannot sneak past the stop limit.

round 1 β€” liveround 2round 1 of 2

Frozen snapshot β€” every relaxation reads only these values

0node 0
∞node 1
∞node 2
∞node 3

New distances β€” built one edge at a time

0node 0
100node 1
∞node 2
∞node 3
test 0β†’1 Β· 100 snapshot[0] = 0 Β· 0 + 100 < ∞ β†’ new[1] = 100

Edges β€” scanned in order

1/5 tested this round
0β†’1 Β· 1001β†’2 Β· 1002β†’0 Β· 1001β†’3 Β· 6002β†’3 Β· 200
1 / 12
edge being testedupdate firedfrozen snapshot / unchangedblocked β€” source ∞ in snapshotround in progress

Round 1 of 2 opens. The snapshot freezes the previous round result β€” `[0, ∞, ∞, ∞]` β€” and the new row below it is built one edge at a time. First edge `0β†’1`: the snapshot says `dist[0] = 0`, and `0 + 100 < ∞`, so `new[1] = 100` pops in (emerald). The snapshot itself never moves, because edges tested later this round must still see the old `∞`.

After 2 rounds, dist[3] = 700 β€” matching the expected output. The tempting path 0β†’1β†’2β†’3 costs 100 + 100 + 200 = 400 β€” genuinely cheaper β€” but it uses 3 edges (2 stops), and a path only gains one edge per round from the fixed starting snapshot, so reaching it would need a third round that k = 1 forbids.

Why it’s correct: by induction, after round i, dist[v] holds the cheapest cost to reach v using at most i edges β€” a direct consequence of the Bellman-Ford invariant, here deliberately capped at k + 1 rounds instead of running to convergence (n - 1 rounds) specifically to enforce the stop constraint.

Complexity: k + 1 rounds, each scanning all E edges β€” O(k Β· E) time (and since k < n, this is also bounded by the more familiar O(n Β· E)). Space is O(V) for the two distance arrays, versus O(V Β· k) for the memoized DFS β€” the main advantage of this approach.