From c54dc021d4e43166740fa28c5769194fb42ef802 Mon Sep 17 00:00:00 2001 From: BrianLusina <12752833+BrianLusina@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:02:17 +0300 Subject: [PATCH 1/2] feat(algorithms, graphs, cheapest-flight): cheapest flight with k stops --- .../cheapest_flights_with_k_stops/README.md | 114 ++++++++++++++++++ .../cheapest_flights_with_k_stops/__init__.py | 21 ++++ .../test_cheapest_flights_with_k_stops.py | 55 +++++++++ 3 files changed, 190 insertions(+) create mode 100644 algorithms/graphs/cheapest_flights_with_k_stops/README.md create mode 100644 algorithms/graphs/cheapest_flights_with_k_stops/__init__.py create mode 100644 algorithms/graphs/cheapest_flights_with_k_stops/test_cheapest_flights_with_k_stops.py diff --git a/algorithms/graphs/cheapest_flights_with_k_stops/README.md b/algorithms/graphs/cheapest_flights_with_k_stops/README.md new file mode 100644 index 00000000..85aa3298 --- /dev/null +++ b/algorithms/graphs/cheapest_flights_with_k_stops/README.md @@ -0,0 +1,114 @@ +# Cheapest Flights Within K Stops + +You are given n cities, numbered from 0 to n−1 connected by several flights. You are also given an array flights, where +each flight is represented as `flights[i]=[fromi ,toi, pricei]` meaning there is a direct flight from city +`fromᵢ` to city `toᵢ` with a cost of `priceᵢ`. + +You are also given three integers: + +- src: The starting city. +- dst: The destination city. +- k: The maximum number of stops allowed on the route (i.e., intermediate cities between src and dst). + +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 +contain up to k + 1 flights). If there is no valid route from src to dst that uses at most k stops, return −1. + +## Constraints + +- 2 <= n <= 100 +- 0 <= flights.length <= (n * (n - 1) / 2) +- flights[i].length == 3 +- 0 <= `fromi`, `toi` < n +- `fromi` != `toi` +- 1 <= `pricei` <= 10^4 +- There will not be any multiple flights between two cities. +- 0 <= src, dst, k < n +- src != dst + +## Examples + +Example 1 +```text +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 +Output: 700 +Explanation: +The graph is shown above. +The optimal path with at most 1 stop from city 0 to 3 is marked in red and has cost 100 + 600 = 700. +Note that the path through cities [0,1,2,3] is cheaper but is invalid because it uses 2 stops. +``` + +Example 2 +```text +Input: n = 3, flights = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 1 +Output: 200 +Explanation: +The graph is shown above. +The optimal path with at most 1 stop from city 0 to 2 is marked in red and has cost 100 + 100 = 200. +``` + +Example 3 +```text +Input: n = 3, flights = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 0 +Output: 500 +Explanation: +The graph is shown above. +The optimal path with no stops from city 0 to 2 is marked in red and has cost 500. +``` + +## Topics + +- Dynamic Programming +- Depth-First Search +- Breadth-First Search +- Graph Theory +- Heap (Priority Queue) +- Shortest Path + +## Solution + +The core intuition behind this solution is to treat the problem as a shortest path in a directed, weighted graph with a +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 +flights (edges). Traditional algorithms, such as Standard Dijkstra with a single dist[node], doesn’t enforce the stop +bound because they always choose the globally cheapest path so far, even if that path exceeds the allowed number of stops. +To correctly enforce the stop limit, we employ a Bellman–Ford–style dynamic programming approach, which naturally handles +constraints on the number of edges in a path. + +The idea is to repeatedly relax all flights, exactly k + 1 times, where iteration t represents allowing routes that use +up to t flights (one more than the previous iteration). After each iteration, we have the cheapest costs using at most r +flights (edges). + +To achieve this, we maintain two arrays: `prices`, which stores the best costs found using up to t − 1 flights, and +`temp_prices`, which stores the best costs for the current iteration t. During iteration t, every update to a path is +made only from values in `prices`, not from updates made earlier in the same iteration. This separation ensures that any +path discovered in iteration t uses at most one more flight than the paths from the previous iteration t − 1, preventing +us from accidentally chaining multiple flights in the same round. + +The algorithm builds valid paths layer by layer, only extending shorter paths into longer ones, guaranteeing that when +all k + 1 iterations are done, we have explored all possible routes that use at most k stops. And the cheapest such route +will be stored in prices[dst]. + +Using the intuition above, we implement the algorithm as follows: + +1. Create an array `prices` of length n and initialize all its entries to infinity. +2. Set `prices[src] = 0` because the cost to reach the starting city from itself is 0 and requires no flights. +3. Iterate at most `k + 1` times to model the flight limit: + - Initialize a new array, `temp_prices`, with a copy of the dist array. Copying ensures we can also keep the best + older answers (using fewer flights) instead of forcing exactly t flights. + - For each flight: + - If `prices[u]` is not equal to infinity, and the candidate cost: `prices[u] + w` is less than the current + `temp_prices[v]`: + - Set `temp_prices[v]` to `prices[u] + w`. + - After processing all flights in this iteration, set `prices` to `temp_prices`. +4. After completing all `k + 1` iterations, check `prices[dst]`. If `prices[dst]` is still inf, it means there is no + valid route from `src` to `dst` that uses at most `k` stops, so we return -1. Otherwise, return `prices[dst]`. + +### Time Complexity + +The time complexity of this algorithm is O((k+1)×m) because we perform k+1 relaxation rounds, and in each round, we +iterate over all m flights once. Asymptotically, this simplifies to O(k×m). + +### Space Complexity + +The space complexity of this algorithm is O(n) because we maintain two arrays, dist and new_dist, each of size n (the +number of cities). These arrays are reused across iterations, and no other auxiliary data structure grows with the input +size. diff --git a/algorithms/graphs/cheapest_flights_with_k_stops/__init__.py b/algorithms/graphs/cheapest_flights_with_k_stops/__init__.py new file mode 100644 index 00000000..393a8d7d --- /dev/null +++ b/algorithms/graphs/cheapest_flights_with_k_stops/__init__.py @@ -0,0 +1,21 @@ +from typing import List +from math import inf + + +def find_cheapest_price( + n: int, flights: List[List[int]], src: int, dst: int, k: int +) -> int: + prices = [inf] * n + prices[src] = 0 + + for i in range(k + 1): + temp_prices = prices.copy() + + for source, destination, price in flights: + if prices[source] == inf: + continue + if prices[source] + price < temp_prices[destination]: + temp_prices[destination] = prices[source] + price + prices = temp_prices + + return -1 if prices[dst] == inf else prices[dst] diff --git a/algorithms/graphs/cheapest_flights_with_k_stops/test_cheapest_flights_with_k_stops.py b/algorithms/graphs/cheapest_flights_with_k_stops/test_cheapest_flights_with_k_stops.py new file mode 100644 index 00000000..32eb32ab --- /dev/null +++ b/algorithms/graphs/cheapest_flights_with_k_stops/test_cheapest_flights_with_k_stops.py @@ -0,0 +1,55 @@ +import unittest +from typing import List +from parameterized import parameterized +from algorithms.graphs.cheapest_flights_with_k_stops import find_cheapest_price + +CHEAPEST_FLIGHTS_WITH_K_STOPS_TEST_CASES = [ + (3, [[0, 1, 100], [1, 2, 100], [0, 2, 500]], 0, 2, 1, 200), + (3, [[0, 1, 100], [1, 2, 400], [0, 2, 350]], 0, 2, 0, 350), + (4, [[0, 1, 100], [1, 2, 100], [2, 3, 100]], 0, 3, 1, -1), + ( + 4, + [[0, 1, 100], [1, 2, 100], [2, 0, 100], [1, 3, 600], [2, 3, 200]], + 0, + 3, + 1, + 700, + ), + ( + 5, + [ + [0, 1, 100], + [1, 2, 100], + [2, 3, 100], + [3, 4, 100], + [0, 4, 1000], + [0, 2, 500], + [1, 3, 250], + [2, 4, 200], + ], + 0, + 4, + 2, + 400, + ), + (3, [[0, 1, 100], [1, 2, 100], [0, 2, 500]], 0, 2, 0, 500), +] + + +class CheapestFlightsWithKStopsTestCase(unittest.TestCase): + @parameterized.expand(CHEAPEST_FLIGHTS_WITH_K_STOPS_TEST_CASES) + def test_cheapest_flights_with_k_stops( + self, + n: int, + flights: List[List[int]], + src: int, + dst: int, + k: int, + expected: int, + ): + actual = find_cheapest_price(n, flights, src, dst, k) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() From 4b31f7a94de255eb0998bda98d08be1424e6a9c4 Mon Sep 17 00:00:00 2001 From: github-actions <${GITHUB_ACTOR}@users.noreply.github.com> Date: Thu, 16 Apr 2026 07:02:39 +0000 Subject: [PATCH 2/2] updating DIRECTORY.md --- DIRECTORY.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DIRECTORY.md b/DIRECTORY.md index 1ae82b52..9310756b 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -137,6 +137,8 @@ * [Test Alien Dictionary](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/alien_dictionary/test_alien_dictionary.py) * Cat And Mouse * [Test Cat And Mouse](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/cat_and_mouse/test_cat_and_mouse.py) + * Cheapest Flights With K Stops + * [Test Cheapest Flights With K Stops](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/cheapest_flights_with_k_stops/test_cheapest_flights_with_k_stops.py) * Course Schedule * [Test Course Schedule](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/course_schedule/test_course_schedule.py) * Evaluate Division