787. Cheapest Flights Within K Stops - Explanation

Problem Link

Description

There are n airports, labeled from 0 to n - 1, which are connected by some flights. You are given an array flights where flights[i] = [from_i, to_i, price_i] represents a one-way flight from airport from_i to airport to_i with cost price_i. You may assume there are no duplicate flights and no flights from an airport to itself.

You are also given three integers src, dst, and k where:

  • src is the starting airport
  • dst is the destination airport
  • src != dst
  • k is the maximum number of stops you can make (not including src and dst)

Return the cheapest price from src to dst with at most k stops, or return -1 if it is impossible.

Example 1:

Input: n = 4, flights = [[0,1,200],[1,2,100],[1,3,300],[2,3,100]], src = 0, dst = 3, k = 1

Output: 500

Explanation:
The optimal path with at most 1 stop from airport 0 to 3 is shown in red, with total cost 200 + 300 = 500.
Note that the path [0 -> 1 -> 2 -> 3] costs only 400, and thus is cheaper, but it requires 2 stops, which is more than k.

Example 2:

Input: n = 3, flights = [[1,0,100],[1,2,200],[0,2,100]], src = 1, dst = 2, k = 1

Output: 200

Explanation:
The optimal path with at most 1 stop from airport 1 to 2 is shown in red and has cost 200.

Constraints:

  • 1 <= n <= 100
  • fromi != toi
  • 1 <= pricei <= 1000
  • 0 <= src, dst, k < n


Topics

Recommended Time & Space Complexity

You should aim for a solution as good or better than O(n + (m * k)) time and O(n) space, where n is the number of cities, m is the number of flights, and k is the number of stops.


Hint 1

Consider this as a graph problem where the cities are nodes and the flights are edges connecting two cities, with the ticket cost as the edge weight. Can you think of a shortest path algorithm to solve the problem? Perhaps a better algorithm than Dijkstra's that can intuitively handle the k stops condition.


Hint 2

We can use the Bellman-Ford algorithm. Initialize a prices array of size n with Infinity, setting prices[source] = 0. These values describe the cost to reach a city from the source city. Iterate (k + 1) times (stops are 0-indexed), updating the cost to each city by extending paths from cities with valid costs. We only update the cost for a city if it is less than the previous cost. How would you implement this?


Hint 3

At each level of iteration, we go through the given flights and use them to update the price array with the minimum costs compared to the previous level. We use a temporary prices array at each level to store the updated costs. After completing all levels, we return the result stored in prices[dst]. If that value is Infinity, we return -1 instead.


Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Graph Representation - Building adjacency lists to represent weighted directed graphs
  • Dijkstra's Algorithm - Finding shortest paths in weighted graphs using a min-heap/priority queue
  • Bellman-Ford Algorithm - Relaxing edges iteratively to find shortest paths with edge count constraints
  • Priority Queue/Min-Heap - Efficiently extracting minimum cost states during graph traversal

1. Dijkstra's Algorithm

Intuition

We want the cheapest cost to go from src to dst, but we can take at most k stops (so at most k+1 flights/edges).
Normal Dijkstra finds the cheapest path, but it ignores stop limits.
So we treat a "state" as: (current city, how many stops used).
That way, reaching the same city with different stop counts is considered different, and we can enforce the limit.

We always expand the currently cheapest state first using a min-heap.
The first time we pop dst, that cost is the cheapest possible within the allowed stops.

Algorithm

  1. Build adjacency list: for each flight u -> v with price w.
  2. Create dist[city][stopsUsed] = bestCost (initialize to infinity).
  3. Push (0, src, -1) into a min-heap, meaning cost 0 at src with "-1 stops so far" (so first flight makes stops = 0).
  4. While heap not empty:
    • Pop (cost, city, stops).
    • If city == dst, return cost.
    • If stops == k, you can't take more flights, continue.
    • If this cost is worse than the best recorded for this (city, stops+1) state, skip.
    • For each neighbor (nextCity, price):
      • nextCost = cost + price
      • nextStops = stops + 1
      • If nextCost improves dist[nextCity][nextStops+1], update it and push into heap.
  5. If the heap empties without reaching dst, return -1.
class Solution:
    def findCheapestPrice(self, n: int, flights: List[List[int]], src: int, dst: int, k: int) -> int:
        INF = float("inf")
        adj = [[] for _ in range(n)]
        dist = [[INF] * (k + 5) for _ in range(n)]
        for u, v, cst in flights:
            adj[u].append([v, cst])

        dist[src][0] = 0
        minHeap = [(0, src, -1)] # cost, node, stops
        while len(minHeap):
            cst, node, stops = heapq.heappop(minHeap)
            if dst == node: return cst
            if stops == k or dist[node][stops + 1] < cst:
                continue
            for nei, w in adj[node]:
                nextCst = cst + w
                nextStops = 1 + stops
                if dist[nei][nextStops + 1] > nextCst:
                    dist[nei][nextStops + 1] = nextCst
                    heapq.heappush(minHeap, (nextCst, nei, nextStops))

        return -1

Time & Space Complexity

  • Time complexity: O((n+m)k)O((n + m) * k)
  • Space complexity: O(nk)O(n * k)

Where nn is the number of cities, mm is the number of flights and kk is the number of stops.


