Menu
DSA interview questionsQuestion 41 of 147

DSA interview question · Question 41 of 147

Cheapest Flights Within K Stops: Bounded Bellman-Ford for Limited Hops

  • Medium
  • coding
  • ~25 min
  • High relevance
  • 7 min read
  • Updated Oct 2026

Short answer

A route with at most k stops uses at most k + 1 flights, and round i of Bellman-Ford finds the cheapest prices using at most i edges. So run exactly k + 1 rounds, and in each round relax every flight from a copy of the previous round's prices, so one round cannot chain two flights. The answer is the destination's price after the last round, or -1. This is O(k * E) time and O(n) space. Plain Dijkstra on price alone is wrong because a cheaper path may use too many stops.

On this page
  1. Problem
  2. Examples
  3. Approach 1: DFS over routes with a stop budget
  4. Approach 2: optimal, k + 1 rounds of Bellman-Ford
  5. State and recurrence
  6. Python solution
  7. Alternative: Dijkstra with stops in the state
  8. Complexity
  9. Tests
  10. Edge cases and pitfalls
  11. Where this shows up in data engineering

Problem

There are n cities labelled 0 to n - 1 and a list of one-way flights (from, to, price). Given a source, a destination and a number k, return the lowest total price to fly from the source to the destination with at most k stops in between (so at most k + 1 flights). Return -1 if no such route exists.

This is widely known as LeetCode 787, “Cheapest Flights Within K Stops”.

Assume up to 100 cities, no duplicate flights between the same pair, positive prices, and k below n.

Examples

n = 4, flights: 0->1 (50), 1->2 (50), 2->3 (50), 0->2 (200), 0->3 (400)
src 0, dst 3
k = 2: 0->1->2->3 costs 150 (2 stops)            -> 150
k = 1: 0->2->3 costs 250 (1 stop)                -> 250
k = 0: only the direct flight 0->3 costs 400      -> 400

n = 3, flights: 0->1 (10), src 0, dst 2, k = 5    -> -1

Approach 1: DFS over routes with a stop budget

Explore every route from the source, stopping when the budget runs out or the price already exceeds the best found.

from collections import defaultdict

def cheapest_dfs(n, flights, src, dst, k):
    graph = defaultdict(list)
    for u, v, p in flights:
        graph[u].append((v, p))
    best = float("inf")

    def dfs(city, cost, flights_left, on_path):
        nonlocal best
        if cost >= best:
            return
        if city == dst:
            best = cost
            return
        if flights_left == 0:
            return
        for nxt, price in graph[city]:
            if nxt not in on_path:
                on_path.add(nxt)
                dfs(nxt, cost + price, flights_left - 1, on_path)
                on_path.remove(nxt)

    dfs(src, 0, k + 1, {src})
    return -1 if best == float("inf") else best

The number of routes grows exponentially with k, so this only works for small graphs.

Approach 2: optimal, k + 1 rounds of Bellman-Ford

State and recurrence

Let prices_i[v] be the cheapest price to reach v using at most i flights.

  • Base case: prices_0[src] = 0, every other city is infinity.
  • Recurrence: prices_i[v] = min(prices_(i-1)[v], min over flights u->v of prices_(i-1)[u] + price).
  • Answer: prices_(k+1)[dst].

Reading from the previous round (a copy) is what enforces the limit; updating in place could use two flights in one round.

Filled table for the first example (cities 0..3):

round (max flights) 0 1 2 3
0 0 inf inf inf
1 0 50 200 400
2 0 50 100 250
3 0 50 100 150

So k = 0 reads row 1 (400), k = 1 reads row 2 (250) and k = 2 reads row 3 (150).

Python solution

def find_cheapest_price(n, flights, src, dst, k):
    INF = float("inf")
    prices = [INF] * n
    prices[src] = 0
    for _ in range(k + 1):
        nxt = prices[:]                       # previous round stays untouched
        for u, v, p in flights:
            if prices[u] + p < nxt[v]:
                nxt[v] = prices[u] + p
        prices = nxt
    return -1 if prices[dst] == INF else prices[dst]

Alternative: Dijkstra with stops in the state

A heap of (cost, city, flights_used) also works if you allow revisiting a city with fewer flights used. Keep the fewest flights seen per city and skip a popped state that is both more expensive (guaranteed by heap order) and not using fewer flights.

import heapq

def find_cheapest_price_heap(n, flights, src, dst, k):
    graph = defaultdict(list)
    for u, v, p in flights:
        graph[u].append((v, p))
    fewest = [float("inf")] * n               # fewest flights used on arrival so far
    heap = [(0, src, 0)]
    while heap:
        cost, city, used = heapq.heappop(heap)
        if city == dst:
            return cost
        if used >= fewest[city] or used == k + 1:
            continue
        fewest[city] = used
        for nxt, price in graph[city]:
            heapq.heappush(heap, (cost + price, nxt, used + 1))
    return -1

Complexity

  • Bounded Bellman-Ford: O(k * E) time, O(n) space.
  • Heap version: O(E * k * log(E * k)) in the worst case, often faster in practice.

Tests

fl = [(0, 1, 50), (1, 2, 50), (2, 3, 50), (0, 2, 200), (0, 3, 400)]
for fn in (find_cheapest_price, find_cheapest_price_heap, cheapest_dfs):
    assert fn(4, fl, 0, 3, 2) == 150
    assert fn(4, fl, 0, 3, 1) == 250
    assert fn(4, fl, 0, 3, 0) == 400
    assert fn(3, [(0, 1, 10)], 0, 2, 5) == -1          # unreachable
    assert fn(2, [], 0, 1, 1) == -1                     # no flights
    assert fn(2, [(0, 1, 7)], 0, 1, 0) == 7             # direct only
    assert fn(3, [(0, 1, 1), (1, 2, 1)], 0, 2, 0) == -1  # needs one stop, k = 0
    assert fn(3, [(0, 1, 1), (1, 0, 1), (1, 2, 5)], 0, 2, 3) == 6   # cycle present

# Random agreement
import random
random.seed(2)
for _ in range(60):
    n = random.randint(2, 6)
    pairs = {(random.randrange(n), random.randrange(n)) for _ in range(10)}
    fs = [(u, v, random.randint(1, 20)) for u, v in pairs if u != v]
    k = random.randint(0, 3)
    r = find_cheapest_price(n, fs, 0, n - 1, k)
    assert r == find_cheapest_price_heap(n, fs, 0, n - 1, k) == cheapest_dfs(n, fs, 0, n - 1, k)

Edge cases and pitfalls

  • Stops versus flights. k stops means k + 1 flights; off-by-one here is the most common bug.
  • Relaxing in place lets a single round use several flights and breaks the limit.
  • Plain Dijkstra settles each city once by price, so it can lock in a cheap path with too many stops and miss a slightly dearer path that fits the limit.
  • Source equals destination costs 0.
  • Unreachable destination returns -1.

Where this shows up in data engineering

Bounded-hop queries appear in lineage and network analysis: “which sources feed this table within three transformations”, or “cheapest data transfer route across regions using at most one intermediate hop”. The round-by-round relaxation also maps neatly onto iterative SQL or Spark jobs, where each iteration is one join of the frontier with the edge table.

By Data Career Hub Editorial · Last reviewed Oct 2026 · Python 3 solutions verified with assert-based tests

Progress is saved in this browser only. No account needed.

Search
Filter by type