Files
codetutor/backend/data/questions/cheapest-flights-within-k-stops.yaml

242 lines
12 KiB
YAML

title: Cheapest Flights Within K Stops
slug: cheapest-flights-within-k-stops
difficulty: medium
leetcode_id: 787
leetcode_url: https://leetcode.com/problems/cheapest-flights-within-k-stops/
categories:
- graphs
- dynamic-programming
patterns:
- slug: bfs
is_optimal: true
- slug: dynamic-programming
is_optimal: false
function_signature: "def findCheapestPrice(n: int, flights: list[list[int]], src: int, dst: int, k: int) -> int:"
test_cases:
visible:
- 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 }
expected: 700
- input: { n: 3, flights: [[0, 1, 100], [1, 2, 100], [0, 2, 500]], src: 0, dst: 2, k: 1 }
expected: 200
- input: { n: 3, flights: [[0, 1, 100], [1, 2, 100], [0, 2, 500]], src: 0, dst: 2, k: 0 }
expected: 500
hidden:
- input: { n: 2, flights: [[0, 1, 100]], src: 0, dst: 1, k: 0 }
expected: 100
- input: { n: 3, flights: [[0, 1, 100], [1, 2, 100]], src: 0, dst: 2, k: 0 }
expected: -1
- input: { n: 4, flights: [[0, 1, 1], [0, 2, 5], [1, 2, 1], [2, 3, 1]], src: 0, dst: 3, k: 1 }
expected: 6
- input: { n: 5, flights: [[0, 1, 5], [1, 2, 5], [0, 3, 2], [3, 1, 2], [1, 4, 1], [4, 2, 1]], src: 0, dst: 2, k: 2 }
expected: 7
description: |
There are `n` cities connected by some number of flights. You are given an array `flights` where `flights[i] = [from_i, to_i, price_i]` indicates that there is a flight from city `from_i` to city `to_i` with cost `price_i`.
You are also given three integers `src`, `dst`, and `k`, return *the cheapest price* from `src` to `dst` with at most `k` stops. If there is no such route, return `-1`.
constraints: |
- `2 <= n <= 100`
- `0 <= flights.length <= (n * (n - 1) / 2)`
- `flights[i].length == 3`
- `0 <= from_i, to_i < n`
- `from_i != to_i`
- `1 <= price_i <= 10^4`
- There will not be any multiple flights between two cities.
- `0 <= src, dst, k < n`
- `src != dst`
examples:
- 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 optimal path with at most 1 stop from city 0 to 3 is 0 → 1 → 3 with cost 100 + 600 = 700. Note that the path 0 → 1 → 2 → 3 is cheaper (400) but is invalid because it uses 2 stops."
- input: "n = 3, flights = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 1"
output: "200"
explanation: "The optimal path with at most 1 stop from city 0 to 2 is 0 → 1 → 2 with cost 100 + 100 = 200."
- input: "n = 3, flights = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 0"
output: "500"
explanation: "The optimal path with no stops from city 0 to 2 is the direct flight 0 → 2 with cost 500."
explanation:
intuition: |
Imagine you're planning a trip with a strict layover limit. You can take connecting flights, but you can only stop at most `k` intermediate cities. The goal is to find the cheapest way to get from your origin to your destination within this constraint.
This is a **shortest path problem with a twist**: traditional algorithms like Dijkstra's find the absolute shortest path, but here we need the shortest path *with a constraint on the number of edges (stops)*. A cheaper path that uses too many stops is invalid.
Think of it like this: at each "level" of stops, we want to know the cheapest way to reach each city. Starting from `src` with 0 stops used, we can relax the costs to neighboring cities. Then with 1 stop used, we can reach cities two hops away, and so on. After `k+1` iterations (since `k` stops means `k+1` edges), we check the cost to reach `dst`.
The **Bellman-Ford algorithm** is perfectly suited for this because it relaxes edges level by level, naturally incorporating the stop constraint. We simply limit the number of relaxation rounds to `k+1`.
approach: |
We solve this using the **Bellman-Ford Algorithm** modified for at most `k` stops:
**Step 1: Initialise the distance array**
- Create a `prices` array of size `n`, initialised to infinity for all cities
- Set `prices[src] = 0` since we start at the source with zero cost
&nbsp;
**Step 2: Relax edges for k+1 iterations**
- We need at most `k+1` edges to make `k` stops (source → stop1 → stop2 → ... → destination)
- For each iteration, create a **copy of the current prices** (critical for correctness)
- For each flight `[from, to, price]`, check if we can improve the cost to `to`:
- If `prices[from] + price < temp[to]`, update `temp[to]`
- After processing all edges, update `prices = temp`
&nbsp;
**Step 3: Return the result**
- If `prices[dst]` is still infinity, return `-1` (no valid path within `k` stops)
- Otherwise, return `prices[dst]`
&nbsp;
The key insight is using a temporary copy each round. This ensures we only use paths from the *previous* iteration, preventing us from using more edges than allowed in a single round.
common_pitfalls:
- title: Using Standard Dijkstra's Algorithm
description: |
Standard Dijkstra's always finds the shortest path regardless of the number of edges. It might return a path with more than `k` stops if that path is cheaper.
For example, with `k = 1`, Dijkstra's might find a path with 5 stops if it's the cheapest overall. We need an algorithm that respects the stop limit.
wrong_approach: "Standard Dijkstra's without stop tracking"
correct_approach: "Bellman-Ford with k+1 iterations or BFS with stop counting"
- title: Not Using a Temporary Copy
description: |
When relaxing edges, if you update `prices` directly without a copy, you might use an edge that was just updated in the same iteration. This means you could take multiple edges in what should be a single "round".
For example, if `prices[A]` is updated and then immediately used to update `prices[B]`, you've effectively used two edges in one iteration.
wrong_approach: "Update prices array directly during relaxation"
correct_approach: "Copy prices to temp before each round, update temp, then assign back"
- title: Confusion About k Stops vs k+1 Edges
description: |
With `k` stops, you can traverse `k+1` edges (flights). If you only iterate `k` times instead of `k+1`, you'll miss valid paths.
Example: `k = 1` means one intermediate stop, so paths like `src → city → dst` are valid (2 edges). You need 2 iterations of Bellman-Ford.
wrong_approach: "Iterate exactly k times"
correct_approach: "Iterate k+1 times"
- title: Forgetting to Handle No Valid Path
description: |
If there's no path from `src` to `dst` within `k` stops, `prices[dst]` remains infinity. You must check for this and return `-1`.
key_takeaways:
- "**Bellman-Ford for constrained shortest paths**: When you need to limit the number of edges, Bellman-Ford's level-by-level relaxation is ideal"
- "**Temporary copy prevents over-relaxation**: Using a copy each round ensures we don't use more edges than allowed"
- "**BFS is an alternative**: BFS level-by-level also works, treating each level as one more stop"
- "**Foundation for flight booking problems**: This pattern appears in real-world scenarios like finding cheapest flights with layover limits"
time_complexity: "O(k * E) where E is the number of flights. We iterate k+1 times, and each iteration processes all E edges."
space_complexity: "O(n). We store the prices array of size n, plus a temporary copy of size n."
solutions:
- approach_name: Bellman-Ford
is_optimal: true
code: |
def findCheapestPrice(n: int, flights: list[list[int]], src: int, dst: int, k: int) -> int:
# Initialise prices to infinity, source is 0
prices = [float('inf')] * n
prices[src] = 0
# Relax edges k+1 times (k stops = k+1 edges)
for _ in range(k + 1):
# Use a copy to prevent using multiple edges in one round
temp = prices.copy()
# Try to relax each edge
for from_city, to_city, price in flights:
# Can we reach to_city cheaper via from_city?
if prices[from_city] != float('inf'):
temp[to_city] = min(temp[to_city], prices[from_city] + price)
prices = temp
# Return -1 if destination unreachable within k stops
return prices[dst] if prices[dst] != float('inf') else -1
explanation: |
**Time Complexity:** O(k * E) — We perform k+1 iterations, each processing all E edges.
**Space Complexity:** O(n) — Two arrays of size n (prices and temp).
This is the classic Bellman-Ford approach adapted for the stop constraint. By limiting iterations to k+1, we naturally enforce the maximum number of edges allowed.
- approach_name: BFS with Level-by-Level Relaxation
is_optimal: false
code: |
from collections import defaultdict, deque
def findCheapestPrice(n: int, flights: list[list[int]], src: int, dst: int, k: int) -> int:
# Build adjacency list
graph = defaultdict(list)
for from_city, to_city, price in flights:
graph[from_city].append((to_city, price))
# prices[city] = minimum cost to reach city
prices = [float('inf')] * n
prices[src] = 0
# BFS: (current_city, current_cost)
queue = deque([(src, 0)])
stops = 0
while queue and stops <= k:
# Process all nodes at current level
for _ in range(len(queue)):
city, cost = queue.popleft()
# Explore all neighbors
for next_city, price in graph[city]:
new_cost = cost + price
# Only add to queue if this is a better path
if new_cost < prices[next_city]:
prices[next_city] = new_cost
queue.append((next_city, new_cost))
stops += 1
return prices[dst] if prices[dst] != float('inf') else -1
explanation: |
**Time Complexity:** O(k * E) — Similar to Bellman-Ford, we process edges level by level.
**Space Complexity:** O(n + E) — Adjacency list takes O(E), queue can hold O(n) nodes per level.
This BFS approach processes cities level by level, where each level represents one additional stop. It's conceptually similar to Bellman-Ford but uses a queue structure.
- approach_name: Dynamic Programming (2D)
is_optimal: false
code: |
def findCheapestPrice(n: int, flights: list[list[int]], src: int, dst: int, k: int) -> int:
# dp[stops][city] = minimum cost to reach city using exactly 'stops' stops
INF = float('inf')
dp = [[INF] * n for _ in range(k + 2)]
dp[0][src] = 0
# Fill DP table
for stops in range(1, k + 2):
# Carry forward: can reach city with fewer stops
dp[stops] = dp[stops - 1].copy()
# Try each flight
for from_city, to_city, price in flights:
if dp[stops - 1][from_city] != INF:
dp[stops][to_city] = min(
dp[stops][to_city],
dp[stops - 1][from_city] + price
)
return dp[k + 1][dst] if dp[k + 1][dst] != INF else -1
explanation: |
**Time Complexity:** O(k * E) — We iterate through k+2 rows and process all E edges per row.
**Space Complexity:** O(k * n) — 2D DP table with k+2 rows and n columns.
This explicit DP formulation makes the state clear: `dp[i][j]` represents the minimum cost to reach city `j` using at most `i` edges. The Bellman-Ford solution is essentially this DP with space optimisation.