242 lines
12 KiB
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
|
|
|
|
|
|
|
|
**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`
|
|
|
|
|
|
|
|
**Step 3: Return the result**
|
|
|
|
- If `prices[dst]` is still infinity, return `-1` (no valid path within `k` stops)
|
|
- Otherwise, return `prices[dst]`
|
|
|
|
|
|
|
|
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.
|