787. Cheapest Flights Within K Stops - Explanation
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:
srcis the starting airportdstis the destination airportsrc != dstkis the maximum number of stops you can make (not includingsrcanddst)
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: 500Explanation:
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: 200Explanation:
The optimal path with at most 1 stop from airport 1 to 2 is shown in red and has cost 200.
Constraints:
1 <= n <= 100fromi != toi1 <= pricei <= 10000 <= 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.
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
- Build adjacency list: for each flight
u->vwith pricew. - Create
dist[city][stopsUsed]= bestCost (initialize to infinity). - Push
(0, src, -1)into a min-heap, meaning cost0atsrcwith "-1 stops so far" (so first flight makes stops = 0). - While heap not empty:
- Pop
(cost, city, stops). - If
city==dst, returncost. - 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+pricenextStops=stops+ 1- If
nextCostimprovesdist[nextCity][nextStops+1], update it and push into heap.
- Pop
- 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 -1Time & Space Complexity
- Time complexity: , commonly written as
- Space complexity:
Where is the number of cities, is the number of flights, and is the maximum number of flights/edges allowed. The layered graph has states and transitions. With the lazy priority queue used here, each successful relaxation can push one heap entry, so the heap operation cost is , and the heap can hold entries in the worst case. The adjacency list takes space and
disttakes space.
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
iiterations, we know the cheapest cost to reach every city using at mostiflights. - 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
- Initialize
pricesarray with infinity, setprices[src]= 0. - 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)
- Try relaxing the edge:
- If
- Assign
prices=tmpPrices.
- Make a copy
- After all iterations:
- If
prices[dst]is still infinity, return-1. - Otherwise, return
prices[dst].
- If
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:
- Space complexity:
Where is the number of cities, is the number of flights and 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
kstops. - 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
- Initialize:
prices[i]= infinity for all cities.prices[src]= 0.
- Build an adjacency list from the flight data.
- Use a queue that stores (
currentCost,city,stopsUsed).- Start with
(0, src, 0).
- Start with
- 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.
- Update
- Compute
- Pop a state (
- After processing:
- If
prices[dst]is still infinity, return-1. - Otherwise, return
prices[dst].
- If
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 -1Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of cities, is the number of flights and 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] + pUsing 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
Sign in to join the discussion