Cheapest Flights Within K Stops (LeetCode 787)
Picture an airline network over n cities, numbered 0 to n - 1. Each entry [from_i, to_i, price_i] of flights is a one-way flight: it leaves from_i, lands in to_i, and its ticket costs price_i.
You want to travel from city src to city dst and pay as little as possible in total, but you accept at most k stops: a stop is a city you land in on the way, other than src and dst, so the trip uses at most k + 1 flights. Return the lowest total ticket price of such a trip, or -1 when no trip with at most k stops gets you to dst.
Worked Examples
n = 4, flights = [[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]], src = 0, dst = 3, k = 1700n = 3, flights = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 1200n = 3, flights = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 0500⚖️Formal Constraints & Bounds
2 <= n <= 1000 <= flights.length <= (n * (n - 1) / 2)flights[i].length == 30 <= from_i, to_i < nfrom_i != to_i1 <= price_i <= 104There will not be any multiple flights between two cities
0 <= src, dst, k < nsrc != dst
Core Insight
Each of the k + 1 rounds reads last round's prices and writes into tmp_prices = prices.copy(), so a round adds exactly one flight and, after round i, prices[v] is the cheapest route to v with at most i flights.
Real-World Scenario & Production Applications
Multi-leg airline routing with strict stop limits, bounded transit budgets, and step-constrained network packet delivery.
Subproblems & Recurrence Decomposition3 Phases
At most k stops means at most k + 1 flights, so run exactly k + 1 rounds, starting from prices[src] = 0 and inf everywhere else.
prices = [float("inf")] * n
prices[src] = 0
for _ in range(k + 1):Step-by-Step Execution Trace Table
| Round | Flight (u, v, price) | prices[u] (last round) | Relaxation | tmp_prices after it |
|---|---|---|---|---|
| Start | - | - | prices[src = 0] = 0, all others | prices = [0, inf, inf, inf] |
| Round 1 (up to 1 flight) | (0, 1, 100) | 0 | tmp_prices[1] = 100 | [0, 100, inf, inf] |
| Round 1 | (1, 2, 100) | inf | Skip: tmp_prices[1] is already 100, but the round reads prices[1], so 0 -> 1 -> 2 must wait | [0, 100, inf, inf] |
| Round 1 | (2, 0, 100), (1, 3, 600), (2, 3, 200) | inf | Skip | [0, 100, inf, inf] |
| Round 1 ends | - | - | prices = tmp_prices | prices = [0, 100, inf, inf] |
| Round 2 (up to 2 flights) | (1, 2, 100) | 100 | tmp_prices[2] = 200 | [0, 100, 200, inf] |
| Round 2 | (1, 3, 600) | 100 | tmp_prices[3] = 700 | [0, 100, 200, 700] |
| Round 2 | (2, 3, 200) | inf | Skip: 0 -> 1 -> 2 -> 3 would need a third flight | [0, 100, 200, 700] |
| End ( rounds) | - | - | return prices[3] | 700 |
n = 4, flights = [[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]], src = 0, dst = 3, k = 1700| Round | Flight (u, v, price) | prices[u] (last round) | Relaxation | tmp_prices after it |
|---|---|---|---|---|
| Start | - | - | prices[src = 0] = 0, all others | prices = [0, inf, inf, inf] |
| Round 1 (up to 1 flight) | (0, 1, 100) | 0 | tmp_prices[1] = 100 | [0, 100, inf, inf] |
| Round 1 | (1, 2, 100) | inf | Skip: tmp_prices[1] is already 100, but the round reads prices[1], so 0 -> 1 -> 2 must wait | [0, 100, inf, inf] |
| Round 1 | (2, 0, 100), (1, 3, 600), (2, 3, 200) | inf | Skip | [0, 100, inf, inf] |
| Round 1 ends | - | - | prices = tmp_prices | prices = [0, 100, inf, inf] |
| Round 2 (up to 2 flights) | (1, 2, 100) | 100 | tmp_prices[2] = 200 | [0, 100, 200, inf] |
| Round 2 | (1, 3, 600) | 100 | tmp_prices[3] = 700 | [0, 100, 200, 700] |
| Round 2 | (2, 3, 200) | inf | Skip: 0 -> 1 -> 2 -> 3 would need a third flight | [0, 100, 200, 700] |
| End ( rounds) | - | - | return prices[3] | 700 |
| 1 | prices = [inf] * n |
| 2 | prices[src] = 0 |
| 3 | for _ in range(k + 1): |
| 4 | tmp_prices = prices.copy() |
| 5 | for u, v, price in flights: |
| 6 | if prices[u] + price < tmp_prices[v]: |
| 7 | tmp_prices[v] = prices[u] + price |
| 8 | prices = tmp_prices |
| 9 | return prices[dst] if prices[dst] != inf else -1 |
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
Standard shortest-path algorithms find the cheapest path no matter how many edges it uses. When the path may use at most stops ( flights), Dijkstra fails: it settles a city by its cheapest route, even when that route has too many stops, and never looks at the dearer route with fewer stops. Bellman-Ford fixes this by running exactly rounds of relaxation over every flight, each round reading last round's prices and writing into a copy, tmp_prices, so that each round adds at most one flight.
✈️ The Analogy: A Flight Pass with Exactly K Layovers
Imagine an airline flight coupon that lets you book at most layovers (meaning at most total flight legs). On Day 1, you can only look at direct 1-leg flights from your origin. On Day 2, you look at all flights reachable by taking 1 flight from any city you could already reach on Day 1. By freezing your prices table at the start of each round (tmp_prices = prices.copy()), you prevent "chaining" multiple flights in a single round — guaranteeing that after rounds, you have explored every possible valid flight itinerary with at most stops.
🔑 The Key Idea in the Code
The copy tmp_prices = prices.copy() is the linchpin: every flight reads prices[u], last round's price, and writes tmp_prices[v]. Without the copy, updating prices[v] in place would let a later flight v -> w in the same round use that new price at once, so a single round could chain 2, 3 or more flights. With it, a round adds strictly one flight.
Writing into
pricesin place instead of a copyRunning
range(k)rounds instead ofrange(k + 1)Returning
prices[dst]when it is stillfloat("inf")Dropping the
prices[u] == INT_MAXcheck in Java or C++
4-Phase Thought Process Model
Pattern Recognition Signals
The 10-second spot
The trip must cost as little as possible "in total" but may use "at most k stops", which means it "uses at most k + 1 flights": a cheapest path with a cap on the number of edges. Dijkstra's settled-means-final rule ignores the cap, so this is Bellman-Ford with k + 1 rounds.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
After round i, prices[v] is the cheapest route from src to v with at most i flights. Each round reads only prices, writes into tmp_prices = prices.copy(), relaxes a flight when prices[u] + price < tmp_prices[v], and ends with prices = tmp_prices.
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
tmp_prices = prices.copy(): relaxing straight intopriceslets one round chain several flights; on Example 1 the two-stop route 0 -> 1 -> 2 -> 3 (400) would beat the one-stop answer 700.for _ in range(k + 1):kstops isk + 1flights, sorange(k)stops one flight short.return prices[dst] if prices[dst] != float("inf") else -1: when no route with at mostkstops reachesdst, the answer is -1, not infinity.Plain Dijkstra on price alone settles a city by its cheapest route even when that route has too many stops, and never looks at it again: the cap needs the rounds (or the stop count in the heap state).
The 60-Second Interview Pitch
Say this out loud before you type a single line
This is Bellman-Ford with a cap on flights. At most k stops means at most k plus one flights, so I run exactly k plus one rounds, and each round lets every route grow by one flight. I keep prices, the cheapest known price per city, with the source at zero. Each round I copy prices into tmp prices, and for every flight from u to v, if u is reached and prices of u plus the ticket beats tmp prices of v, I write the new price into the copy. Then I publish the copy. After round i, prices holds the cheapest route of at most i flights, because any such route is a shorter route plus one last flight, and the round tries every last flight. The trap is updating in place: one round would chain several flights, and a cheap route with too many stops would win. It's O(K times (V plus E)) time and O(V) space.
Complexity & Mathematical Proof
O(K · (V + E))
Look at the code: for _ in range(k + 1) runs K + 1 rounds. Each round does tmp_prices = prices.copy(), which is O(V), then for u, v, price in flights tries each of the E flights once with O(1) work: the infinity test, the comparison and at most one write. One round is O(V + E), so the run is (K + 1) · (V + E) = O(K · (V + E)). On a dense network (E close to V²) the flights dominate; on a sparse one the copies do, which is why V stays in the bound.
O(V)
prices and tmp_prices hold V entries each; after prices = tmp_prices the old array is freed, so at most two arrays live at once. There is no recursion and no heap. The input flights is not counted.
T(K, V, E) = (K + 1) · (V + E) = O(K · (V + E))
Derivation Progression
for _ in range(k + 1): K + 1 iterations
One round per allowed flight: k stops means at most k + 1 flights.
tmp_prices = prices.copy(): O(V)
Each round starts by copying the V prices, so writes never touch the array being read.
sum over flights of O(1) = O(E)
Each flight costs one infinity test, one comparison and at most one write into tmp_prices.
(K + 1) · (O(V) + O(E)) = O(K · (V + E))
The rounds multiply the per-round cost of one copy plus one pass over the flights.
Variable Definitions
The stop limit k; the loop runs K + 1 rounds, one per allowed flight
Number of flights, len(flights)
Number of cities, n
Memory Architecture & Bounds
O(1): iterative, no recursion
O(V): prices and tmp_prices, V entries each, at most two at a time
O(1): one integer
Boundary Best / Worst Cases
: every round tries every flight, even after the prices stop changing.
.
: the same work on every input of that size.
Graph & Tree Traversal Frontier
Senior SWE Deconstruction & Hardware Caveats
Scale signal: each round is one pass over the flights, so the whole run is steps, while listing every route of up to flights grows like .
cities, at most flights, . Budget: steps, and two arrays of prices.
A round that changes no price means later rounds can't change any either, so a production version stops early there. Without a cap, rounds suffice, and a round after that which still lowers a price means a negative cycle.
Core Algorithmic State Invariants
- Example 2: The Second Round Finds the Cheaper Route
Round 1 gives prices = [0, 100, 500]: only direct flights count. Round 2 tries 0 -> 1 -> 2 for 100 + 100 = 200 < 500, so with k = 1 the answer is 200.
- Example 3: k = 0 Is One Round
Only round 1 runs, and prices[2] = 500 from the direct flight. The 200 route needs a second flight, which k = 0 does not allow, so the answer is 500.
- Example 1 With k = 2
A third round makes 0 -> 1 -> 2 -> 3 legal, and prices[3] drops from 700 to 400. The graph is the same; the cap alone decides which routes count.
Cheapest Flights Within K Stops (LeetCode 787)
Picture an airline network over n cities, numbered 0 to n - 1. Each entry [from_i, to_i, price_i] of flights is a one-way flight: it leaves from_i, lands in to_i, and its ticket costs price_i.
You want to travel from city src to city dst and pay as little as possible in total, but you accept at most k stops: a stop is a city you land in on the way, other than src and dst, so the trip uses at most k + 1 flights. Return the lowest total ticket price of such a trip, or -1 when no trip with at most k stops gets you to dst.
Worked Examples
n = 4, flights = [[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]], src = 0, dst = 3, k = 1700n = 3, flights = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 1200n = 3, flights = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 0500⚖️Formal Constraints & Bounds
2 <= n <= 1000 <= flights.length <= (n * (n - 1) / 2)flights[i].length == 30 <= from_i, to_i < nfrom_i != to_i1 <= price_i <= 104There will not be any multiple flights between two cities
0 <= src, dst, k < nsrc != dst
Core Insight
Each of the k + 1 rounds reads last round's prices and writes into tmp_prices = prices.copy(), so a round adds exactly one flight and, after round i, prices[v] is the cheapest route to v with at most i flights.
Real-World Scenario & Production Applications
Multi-leg airline routing with strict stop limits, bounded transit budgets, and step-constrained network packet delivery.
Subproblems & Recurrence Decomposition3 Phases
At most k stops means at most k + 1 flights, so run exactly k + 1 rounds, starting from prices[src] = 0 and inf everywhere else.
prices = [float("inf")] * n
prices[src] = 0
for _ in range(k + 1):Step-by-Step Execution Trace Table
| Round | Flight (u, v, price) | prices[u] (last round) | Relaxation | tmp_prices after it |
|---|---|---|---|---|
| Start | - | - | prices[src = 0] = 0, all others | prices = [0, inf, inf, inf] |
| Round 1 (up to 1 flight) | (0, 1, 100) | 0 | tmp_prices[1] = 100 | [0, 100, inf, inf] |
| Round 1 | (1, 2, 100) | inf | Skip: tmp_prices[1] is already 100, but the round reads prices[1], so 0 -> 1 -> 2 must wait | [0, 100, inf, inf] |
| Round 1 | (2, 0, 100), (1, 3, 600), (2, 3, 200) | inf | Skip | [0, 100, inf, inf] |
| Round 1 ends | - | - | prices = tmp_prices | prices = [0, 100, inf, inf] |
| Round 2 (up to 2 flights) | (1, 2, 100) | 100 | tmp_prices[2] = 200 | [0, 100, 200, inf] |
| Round 2 | (1, 3, 600) | 100 | tmp_prices[3] = 700 | [0, 100, 200, 700] |
| Round 2 | (2, 3, 200) | inf | Skip: 0 -> 1 -> 2 -> 3 would need a third flight | [0, 100, 200, 700] |
| End ( rounds) | - | - | return prices[3] | 700 |
n = 4, flights = [[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]], src = 0, dst = 3, k = 1700| Round | Flight (u, v, price) | prices[u] (last round) | Relaxation | tmp_prices after it |
|---|---|---|---|---|
| Start | - | - | prices[src = 0] = 0, all others | prices = [0, inf, inf, inf] |
| Round 1 (up to 1 flight) | (0, 1, 100) | 0 | tmp_prices[1] = 100 | [0, 100, inf, inf] |
| Round 1 | (1, 2, 100) | inf | Skip: tmp_prices[1] is already 100, but the round reads prices[1], so 0 -> 1 -> 2 must wait | [0, 100, inf, inf] |
| Round 1 | (2, 0, 100), (1, 3, 600), (2, 3, 200) | inf | Skip | [0, 100, inf, inf] |
| Round 1 ends | - | - | prices = tmp_prices | prices = [0, 100, inf, inf] |
| Round 2 (up to 2 flights) | (1, 2, 100) | 100 | tmp_prices[2] = 200 | [0, 100, 200, inf] |
| Round 2 | (1, 3, 600) | 100 | tmp_prices[3] = 700 | [0, 100, 200, 700] |
| Round 2 | (2, 3, 200) | inf | Skip: 0 -> 1 -> 2 -> 3 would need a third flight | [0, 100, 200, 700] |
| End ( rounds) | - | - | return prices[3] | 700 |
| 1 | prices = [inf] * n |
| 2 | prices[src] = 0 |
| 3 | for _ in range(k + 1): |
| 4 | tmp_prices = prices.copy() |
| 5 | for u, v, price in flights: |
| 6 | if prices[u] + price < tmp_prices[v]: |
| 7 | tmp_prices[v] = prices[u] + price |
| 8 | prices = tmp_prices |
| 9 | return prices[dst] if prices[dst] != inf else -1 |
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
Standard shortest-path algorithms find the cheapest path no matter how many edges it uses. When the path may use at most stops ( flights), Dijkstra fails: it settles a city by its cheapest route, even when that route has too many stops, and never looks at the dearer route with fewer stops. Bellman-Ford fixes this by running exactly rounds of relaxation over every flight, each round reading last round's prices and writing into a copy, tmp_prices, so that each round adds at most one flight.
✈️ The Analogy: A Flight Pass with Exactly K Layovers
Imagine an airline flight coupon that lets you book at most layovers (meaning at most total flight legs). On Day 1, you can only look at direct 1-leg flights from your origin. On Day 2, you look at all flights reachable by taking 1 flight from any city you could already reach on Day 1. By freezing your prices table at the start of each round (tmp_prices = prices.copy()), you prevent "chaining" multiple flights in a single round — guaranteeing that after rounds, you have explored every possible valid flight itinerary with at most stops.
🔑 The Key Idea in the Code
The copy tmp_prices = prices.copy() is the linchpin: every flight reads prices[u], last round's price, and writes tmp_prices[v]. Without the copy, updating prices[v] in place would let a later flight v -> w in the same round use that new price at once, so a single round could chain 2, 3 or more flights. With it, a round adds strictly one flight.
Writing into
pricesin place instead of a copyRunning
range(k)rounds instead ofrange(k + 1)Returning
prices[dst]when it is stillfloat("inf")Dropping the
prices[u] == INT_MAXcheck in Java or C++
4-Phase Thought Process Model
Pattern Recognition Signals
The 10-second spot
The trip must cost as little as possible "in total" but may use "at most k stops", which means it "uses at most k + 1 flights": a cheapest path with a cap on the number of edges. Dijkstra's settled-means-final rule ignores the cap, so this is Bellman-Ford with k + 1 rounds.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
After round i, prices[v] is the cheapest route from src to v with at most i flights. Each round reads only prices, writes into tmp_prices = prices.copy(), relaxes a flight when prices[u] + price < tmp_prices[v], and ends with prices = tmp_prices.
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
tmp_prices = prices.copy(): relaxing straight intopriceslets one round chain several flights; on Example 1 the two-stop route 0 -> 1 -> 2 -> 3 (400) would beat the one-stop answer 700.for _ in range(k + 1):kstops isk + 1flights, sorange(k)stops one flight short.return prices[dst] if prices[dst] != float("inf") else -1: when no route with at mostkstops reachesdst, the answer is -1, not infinity.Plain Dijkstra on price alone settles a city by its cheapest route even when that route has too many stops, and never looks at it again: the cap needs the rounds (or the stop count in the heap state).
The 60-Second Interview Pitch
Say this out loud before you type a single line
This is Bellman-Ford with a cap on flights. At most k stops means at most k plus one flights, so I run exactly k plus one rounds, and each round lets every route grow by one flight. I keep prices, the cheapest known price per city, with the source at zero. Each round I copy prices into tmp prices, and for every flight from u to v, if u is reached and prices of u plus the ticket beats tmp prices of v, I write the new price into the copy. Then I publish the copy. After round i, prices holds the cheapest route of at most i flights, because any such route is a shorter route plus one last flight, and the round tries every last flight. The trap is updating in place: one round would chain several flights, and a cheap route with too many stops would win. It's O(K times (V plus E)) time and O(V) space.
Complexity & Mathematical Proof
O(K · (V + E))
Look at the code: for _ in range(k + 1) runs K + 1 rounds. Each round does tmp_prices = prices.copy(), which is O(V), then for u, v, price in flights tries each of the E flights once with O(1) work: the infinity test, the comparison and at most one write. One round is O(V + E), so the run is (K + 1) · (V + E) = O(K · (V + E)). On a dense network (E close to V²) the flights dominate; on a sparse one the copies do, which is why V stays in the bound.
O(V)
prices and tmp_prices hold V entries each; after prices = tmp_prices the old array is freed, so at most two arrays live at once. There is no recursion and no heap. The input flights is not counted.
T(K, V, E) = (K + 1) · (V + E) = O(K · (V + E))
Derivation Progression
for _ in range(k + 1): K + 1 iterations
One round per allowed flight: k stops means at most k + 1 flights.
tmp_prices = prices.copy(): O(V)
Each round starts by copying the V prices, so writes never touch the array being read.
sum over flights of O(1) = O(E)
Each flight costs one infinity test, one comparison and at most one write into tmp_prices.
(K + 1) · (O(V) + O(E)) = O(K · (V + E))
The rounds multiply the per-round cost of one copy plus one pass over the flights.
Variable Definitions
The stop limit k; the loop runs K + 1 rounds, one per allowed flight
Number of flights, len(flights)
Number of cities, n
Memory Architecture & Bounds
O(1): iterative, no recursion
O(V): prices and tmp_prices, V entries each, at most two at a time
O(1): one integer
Boundary Best / Worst Cases
: every round tries every flight, even after the prices stop changing.
.
: the same work on every input of that size.
Graph & Tree Traversal Frontier
Senior SWE Deconstruction & Hardware Caveats
Scale signal: each round is one pass over the flights, so the whole run is steps, while listing every route of up to flights grows like .
cities, at most flights, . Budget: steps, and two arrays of prices.
A round that changes no price means later rounds can't change any either, so a production version stops early there. Without a cap, rounds suffice, and a round after that which still lowers a price means a negative cycle.
Core Algorithmic State Invariants
- Example 2: The Second Round Finds the Cheaper Route
Round 1 gives prices = [0, 100, 500]: only direct flights count. Round 2 tries 0 -> 1 -> 2 for 100 + 100 = 200 < 500, so with k = 1 the answer is 200.
- Example 3: k = 0 Is One Round
Only round 1 runs, and prices[2] = 500 from the direct flight. The 200 route needs a second flight, which k = 0 does not allow, so the answer is 500.
- Example 1 With k = 2
A third round makes 0 -> 1 -> 2 -> 3 legal, and prices[3] drops from 700 to 400. The graph is the same; the cap alone decides which routes count.