Topics › Graphs › Shortest Path Algorithms and Problems
Cheapest flight within K stops
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
There are n cities numbered 0 to n - 1 and a list of one-way flights, each written as [from, to, price]. Given a source city src, a destination dst and an integer k, return the cheapest total price of a trip from src to dst that makes at most k stops, or -1 if no such trip exists. A stop is an intermediate city, so k stops means at most k + 1 flights.
Constraints (LeetCode 787): 1 ≤ n ≤ 100, every price is between 1 and 10^4, there are no duplicate flights, 0 ≤ k < n and src != dst.
Example 1
n = 4, flights = [[0,1,100], [1,2,100], [2,0,100], [1,3,600], [2,3,200]]
src = 0, dst = 3, k = 1
answer = 700 // 0 -> 1 -> 3; the cheaper 0 -> 1 -> 2 -> 3 (400) needs 2 stops
Example 2
n = 3, flights = [[0,1,100], [1,2,100], [0,2,500]], src = 0, dst = 2, k = 0
answer = 500 // no stops allowed, so only the direct flight counts
The stop limit is what makes this problem different from an ordinary shortest path. The cheapest route overall may be useless because it is too long, so the algorithm has to remember not just “how cheaply can I reach this city” but “how cheaply can I reach it with this many flights”.
Approach 1: Brute force
Intuition. Try every route that uses at most k + 1 flights. From the current city, branch on every outgoing flight and recurse with one flight fewer in the budget. The budget strictly shrinks, so the recursion terminates even when the flight graph has cycles. Because prices are positive, once we land in dst there is no point flying on: any continuation that returns to dst costs more.
Algorithm.
- Build an adjacency list
city -> list of {to, price}. cheapestFrom(node, flightsLeft)returns0ifnode == dst, infinity ifflightsLeft == 0, and otherwise the minimum over flightsnode -> nextofprice + cheapestFrom(next, flightsLeft - 1).- Call it with
(src, k + 1)and turn infinity into-1.
static int bruteForce(int n, int[][] flights, int src, int dst, int k) {
List<List<int[]>> adj = buildGraph(n, flights);
long best = cheapestFrom(adj, src, dst, k + 1);
return best == Long.MAX_VALUE ? -1 : (int) best;
}
static long cheapestFrom(List<List<int[]>> adj, int node, int dst, int flightsLeft) {
if (node == dst) return 0; // prices are positive, so going on never helps
if (flightsLeft == 0) return Long.MAX_VALUE;
long best = Long.MAX_VALUE;
for (int[] e : adj.get(node)) {
long rest = cheapestFrom(adj, e[0], dst, flightsLeft - 1);
if (rest != Long.MAX_VALUE) best = Math.min(best, e[1] + rest);
}
return best;
}
Complexity. Time O(d^(k+1)) where d is the largest number of outgoing flights from a city, because every route of up to k + 1 flights is explored; with n = 100 and k = 99 that is astronomically large. Space O(n + E) for the graph and O(k) for the recursion stack.
The recursion revisits the same (city, flightsLeft) pair over and over. Both faster approaches avoid that by treating the pair, rather than the city alone, as the real state.
Approach 2: Better, BFS layered by stops
Intuition. The natural first idea is Dijkstra on price, but it gives wrong answers here. Dijkstra keeps one best price per city and throws away any later, more expensive arrival. In this problem the more expensive arrival may be the one that used fewer flights, and only it can still be extended. Take flights 0→1 (1), 1→2 (1), 2→3 (1), 0→2 (5) with k = 1. Dijkstra settles city 2 at price 2 (two flights), discards the arrival at price 5 (one flight), and then cannot reach 3 without a third flight. The correct answer is 6 via 0 → 2 → 3.
The fix is to explore in order of stops instead of price. Put states {stops, city, cost} into a plain FIFO queue. Each push adds exactly one flight, so every state with s stops leaves the queue before any state with s + 1. Keep dist[city] as the cheapest price seen so far for that city, and push a new state only when it lowers dist[next].
Why is that pruning safe? Suppose we try to reach next with cost c and find dist[next] ≤ c. That record was created by a state with at most as many flights as ours, because the queue has not yet produced anything with more stops. That earlier state is no more expensive and no longer, so anything our state could do later, it can do at least as well. Note also that a popped state expands using its own cost, not dist[node], because dist[node] may since have been lowered by a longer route that cannot afford the extra flights.
Algorithm.
dist[src] = 0, all others infinity. Push{0, src, 0}.- Pop
{stops, node, cost}. Ifstops > k, skip it: one more flight would exceed the limit. - For each flight
node -> nextwithprice: ifcost + price < dist[next], setdist[next]and push{stops + 1, next, cost + price}. - When the queue empties, return
dist[dst]or-1.
static int bfsByStops(int n, int[][] flights, int src, int dst, int k) {
List<List<int[]>> adj = buildGraph(n, flights);
int[] dist = new int[n];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[src] = 0;
Deque<int[]> queue = new ArrayDeque<>();
queue.offer(new int[]{0, src, 0}); // {stops, node, cost}
while (!queue.isEmpty()) {
int[] cur = queue.poll();
int stops = cur[0], node = cur[1], cost = cur[2];
if (stops > k) continue; // one more flight would exceed k stops
for (int[] e : adj.get(node)) {
int next = e[0], price = e[1];
if (cost + price < dist[next]) {
dist[next] = cost + price;
queue.offer(new int[]{stops + 1, next, cost + price});
}
}
}
return dist[dst] == Integer.MAX_VALUE ? -1 : dist[dst];
}
Dry run on the trap example (k = 1, dst = 3):
| popped | stops ok? | relaxations | queue after |
|---|---|---|---|
| {0, 0, 0} | yes | dist[1] = 1, dist[2] = 5 | {1,1,1}, {1,2,5} |
| {1, 1, 1} | yes | dist[2] = 2 | {1,2,5}, {2,2,2} |
| {1, 2, 5} | yes | dist[3] = 6 | {2,2,2}, {2,3,6} |
| {2, 2, 2} | no (2 > 1) | none | {2,3,6} |
| {2, 3, 6} | no | none | empty |
The state {1, 2, 5} is expanded with cost 5 even though dist[2] is already 2. That is exactly the arrival plain Dijkstra would have thrown away.
Complexity. Each state relaxes its city’s outgoing flights, states are created only when they strictly lower a dist value, and nothing past level k + 1 is expanded. In practice this runs in about O(k · E) time. The bound is not tight in theory, because a city can be improved several times within one level and is then expanded several times. Space O(n + E) for the graph plus the queue.
Approach 3: Optimal, Bellman-Ford with k + 1 rounds
Intuition. Bellman-Ford relaxes every edge once per round. Its invariant is that after r rounds, dist[v] is no more than the cheapest route to v that uses at most r edges. Normally a round can do better than that: if edge u → v is relaxed after dist[u] was lowered in the same round, a route can grow by several edges in one round. For ordinary shortest paths that is a harmless speed-up, but here it would silently break the flight limit.
So each round reads from the previous round’s array and writes into a copy. Then after round r (counting from 0), dist[v] is exactly the cheapest price using at most r + 1 flights. Running k + 1 rounds answers the question.
Algorithm.
dist[src] = 0, all others infinity.- Repeat
k + 1times:next = copy of dist; for every flightu -> v, ifdist[u]is finite anddist[u] + price < next[v], setnext[v]. Thendist = next. - Return
dist[dst]or-1.
static int bellmanFordKRounds(int n, int[][] flights, int src, int dst, int k) {
int[] dist = new int[n];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[src] = 0;
for (int round = 0; round <= k; round++) { // after round r, dist uses at most r + 1 flights
int[] next = dist.clone();
for (int[] f : flights) {
int u = f[0], v = f[1], price = f[2];
if (dist[u] != Integer.MAX_VALUE && dist[u] + price < next[v]) next[v] = dist[u] + price;
}
dist = next;
}
return dist[dst] == Integer.MAX_VALUE ? -1 : dist[dst];
}
Dry run on Example 1 (k = 1, so two rounds):
| round | reads | writes | dist after |
|---|---|---|---|
| start | [0, ∞, ∞, ∞] | ||
| 0 (1 flight) | dist[0] = 0 | next[1] = 100 | [0, 100, ∞, ∞] |
| 1 (2 flights) | dist[1] = 100 | next[2] = 200, next[3] = 700 | [0, 100, 200, 700] |
In round 1 the flight 2 → 3 reads the old dist[2] = ∞, so it does nothing. Had we updated in place, it would have read the fresh 200 and produced 400, a three-flight route that k = 1 does not allow.
Complexity. Time O(k · (n + E)): k + 1 rounds, each copying n values and scanning every flight once. Space O(n) for two distance arrays. The bound is guaranteed regardless of graph shape, which is why this is the version to reach for when you need a predictable worst case.
Edge cases and pitfalls
- Stops versus flights.
kstops isk + 1flights. In the BFS, the checkstops > kis applied before taking one more flight. In Bellman-Ford, the loop runsk + 1times. - In-place relaxation. Updating
distdirectly inside a Bellman-Ford round lets one round chain several flights. Always relax from the previous round’s array. - Plain Dijkstra. Pruning by price alone discards cheaper-in-stops arrivals. The Java tests include the trap graph above with expected answer
6. - Unreachable destination. Return
-1. Checkdist[u] != Integer.MAX_VALUEbefore adding a price, or the sum overflows into a negative number that looks like a bargain. - Cycles. The flight graph may contain cycles (Example 1 has
0 → 1 → 2 → 0). The flight budget in the brute force and the level limit in the other two approaches stop them from looping forever. - One-way flights. A flight
[1, 0, 3]does not let you fly from0to1.
Related problems
- Djisktra’s Algorithm is the cost-ordered search that fails here without the stop dimension.
- Bellman Ford Algorithm explains the round invariant that Approach 3 restricts.
- Network Delay Time is the same graph model with no stop limit.
- Shortest path in undirected graph with unit weights shows the plain BFS layering that Approach 2 builds on.
- Path with minimum effort is another Dijkstra variant with a non-standard cost.