TL;DR
Bellman-Ford limited to k + 1 relaxation rounds β O(k Β· E) time, O(n) space.
Approach 1 β Brute force (DFS over all routes)
Try every route from src that uses at most k + 1 flights, tracking the running cost and keeping the best price that reaches dst. Pruning when the running cost already exceeds the best found keeps it from looping forever (prices are positive).
class Solution:
def findCheapestPrice(self, n: int, flights: list[list[int]],
src: int, dst: int, k: int) -> int:
adj = [[] for _ in range(n)]
for u, v, price in flights:
adj[u].append((v, price))
best = [float("inf")]
def dfs(node: int, edges_left: int, cost: int) -> None:
if cost >= best[0]:
return
if node == dst:
best[0] = cost
return
if edges_left == 0:
return
for nxt, price in adj[node]:
dfs(nxt, edges_left - 1, cost + price)
dfs(src, k + 1, 0)
return -1 if best[0] == float("inf") else best[0]
Complexity: worst case O(n^(k+1)) time (branching factor up to n, depth k + 1), O(k) recursion space. With n = 100 and k up to 99, the route count explodes β the constraints kill it.
Approach 2 β Bellman-Ford, capped at k + 1 rounds
The insight: Bellman-Ford (the classic dynamic-programming shortest-path algorithm that relaxes every edge once per round) has an invariant tailor-made for this problem: after r rounds, dist[v] holds the cheapest cost to reach v using at most r edges. βAt most k stopsβ is exactly βat most k + 1 edgesβ β so run precisely k + 1 rounds and stop. One subtlety: within a round, relax against a snapshot of the previous roundβs distances, otherwise one round could chain several new edges together and overshoot the edge budget.
class Solution:
def findCheapestPrice(self, n: int, flights: list[list[int]],
src: int, dst: int, k: int) -> int:
INF = float("inf")
dist = [INF] * n
dist[src] = 0
for _ in range(k + 1):
prev = dist[:]
for u, v, price in flights:
if prev[u] + price < dist[v]:
dist[v] = prev[u] + price
return -1 if dist[dst] == INF else dist[dst]
Walkthrough (n = 4, flights [[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]], src = 0, dst = 3, k = 1):
- Start:
dist = [0, INF, INF, INF].
- Round 1 (snapshot
[0, INF, INF, INF]): only edge 0β1 has a finite source, so dist = [0, 100, INF, INF].
- Round 2 (snapshot
[0, 100, INF, INF]): 1β2 gives dist[2] = 200; 1β3 gives dist[3] = 700. dist = [0, 100, 200, 700].
- Two rounds = at most 2 flights = at most 1 stop. Answer:
dist[3] = 700. The 400-cost route through city 2 needs a third flight, and the snapshot discipline correctly kept it out.
Complexity: O(k Β· E) time, O(n) space.
Approach 3 β Dijkstra on (city, edges-used) states
The insight: ordinary Dijkstra (greedy shortest-path: always settle the unvisited node with the smallest known distance, using a min-heap) breaks here because the cheapest way into a city may have burned too many edges. Fix it by making the state richer: run Dijkstra over pairs (city, edges used). The first time we pop dst its cost is optimal, because heap order is by cost. To prune, skip a state if weβve already reached this city using fewer edges β any route through it would have been found cheaper-or-equal already.
import heapq
class Solution:
def findCheapestPrice(self, n: int, flights: list[list[int]],
src: int, dst: int, k: int) -> int:
adj = [[] for _ in range(n)]
for u, v, price in flights:
adj[u].append((v, price))
best_edges = [float("inf")] * n
heap = [(0, 0, src)] # (cost, edges used, city)
while heap:
cost, edges, node = heapq.heappop(heap)
if node == dst:
return cost
if edges > k or edges >= best_edges[node]:
continue
best_edges[node] = edges
for nxt, price in adj[node]:
heapq.heappush(heap, (cost + price, edges + 1, nxt))
return -1
Walkthrough (same example, k = 1): pop (0, 0, 0), push (100, 1, 1). Pop (100, 1, 1), push (200, 2, 2) and (700, 2, 3). Pop (200, 2, 2): edges = 2 > k, skip β city 2 is a dead end under this budget. Pop (700, 2, 3): itβs dst, return 700.
Complexity: each city can enter the heap once per edge count, so O(E Β· k) heap entries β O(E Β· k Β· log(E Β· k)) time, O(E Β· k) space. Fine here, but note Bellman-Ford is both simpler and asymptotically better for this problem.
Common pitfalls
- Relaxing against the live
dist array instead of a snapshot β one Bellman-Ford round then chains multiple edges and violates the stop budget.
- Confusing stops with edges:
k stops means k + 1 flights, so the loop runs k + 1 times.
- Using plain Dijkstra with a per-city visited set β it discards more-expensive-but-fewer-edges routes that are the only legal way to reach
dst.
- Forgetting the unreachable case: return
-1 when dst still holds infinity.
Pattern takeaway
When a shortest-path problem adds a budget (edge count, stops, fuel), the plain node-distance table is no longer a valid state space. Either bound the rounds of Bellman-Ford (each round = one more edge allowed) or expand Dijkstraβs state to (node, budget used). Bellman-Fordβs round-by-round invariant is the cleanest tool whenever the budget is on the number of edges.