Skip to content

Commit c54dc02

Browse files
committed
feat(algorithms, graphs, cheapest-flight): cheapest flight with k stops
1 parent 99f5f91 commit c54dc02

3 files changed

Lines changed: 190 additions & 0 deletions

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Cheapest Flights Within K Stops
2+
3+
You are given n cities, numbered from 0 to n−1 connected by several flights. You are also given an array flights, where
4+
each flight is represented as `flights[i]=[fromi ,toi, pricei]` meaning there is a direct flight from city
5+
`fromᵢ` to city `toᵢ` with a cost of `priceᵢ`.
6+
7+
You are also given three integers:
8+
9+
- src: The starting city.
10+
- dst: The destination city.
11+
- k: The maximum number of stops allowed on the route (i.e., intermediate cities between src and dst).
12+
13+
Your task is to find the minimum possible cost to travel from src to dst using at most k stops (i.e., the route may
14+
contain up to k + 1 flights). If there is no valid route from src to dst that uses at most k stops, return −1.
15+
16+
## Constraints
17+
18+
- 2 <= n <= 100
19+
- 0 <= flights.length <= (n * (n - 1) / 2)
20+
- flights[i].length == 3
21+
- 0 <= `fromi`, `toi` < n
22+
- `fromi` != `toi`
23+
- 1 <= `pricei` <= 10^4
24+
- There will not be any multiple flights between two cities.
25+
- 0 <= src, dst, k < n
26+
- src != dst
27+
28+
## Examples
29+
30+
Example 1
31+
```text
32+
Input: n = 4, flights = [[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]], src = 0, dst = 3, k = 1
33+
Output: 700
34+
Explanation:
35+
The graph is shown above.
36+
The optimal path with at most 1 stop from city 0 to 3 is marked in red and has cost 100 + 600 = 700.
37+
Note that the path through cities [0,1,2,3] is cheaper but is invalid because it uses 2 stops.
38+
```
39+
40+
Example 2
41+
```text
42+
Input: n = 3, flights = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 1
43+
Output: 200
44+
Explanation:
45+
The graph is shown above.
46+
The optimal path with at most 1 stop from city 0 to 2 is marked in red and has cost 100 + 100 = 200.
47+
```
48+
49+
Example 3
50+
```text
51+
Input: n = 3, flights = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 0
52+
Output: 500
53+
Explanation:
54+
The graph is shown above.
55+
The optimal path with no stops from city 0 to 2 is marked in red and has cost 500.
56+
```
57+
58+
## Topics
59+
60+
- Dynamic Programming
61+
- Depth-First Search
62+
- Breadth-First Search
63+
- Graph Theory
64+
- Heap (Priority Queue)
65+
- Shortest Path
66+
67+
## Solution
68+
69+
The core intuition behind this solution is to treat the problem as a shortest path in a directed, weighted graph with a
70+
strict limit on the number of edges (flights), i.e., we are only allowed to use at most k stops, meaning at most k + 1
71+
flights (edges). Traditional algorithms, such as Standard Dijkstra with a single dist[node], doesn’t enforce the stop
72+
bound because they always choose the globally cheapest path so far, even if that path exceeds the allowed number of stops.
73+
To correctly enforce the stop limit, we employ a Bellman–Ford–style dynamic programming approach, which naturally handles
74+
constraints on the number of edges in a path.
75+
76+
The idea is to repeatedly relax all flights, exactly k + 1 times, where iteration t represents allowing routes that use
77+
up to t flights (one more than the previous iteration). After each iteration, we have the cheapest costs using at most r
78+
flights (edges).
79+
80+
To achieve this, we maintain two arrays: `prices`, which stores the best costs found using up to t − 1 flights, and
81+
`temp_prices`, which stores the best costs for the current iteration t. During iteration t, every update to a path is
82+
made only from values in `prices`, not from updates made earlier in the same iteration. This separation ensures that any
83+
path discovered in iteration t uses at most one more flight than the paths from the previous iteration t − 1, preventing
84+
us from accidentally chaining multiple flights in the same round.
85+
86+
The algorithm builds valid paths layer by layer, only extending shorter paths into longer ones, guaranteeing that when
87+
all k + 1 iterations are done, we have explored all possible routes that use at most k stops. And the cheapest such route
88+
will be stored in prices[dst].
89+
90+
Using the intuition above, we implement the algorithm as follows:
91+
92+
1. Create an array `prices` of length n and initialize all its entries to infinity.
93+
2. Set `prices[src] = 0` because the cost to reach the starting city from itself is 0 and requires no flights.
94+
3. Iterate at most `k + 1` times to model the flight limit:
95+
- Initialize a new array, `temp_prices`, with a copy of the dist array. Copying ensures we can also keep the best
96+
older answers (using fewer flights) instead of forcing exactly t flights.
97+
- For each flight:
98+
- If `prices[u]` is not equal to infinity, and the candidate cost: `prices[u] + w` is less than the current
99+
`temp_prices[v]`:
100+
- Set `temp_prices[v]` to `prices[u] + w`.
101+
- After processing all flights in this iteration, set `prices` to `temp_prices`.
102+
4. After completing all `k + 1` iterations, check `prices[dst]`. If `prices[dst]` is still inf, it means there is no
103+
valid route from `src` to `dst` that uses at most `k` stops, so we return -1. Otherwise, return `prices[dst]`.
104+
105+
### Time Complexity
106+
107+
The time complexity of this algorithm is O((k+1)×m) because we perform k+1 relaxation rounds, and in each round, we
108+
iterate over all m flights once. Asymptotically, this simplifies to O(k×m).
109+
110+
### Space Complexity
111+
112+
The space complexity of this algorithm is O(n) because we maintain two arrays, dist and new_dist, each of size n (the
113+
number of cities). These arrays are reused across iterations, and no other auxiliary data structure grows with the input
114+
size.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from typing import List
2+
from math import inf
3+
4+
5+
def find_cheapest_price(
6+
n: int, flights: List[List[int]], src: int, dst: int, k: int
7+
) -> int:
8+
prices = [inf] * n
9+
prices[src] = 0
10+
11+
for i in range(k + 1):
12+
temp_prices = prices.copy()
13+
14+
for source, destination, price in flights:
15+
if prices[source] == inf:
16+
continue
17+
if prices[source] + price < temp_prices[destination]:
18+
temp_prices[destination] = prices[source] + price
19+
prices = temp_prices
20+
21+
return -1 if prices[dst] == inf else prices[dst]
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import unittest
2+
from typing import List
3+
from parameterized import parameterized
4+
from algorithms.graphs.cheapest_flights_with_k_stops import find_cheapest_price
5+
6+
CHEAPEST_FLIGHTS_WITH_K_STOPS_TEST_CASES = [
7+
(3, [[0, 1, 100], [1, 2, 100], [0, 2, 500]], 0, 2, 1, 200),
8+
(3, [[0, 1, 100], [1, 2, 400], [0, 2, 350]], 0, 2, 0, 350),
9+
(4, [[0, 1, 100], [1, 2, 100], [2, 3, 100]], 0, 3, 1, -1),
10+
(
11+
4,
12+
[[0, 1, 100], [1, 2, 100], [2, 0, 100], [1, 3, 600], [2, 3, 200]],
13+
0,
14+
3,
15+
1,
16+
700,
17+
),
18+
(
19+
5,
20+
[
21+
[0, 1, 100],
22+
[1, 2, 100],
23+
[2, 3, 100],
24+
[3, 4, 100],
25+
[0, 4, 1000],
26+
[0, 2, 500],
27+
[1, 3, 250],
28+
[2, 4, 200],
29+
],
30+
0,
31+
4,
32+
2,
33+
400,
34+
),
35+
(3, [[0, 1, 100], [1, 2, 100], [0, 2, 500]], 0, 2, 0, 500),
36+
]
37+
38+
39+
class CheapestFlightsWithKStopsTestCase(unittest.TestCase):
40+
@parameterized.expand(CHEAPEST_FLIGHTS_WITH_K_STOPS_TEST_CASES)
41+
def test_cheapest_flights_with_k_stops(
42+
self,
43+
n: int,
44+
flights: List[List[int]],
45+
src: int,
46+
dst: int,
47+
k: int,
48+
expected: int,
49+
):
50+
actual = find_cheapest_price(n, flights, src, dst, k)
51+
self.assertEqual(expected, actual)
52+
53+
54+
if __name__ == "__main__":
55+
unittest.main()

0 commit comments

Comments
 (0)