Builds on Dijkstra, which you haven’t finished yet.
Bellman-Ford (K-Stop Relaxation)
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.
Bellman-Ford
edges ➔ k + 1 rounds of relaxation on a copy ➔ cheapest price within k stopsk stops, flights or moves), or when some edge weights are negative, so Dijkstra's settled-means-final rule no longer holds.| 1 | from typing import List |
| 2 | |
| 3 | def bellman_ford_k_stops(n: int, flights: List[List[int]], src: int, dst: int, k: int) -> int: |
| 4 | """Bellman-Ford (Cheapest Flights Within K Stops): lowest total fare of a trip src |
| 5 | -> dst that lands in no more than k cities on the way, or -1. |
| 6 | Time: O(K · (V + E)) | Space: O(V) |
| 7 | """ |
| 8 | prices = [float("inf")] * n |
| 9 | prices[src] = 0 # 0 flights: only src is reached |
| 10 | # k stops = at most k + 1 flights, one more per round |
| 11 | for _ in range(k + 1): |
| 12 | tmp_prices = prices.copy() # write here, read last round's prices |
| 13 | for u, v, price in flights: |
| 14 | if prices[u] == float("inf"): |
| 15 | # no route from the earlier rounds reaches u |
| 16 | continue |
| 17 | # one more flight, from last round's price |
| 18 | if prices[u] + price < tmp_prices[v]: |
| 19 | tmp_prices[v] = prices[u] + price |
| 20 | prices = tmp_prices # the round is done: publish it |
| 21 | return prices[dst] if prices[dst] != float("inf") else -1 |
i, prices[v] is the cheapest price of a route from src to v that uses at most i flights, and a round only ever reads prices and writes tmp_prices.A route with at most k stops has at most k + 1 flights, so run exactly k + 1 rounds and let each round add one more flight: for every flight u -> v, keep prices[u] + price in tmp_prices[v] when it is cheaper. This is correct because every route of at most i flights is a route of at most i - 1 flights, whose cheapest price is already in prices, plus one last flight, and round i tries every possible last flight; so after round i, prices[v] is the cheapest route to v of at most i flights. The trap is relaxing in place: a round must read the previous round's prices and write into its copy, tmp_prices = prices.copy(). Written straight into prices, a flight relaxed earlier in the round feeds the next flight in the same round, one round chains several flights, and a cheaper route with too many stops wins.
See it run
Bellman-Ford with at most 1 stops, from city 0 to city 3, step by step
round 0 of 2prices [0, inf, inf, inf]tmp_prices -
Column j is city j. The top row is prices. The bottom row is tmp_prices, the copy that one round writes to.
readprices[u], the price from the last roundtargettmp_prices[v], which the flight tries to lowerwrittena value just setanswerprices[dst], the final answer
prices[0] = 0 and every other city is inf. 1 stop allows at most 2 flights, so run 2 rounds.
Gotcha 1 of 4
- The trap
Gotcha 1: Writing into
pricesin place instead of a copyMistaketmp_prices = pricesFixtmp_prices = prices.copy()What happens:
tmp_prices = pricesmakes a second name for the same list. A price written in a round is then read again in the same round. So one round can add several flights. A cheap route with too many stops wins. With no stop limit, relaxing in place is still correct, and only finishes sooner.Example: Cheapest Flights Within K Stops (LC 787): n = 4, flights = [[0, 1, 100], [1, 2, 100], [2, 0, 100], [1, 3, 600], [2, 3, 200]], src = 0, dst = 3, k = 1Mistake: returns 400Fix: returns 700
- Nothing caps the number of edges and every weight is non-negative, as in Network Delay Time (LC 743): switch to Dijkstra. See Dijkstra
- You need the shortest distance between every pair of cities, as in Find the City With the Smallest Number of Neighbors at a Threshold Distance (LC 1334), not the cheapest route from one source. See Floyd-Warshall
Deep Dive
What changes from Dijkstra: rounds on a copy, not a heap
The Dijkstra card keeps a heap and settles one node at a time, by cost. This card has no heap. It runs rounds, and round i adds one more flight to every route.
Why this is needed: Dijkstra settles a node by its cost alone, and it cannot count flights. In Cheapest Flights Within K Stops (LC 787), the cheapest route overall may use too many stops. For the example in the board below, Dijkstra would find the route 0 -> 1 -> 2 -> 3 at price 400. It uses 3 flights, but k = 1 allows only 2 flights. The right answer is 700.
So each round keeps a list of prices by number of flights. After round i, prices[v] is the cheapest price of a route from src to v with at most i flights. The list has one number per city, so the cost of a round is small.
A round reads prices and writes tmp_prices. This keeps the rounds apart, so one round can add exactly one flight.
One trace: two rounds on the LeetCode example
The board runs bellman_ford_k_stops with n = 4, src = 0, dst = 3 and k = 1. One stop means at most 2 flights, so there are 2 rounds.
Round 1 starts with prices = [0, inf, inf, inf]. Only flight 0 -> 1 has a reachable start, so tmp_prices[1] = 100. The result is [0, 100, inf, inf].
Round 2 relaxes flight 1 -> 2 to 200 and flight 1 -> 3 to 700. Flight 2 -> 3 is skipped, because prices[2] was still inf at the end of round 1. The result is [0, 100, 200, 700], and the answer is 700.
The step marked as a trap shows what in-place writing would do. tmp_prices[2] = 200 is already set in round 2, so flight 2 -> 3 would give 400. That route has 3 flights, which k = 1 does not allow.
The trap: relaxing in place
tmp_prices = prices does not copy the list. It gives the same list a second name. A price written in a round is then read again in the same round, so one round can add several flights.
For the trace above, writing in place returns 400 and not 700. Flight 0 -> 1 sets prices[1] = 100, flight 1 -> 2 then sets prices[2] = 200, and flight 2 -> 3 sets prices[3] = 400. All three happen in one round.
The second trap is the round count. k stops allow k + 1 flights, so run range(k + 1) rounds. With range(k) the same example returns -1, because the route to city 3 needs both flights.
Last, if prices[dst] is still inf at the end, return -1. For n = 3, flights = [[0, 1, 100]], src = 0, dst = 2 and k = 1, no route exists.
Negative weights, and the cost
Dijkstra needs every weight to be zero or more. A rounds method does not settle a node early, so a negative weight is fine here. A later round can still lower any price.
Example: edges = [[0, 1, 2], [0, 2, 3], [2, 1, -2]] from city 0 to city 1. A Dijkstra that never revisits a settled node returns 2. Bellman-Ford with 2 rounds returns 1, through the route 0 -> 2 -> 1 that costs 3 - 2. With 1 round it still returns 2, as it should for a cap of one flight.
Each round copies prices and reads every flight once, so the time is O(k * (V + E)) and the space is O(V). The board has 5 flights and 2 rounds, so it makes 10 flight checks.
With no cap, run n - 1 rounds. A route without a repeated city has at most n - 1 flights. This costs O(V * E), which is slower than Dijkstra. That is the price of allowing negative weights.
60-Second Pitch
"The path has a cap on stops, so Dijkstra does not fit: it settles a node by cost and cannot count flights. I use Bellman-Ford with rounds. k stops allow k + 1 flights, so I run k + 1 rounds. In each round I copy prices, then for every flight I read the old prices[u] and write the better value into the copy. After round i, prices[v] is the cheapest price with at most i flights. I must write to a copy. If I write in place, one round can use several flights, and the cap breaks. The time is O(k * (V + E)) and the space is O(V). I return prices[dst], or -1 if it is still infinity."
Common Questions
- Why does Dijkstra fail when the path has a cap on stops?
Dijkstra settles a node by its cost, and a settled node is never revisited. The cheapest route to a node may use too many stops, and a more expensive route with fewer stops may be the only one the cap allows. Dijkstra keeps one value per node and cannot tell them apart.
- Why
k + 1rounds and notk? kstops are cities between the start and the end, so a route withkstops hask + 1flights. Each round adds exactly one flight. Withkrounds the last flight is never added.- Why copy
pricesin every round? The copy keeps rounds apart. If you write in place, a price set earlier in a round is read by a later flight in the same round. One round then adds several flights, and a route with too many stops can win.
- What are the time and space complexity?
O(k * (V + E)) time, because each round copies V prices and reads E flights. O(V) space for two lists of prices. With no cap,
k + 1becomesV - 1rounds, so the time is O(V * E).- Why does this work with negative weights?
No node is settled early. Every round tries every flight again, so a later round can lower any price. Dijkstra assumes a popped distance is final, and a negative edge breaks that. A negative cycle still breaks the answer when there is no cap.
SWE Reasoning
Spot it
- The question asks for the cheapest path, and the path may use at most
kstops, flights or moves. - The cheapest path overall may be a path that the cap forbids.
- Some edge weights can be negative.
- The graph is small enough for
ktimes the number of edges to be fast. - The answer is for one source and one destination, with -1 when no route fits.
The invariant, step by step
prices[src] = 0and all other prices are infinity. This is the state with 0 flights.kstops allowk + 1flights, so run exactlyk + 1rounds.- Each round starts with
tmp_prices = prices.copy(). The round readspricesand writestmp_prices. - For each flight
u -> v, skip it ifprices[u]is infinity. Otherwise keepprices[u] + priceintmp_prices[v]when it is cheaper. prices = tmp_pricespublishes the round. Every route of at mostiflights is a route of at mosti - 1flights plus one last flight, soprices[v]is now the cheapest price with at mostiflights.- After the last round, return
prices[dst], or -1 if it is still infinity.
Traps
- Writing into
pricesin place. One round can chain several flights, and a cheap route with too many stops wins. Write into a copy. range(k)rounds.kstops arek + 1flights. Runrange(k + 1).- Reading
prices[dst]without a check. It may still be infinity. Return -1 then. - Using Dijkstra with a cap. A cheapest route overall may use too many flights. Dijkstra cannot count them.
- Skipping the infinity check. Adding a price to infinity is harmless in Python. In Java or C++, a large integer can overflow, so skip those flights.
Practice
Builds on Dijkstra, which you haven’t finished yet.
Bellman-Ford (K-Stop Relaxation)
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.
Bellman-Ford
edges ➔ k + 1 rounds of relaxation on a copy ➔ cheapest price within k stopsk stops, flights or moves), or when some edge weights are negative, so Dijkstra's settled-means-final rule no longer holds.| 1 | from typing import List |
| 2 | |
| 3 | def bellman_ford_k_stops(n: int, flights: List[List[int]], src: int, dst: int, k: int) -> int: |
| 4 | """Bellman-Ford (Cheapest Flights Within K Stops): lowest total fare of a trip src |
| 5 | -> dst that lands in no more than k cities on the way, or -1. |
| 6 | Time: O(K · (V + E)) | Space: O(V) |
| 7 | """ |
| 8 | prices = [float("inf")] * n |
| 9 | prices[src] = 0 # 0 flights: only src is reached |
| 10 | # k stops = at most k + 1 flights, one more per round |
| 11 | for _ in range(k + 1): |
| 12 | tmp_prices = prices.copy() # write here, read last round's prices |
| 13 | for u, v, price in flights: |
| 14 | if prices[u] == float("inf"): |
| 15 | # no route from the earlier rounds reaches u |
| 16 | continue |
| 17 | # one more flight, from last round's price |
| 18 | if prices[u] + price < tmp_prices[v]: |
| 19 | tmp_prices[v] = prices[u] + price |
| 20 | prices = tmp_prices # the round is done: publish it |
| 21 | return prices[dst] if prices[dst] != float("inf") else -1 |
i, prices[v] is the cheapest price of a route from src to v that uses at most i flights, and a round only ever reads prices and writes tmp_prices.A route with at most k stops has at most k + 1 flights, so run exactly k + 1 rounds and let each round add one more flight: for every flight u -> v, keep prices[u] + price in tmp_prices[v] when it is cheaper. This is correct because every route of at most i flights is a route of at most i - 1 flights, whose cheapest price is already in prices, plus one last flight, and round i tries every possible last flight; so after round i, prices[v] is the cheapest route to v of at most i flights. The trap is relaxing in place: a round must read the previous round's prices and write into its copy, tmp_prices = prices.copy(). Written straight into prices, a flight relaxed earlier in the round feeds the next flight in the same round, one round chains several flights, and a cheaper route with too many stops wins.
See it run
Bellman-Ford with at most 1 stops, from city 0 to city 3, step by step
round 0 of 2prices [0, inf, inf, inf]tmp_prices -
Column j is city j. The top row is prices. The bottom row is tmp_prices, the copy that one round writes to.
readprices[u], the price from the last roundtargettmp_prices[v], which the flight tries to lowerwrittena value just setanswerprices[dst], the final answer
prices[0] = 0 and every other city is inf. 1 stop allows at most 2 flights, so run 2 rounds.
Gotcha 1 of 4
- The trap
Gotcha 1: Writing into
pricesin place instead of a copyMistaketmp_prices = pricesFixtmp_prices = prices.copy()What happens:
tmp_prices = pricesmakes a second name for the same list. A price written in a round is then read again in the same round. So one round can add several flights. A cheap route with too many stops wins. With no stop limit, relaxing in place is still correct, and only finishes sooner.Example: Cheapest Flights Within K Stops (LC 787): n = 4, flights = [[0, 1, 100], [1, 2, 100], [2, 0, 100], [1, 3, 600], [2, 3, 200]], src = 0, dst = 3, k = 1Mistake: returns 400Fix: returns 700
- Nothing caps the number of edges and every weight is non-negative, as in Network Delay Time (LC 743): switch to Dijkstra. See Dijkstra
- You need the shortest distance between every pair of cities, as in Find the City With the Smallest Number of Neighbors at a Threshold Distance (LC 1334), not the cheapest route from one source. See Floyd-Warshall
Deep Dive
What changes from Dijkstra: rounds on a copy, not a heap
The Dijkstra card keeps a heap and settles one node at a time, by cost. This card has no heap. It runs rounds, and round i adds one more flight to every route.
Why this is needed: Dijkstra settles a node by its cost alone, and it cannot count flights. In Cheapest Flights Within K Stops (LC 787), the cheapest route overall may use too many stops. For the example in the board below, Dijkstra would find the route 0 -> 1 -> 2 -> 3 at price 400. It uses 3 flights, but k = 1 allows only 2 flights. The right answer is 700.
So each round keeps a list of prices by number of flights. After round i, prices[v] is the cheapest price of a route from src to v with at most i flights. The list has one number per city, so the cost of a round is small.
A round reads prices and writes tmp_prices. This keeps the rounds apart, so one round can add exactly one flight.
One trace: two rounds on the LeetCode example
The board runs bellman_ford_k_stops with n = 4, src = 0, dst = 3 and k = 1. One stop means at most 2 flights, so there are 2 rounds.
Round 1 starts with prices = [0, inf, inf, inf]. Only flight 0 -> 1 has a reachable start, so tmp_prices[1] = 100. The result is [0, 100, inf, inf].
Round 2 relaxes flight 1 -> 2 to 200 and flight 1 -> 3 to 700. Flight 2 -> 3 is skipped, because prices[2] was still inf at the end of round 1. The result is [0, 100, 200, 700], and the answer is 700.
The step marked as a trap shows what in-place writing would do. tmp_prices[2] = 200 is already set in round 2, so flight 2 -> 3 would give 400. That route has 3 flights, which k = 1 does not allow.
The trap: relaxing in place
tmp_prices = prices does not copy the list. It gives the same list a second name. A price written in a round is then read again in the same round, so one round can add several flights.
For the trace above, writing in place returns 400 and not 700. Flight 0 -> 1 sets prices[1] = 100, flight 1 -> 2 then sets prices[2] = 200, and flight 2 -> 3 sets prices[3] = 400. All three happen in one round.
The second trap is the round count. k stops allow k + 1 flights, so run range(k + 1) rounds. With range(k) the same example returns -1, because the route to city 3 needs both flights.
Last, if prices[dst] is still inf at the end, return -1. For n = 3, flights = [[0, 1, 100]], src = 0, dst = 2 and k = 1, no route exists.
Negative weights, and the cost
Dijkstra needs every weight to be zero or more. A rounds method does not settle a node early, so a negative weight is fine here. A later round can still lower any price.
Example: edges = [[0, 1, 2], [0, 2, 3], [2, 1, -2]] from city 0 to city 1. A Dijkstra that never revisits a settled node returns 2. Bellman-Ford with 2 rounds returns 1, through the route 0 -> 2 -> 1 that costs 3 - 2. With 1 round it still returns 2, as it should for a cap of one flight.
Each round copies prices and reads every flight once, so the time is O(k * (V + E)) and the space is O(V). The board has 5 flights and 2 rounds, so it makes 10 flight checks.
With no cap, run n - 1 rounds. A route without a repeated city has at most n - 1 flights. This costs O(V * E), which is slower than Dijkstra. That is the price of allowing negative weights.
60-Second Pitch
"The path has a cap on stops, so Dijkstra does not fit: it settles a node by cost and cannot count flights. I use Bellman-Ford with rounds. k stops allow k + 1 flights, so I run k + 1 rounds. In each round I copy prices, then for every flight I read the old prices[u] and write the better value into the copy. After round i, prices[v] is the cheapest price with at most i flights. I must write to a copy. If I write in place, one round can use several flights, and the cap breaks. The time is O(k * (V + E)) and the space is O(V). I return prices[dst], or -1 if it is still infinity."
Common Questions
- Why does Dijkstra fail when the path has a cap on stops?
Dijkstra settles a node by its cost, and a settled node is never revisited. The cheapest route to a node may use too many stops, and a more expensive route with fewer stops may be the only one the cap allows. Dijkstra keeps one value per node and cannot tell them apart.
- Why
k + 1rounds and notk? kstops are cities between the start and the end, so a route withkstops hask + 1flights. Each round adds exactly one flight. Withkrounds the last flight is never added.- Why copy
pricesin every round? The copy keeps rounds apart. If you write in place, a price set earlier in a round is read by a later flight in the same round. One round then adds several flights, and a route with too many stops can win.
- What are the time and space complexity?
O(k * (V + E)) time, because each round copies V prices and reads E flights. O(V) space for two lists of prices. With no cap,
k + 1becomesV - 1rounds, so the time is O(V * E).- Why does this work with negative weights?
No node is settled early. Every round tries every flight again, so a later round can lower any price. Dijkstra assumes a popped distance is final, and a negative edge breaks that. A negative cycle still breaks the answer when there is no cap.
SWE Reasoning
Spot it
- The question asks for the cheapest path, and the path may use at most
kstops, flights or moves. - The cheapest path overall may be a path that the cap forbids.
- Some edge weights can be negative.
- The graph is small enough for
ktimes the number of edges to be fast. - The answer is for one source and one destination, with -1 when no route fits.
The invariant, step by step
prices[src] = 0and all other prices are infinity. This is the state with 0 flights.kstops allowk + 1flights, so run exactlyk + 1rounds.- Each round starts with
tmp_prices = prices.copy(). The round readspricesand writestmp_prices. - For each flight
u -> v, skip it ifprices[u]is infinity. Otherwise keepprices[u] + priceintmp_prices[v]when it is cheaper. prices = tmp_pricespublishes the round. Every route of at mostiflights is a route of at mosti - 1flights plus one last flight, soprices[v]is now the cheapest price with at mostiflights.- After the last round, return
prices[dst], or -1 if it is still infinity.
Traps
- Writing into
pricesin place. One round can chain several flights, and a cheap route with too many stops wins. Write into a copy. range(k)rounds.kstops arek + 1flights. Runrange(k + 1).- Reading
prices[dst]without a check. It may still be infinity. Return -1 then. - Using Dijkstra with a cap. A cheapest route overall may use too many flights. Dijkstra cannot count them.
- Skipping the infinity check. Adding a price to infinity is harmless in Python. In Java or C++, a large integer can overflow, so skip those flights.