2. Bellman Ford Algorithm

Intuition

We are allowed at most k stops, which means at most k + 1 flights (edges).
Bellman–Ford is perfect here because it relaxes edges level by level, where each iteration allows one more edge in the path.

Key idea:

  • After i iterations, we know the cheapest cost to reach every city using at most i flights.
  • By running the relaxation k + 1 times, we ensure we only consider paths that respect the stop limit.
  • We use a temporary array each iteration so that paths from the same iteration don't chain together and accidentally exceed the allowed number of flights.

Algorithm

  1. Initialize prices array with infinity, set prices[src] = 0.
  2. Repeat k + 1 times:
    • Make a copy tmpPrices = prices.
    • For every flight (s -> d, cost):
      • If prices[s] is reachable:
        • Try relaxing the edge: tmpPrices[d] = min(tmpPrices[d], prices[s] + cost)
    • Assign prices = tmpPrices.
  3. After all iterations:
    • If prices[dst] is still infinity, return -1.
    • Otherwise, return prices[dst].
class Solution:
    def findCheapestPrice(self, n: int, flights: List[List[int]], src: int, dst: int, k: int) -> int:
        prices = [float("inf")] * n
        prices[src] = 0

        for i in range(k + 1):
            tmpPrices = prices.copy()

            for s, d, p in flights:  # s=source, d=dest, p=price
                if prices[s] == float("inf"):
                    continue
                if prices[s] + p < tmpPrices[d]:
                    tmpPrices[d] = prices[s] + p
            prices = tmpPrices
        return -1 if prices[dst] == float("inf") else prices[dst]

Time & Space Complexity

  • Time complexity: O(n+(mk))O(n + (m * k))
  • Space complexity: O(n)O(n)

Where nn is the number of cities, mm is the number of flights and kk is the number of stops.


3. Shortest Path Faster Algorithm

Intuition

This problem is still about finding the cheapest path with at most k stops.
SPFA (Shortest Path Faster Algorithm) is essentially a queue-optimized Bellman–Ford.

Key observations:

  • Each time we relax an edge, we may improve the cost to reach a city.
  • However, unlike classic shortest path problems, we must not exceed k stops.
  • So every state in the queue must track:
    • the current city
    • the total cost so far
    • the number of stops used
  • We only continue expanding a path if stops <= k.

This approach works well because:

  • Only promising states (those that improve cost) are pushed into the queue.
  • The stop constraint naturally prevents infinite relaxation loops.

Algorithm

  1. Initialize:
    • prices[i] = infinity for all cities.
    • prices[src] = 0.
  2. Build an adjacency list from the flight data.
  3. Use a queue that stores (currentCost, city, stopsUsed).
    • Start with (0, src, 0).
  4. While the queue is not empty:
    • Pop a state (cost, node, stops).
    • If stops > k, skip it.
    • For each outgoing flight (node -> neighbor, price):
      • Compute nextCost = cost + price.
      • If nextCost < prices[neighbor]:
        • Update prices[neighbor].
        • Push (nextCost, neighbor, stops + 1) into the queue.
  5. After processing:
    • If prices[dst] is still infinity, return -1.
    • Otherwise, return prices[dst].
class Solution:
    def findCheapestPrice(self, n: int, flights: List[List[int]], src: int, dst: int, k: int) -> int:
        prices = [float("inf")] * n
        prices[src] = 0
        adj = [[] for _ in range(n)]
        for u, v, cst in flights:
            adj[u].append([v, cst])

        q = deque([(0, src, 0)])
        while q:
            cst, node, stops = q.popleft()
            if stops > k:
                continue

            for nei, w in adj[node]:
                nextCost = cst + w
                if nextCost < prices[nei]:
                    prices[nei] = nextCost
                    q.append((nextCost, nei, stops + 1))

        return prices[dst] if prices[dst] != float("inf") else -1

Time & Space Complexity

  • Time complexity: O(nk)O(n * k)
  • Space complexity: O(n+m)O(n + m)

Where nn is the number of cities, mm is the number of flights and kk is the number of stops.


Common Pitfalls

Confusing Stops vs Flights (Off-by-One)

The problem allows at most k stops, which means at most k+1 flights. A common mistake is running Bellman-Ford for k iterations instead of k+1, or checking stops > k when you should check stops == k before taking another flight.

# Wrong: only k iterations (k flights, not k+1)
for i in range(k):
# Correct: k+1 iterations for k stops
for i in range(k + 1):

Not Using a Temporary Array in Bellman-Ford

When relaxing edges, using the same array for both reading and writing allows paths from the current iteration to chain together, potentially exceeding the allowed number of flights in a single round.

# Wrong: updates affect same iteration
if prices[s] + p < prices[d]:
    prices[d] = prices[s] + p
# Correct: use temporary array
if prices[s] + p < tmpPrices[d]:
    tmpPrices[d] = prices[s] + p

Using Standard Dijkstra Without Stop Tracking

Standard Dijkstra finds the cheapest path but ignores the stop constraint. A path with fewer stops might be more expensive but still valid, while the cheapest path might exceed k stops. The state must include both cost and number of stops used.

# Wrong: only tracks cost
dist[city] = cost
# Correct: track cost at each stop level
dist[city][stops] = cost