questions C
This commit is contained in:
219
backend/data/questions/cheapest-flights-within-k-stops.yaml
Normal file
219
backend/data/questions/cheapest-flights-within-k-stops.yaml
Normal file
@@ -0,0 +1,219 @@
|
||||
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:
|
||||
- bfs
|
||||
- dynamic-programming
|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user