Facebook Pixel

787. Cheapest Flights Within K Stops

Problem Description

You are given a network of n cities connected by flights. Each flight is represented as [from, to, price], indicating a direct flight from one city to another with a specific cost.

Your task is to find the cheapest way to travel from a source city src to a destination city dst, with the constraint that you can take at most k stops (intermediate cities) along the way.

Key Points:

  • The flights array contains all available direct flights between cities
  • Each flight has a fixed price and goes in one direction only
  • You want to minimize the total cost of travel
  • You can have at most k stops between source and destination (not counting the source and destination themselves)
  • If no valid route exists within the stop limit, return -1

Example Scenario: If you have flights connecting cities 0β†’1 (cost 100), 1β†’2 (cost 100), and 0β†’2 (cost 500), and you want to go from city 0 to city 2 with at most 1 stop:

  • Direct route: 0β†’2 costs 500
  • Route with 1 stop: 0β†’1β†’2 costs 200
  • The cheapest price would be 200

The solution uses a modified Bellman-Ford algorithm that relaxes edges exactly k+1 times to ensure we find the shortest path with at most k stops. The algorithm maintains an array dist where dist[i] represents the minimum cost to reach city i from the source. By iterating k+1 times and updating distances based on available flights, it finds the optimal path within the stop constraint.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: The problem involves cities (nodes) connected by flights (edges), forming a graph structure.

Is it a tree?

  • No: The graph can have multiple paths between cities and may contain cycles, so it's not a tree structure.

Is the problem related to directed acyclic graphs?

  • No: While the flights are directed edges, the graph may contain cycles (you could potentially fly in circles between cities).

Is the problem related to shortest paths?

  • Yes: We're looking for the cheapest (minimum cost) path from source to destination.

Is the graph weighted?

  • Yes: Each flight has an associated cost/price, making this a weighted graph.

Dijkstra's Algorithm

  • The flowchart leads us to Dijkstra's Algorithm for weighted shortest path problems.

However, there's an important constraint that modifies our approach: we need to limit the path to at most k stops. This constraint makes the problem slightly different from a standard shortest path problem. While the flowchart suggests Dijkstra's, the solution actually uses a modified Bellman-Ford algorithm (which can be viewed as a form of dynamic programming with controlled depth).

The connection to DFS: Although the provided solution uses Bellman-Ford, this problem can also be solved using DFS with memoization, where we explore all paths from source to destination while tracking the number of stops and pruning paths that exceed k stops. The DFS approach would backtrack when either reaching the destination, exceeding the stop limit, or encountering a more expensive path than previously found.

Conclusion: While the flowchart correctly identifies this as a weighted shortest path problem leading to Dijkstra's, the additional constraint of limiting stops makes modified Bellman-Ford or DFS with pruning more suitable approaches.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is recognizing that this problem asks for the shortest path with a constraint on the number of edges (stops) we can traverse. This immediately suggests we need to track both the cost and the number of stops taken to reach each city.

Think of it this way: in a standard shortest path problem, we only care about the minimum cost to reach each node. But here, a path might be cheaper overall but use too many stops, making it invalid. Conversely, a more expensive path with fewer stops might be our only valid option.

The Bellman-Ford algorithm naturally fits this problem because it works by relaxing edges in iterations. Each iteration essentially extends paths by one more edge. If we run the algorithm for exactly k+1 iterations, we guarantee finding the shortest path that uses at most k+1 edges (which means at most k stops between source and destination).

Why k+1 iterations? Because:

  • After 0 iterations: we only know the cost to the source (0)
  • After 1 iteration: we know the best cost to cities reachable with direct flights
  • After 2 iterations: we know the best cost to cities reachable with 1 stop
  • After k+1 iterations: we know the best cost to cities reachable with k stops

The clever part of the implementation is using a backup array. During each iteration, we read costs from the backup (representing paths with one fewer edge) and write to the current array. This ensures that within a single iteration, we only extend paths by exactly one edge, preventing us from accidentally using results from the current iteration to further extend paths in the same iteration.

This approach guarantees we find the minimum cost path that respects our stop constraint, returning -1 if no such path exists within the limit.

Learn more about Depth-First Search, Breadth-First Search, Graph, Dynamic Programming, Shortest Path and Heap (Priority Queue) patterns.

Solution Approach

The solution implements a modified Bellman-Ford algorithm to find the shortest path with at most k stops. Let's walk through the implementation step by step:

Initialization:

INF = 0x3F3F3F3F
dist = [INF] * n
dist[src] = 0
  • We initialize a distance array dist where dist[i] represents the minimum cost to reach city i
  • All cities start with infinite cost (INF), except the source city which has cost 0
  • The value 0x3F3F3F3F is used as infinity - it's large enough to represent unreachable cities but small enough to avoid overflow when adding prices

Main Algorithm Loop:

for _ in range(k + 1):
    backup = dist.copy()
    for f, t, p in flights:
        dist[t] = min(dist[t], backup[f] + p)

The algorithm runs for exactly k + 1 iterations:

  1. Create a backup: Before each iteration, we copy the current distance array. This backup represents the shortest distances using paths with one fewer edge.

  2. Relax all edges: For each flight [from, to, price], we check if we can reach the destination city to more cheaply by taking this flight from city from. The key formula is:

    dist[to] = min(dist[to], backup[from] + price)
  3. Why use backup? By reading from backup[from] instead of dist[from], we ensure that within a single iteration, we only extend paths by exactly one edge. This prevents us from using updated values from the current iteration, which would incorrectly allow paths with more edges than intended.

Example walkthrough:

  • Iteration 0: Only source city has cost 0
  • Iteration 1: Cities directly reachable from source get updated
  • Iteration 2: Cities reachable with 1 stop get updated
  • ...
  • Iteration k+1: Cities reachable with k stops get updated

Return Result:

return -1 if dist[dst] == INF else dist[dst]

After k + 1 iterations, dist[dst] contains the minimum cost to reach the destination with at most k stops. If it's still infinity, no valid path exists within the stop limit.

Time Complexity: O((k + 1) Γ— E) where E is the number of flights Space Complexity: O(n) for the distance array and its backup

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to illustrate how the modified Bellman-Ford algorithm finds the cheapest price with limited stops.

Example Setup:

  • Cities: 0, 1, 2, 3
  • Flights: [[0,1,100], [1,2,100], [0,2,500], [1,3,200], [2,3,100]]
  • Source: 0, Destination: 3, Maximum stops k: 1

We need to find the cheapest way from city 0 to city 3 with at most 1 stop.

Initialization:

dist = [0, INF, INF, INF]  // Only source (city 0) starts with cost 0

Iteration 1 (Direct flights from source):

  • Create backup: backup = [0, INF, INF, INF]
  • Process each flight:
    • Flight [0,1,100]: dist[1] = min(INF, 0 + 100) = 100
    • Flight [1,2,100]: dist[2] = min(INF, INF + 100) = INF (can't use, city 1 not reached in backup)
    • Flight [0,2,500]: dist[2] = min(INF, 0 + 500) = 500
    • Flight [1,3,200]: dist[3] = min(INF, INF + 200) = INF (can't use, city 1 not reached in backup)
    • Flight [2,3,100]: dist[3] = min(INF, INF + 100) = INF (can't use, city 2 not reached in backup)
  • After iteration 1: dist = [0, 100, 500, INF]

Iteration 2 (Paths with 1 stop):

  • Create backup: backup = [0, 100, 500, INF]
  • Process each flight:
    • Flight [0,1,100]: dist[1] = min(100, 0 + 100) = 100 (no improvement)
    • Flight [1,2,100]: dist[2] = min(500, 100 + 100) = 200 (improved! Found 0β†’1β†’2 path)
    • Flight [0,2,500]: dist[2] = min(200, 0 + 500) = 200 (no improvement)
    • Flight [1,3,200]: dist[3] = min(INF, 100 + 200) = 300 (found 0β†’1β†’3 path)
    • Flight [2,3,100]: dist[3] = min(300, 500 + 100) = 300 (0β†’2β†’3 costs 600, worse than current)
  • After iteration 2: dist = [0, 100, 200, 300]

Result: dist[3] = 300, so the cheapest price from city 0 to city 3 with at most 1 stop is 300.

The winning path is 0β†’1β†’3 (1 stop at city 1), costing 100 + 200 = 300.

Key Observations:

  1. In iteration 1, we only discovered cities directly reachable from the source
  2. In iteration 2, we discovered paths using exactly 1 stop
  3. The backup array ensured we didn't chain multiple flights within the same iteration
  4. The algorithm correctly found that 0β†’1β†’3 (300) is cheaper than the alternative 0β†’2β†’3 (600)

Solution Implementation

1class Solution:
2    def findCheapestPrice(
3        self, n: int, flights: List[List[int]], src: int, dst: int, k: int
4    ) -> int:
5        # Initialize a large value to represent infinity (unreachable nodes)
6        INF = 0x3F3F3F3F
7      
8        # Initialize distance array with infinity for all nodes
9        # dist[i] represents the minimum cost to reach node i from source
10        dist = [INF] * n
11      
12        # Set the distance to source node as 0 (no cost to reach itself)
13        dist[src] = 0
14      
15        # Perform Bellman-Ford relaxation for at most k+1 edges
16        # k stops means we can use at most k+1 edges in our path
17        for _ in range(k + 1):
18            # Create a backup of current distances to avoid using updated values
19            # in the same iteration (ensures we only consider paths with exact edge count)
20            backup = dist.copy()
21          
22            # Relax all edges: try to find shorter paths
23            for from_city, to_city, price in flights:
24                # Update distance to destination if going through this edge is cheaper
25                # Use backup[from_city] to ensure we're using distances from previous iteration
26                dist[to_city] = min(dist[to_city], backup[from_city] + price)
27      
28        # Return -1 if destination is unreachable, otherwise return the minimum cost
29        return -1 if dist[dst] == INF else dist[dst]
30
1class Solution {
2    // Use a large value to represent infinity (unreachable nodes)
3    private static final int INFINITY = 0x3f3f3f3f;
4
5    /**
6     * Finds the cheapest price from source to destination with at most k stops.
7     * Uses Bellman-Ford algorithm variant that runs exactly k+1 iterations.
8     * 
9     * @param n       Number of cities (nodes)
10     * @param flights Array of flights where each flight is [from, to, price]
11     * @param src     Source city
12     * @param dst     Destination city
13     * @param k       Maximum number of stops allowed
14     * @return        Minimum cost to reach destination, or -1 if unreachable
15     */
16    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
17        // Initialize distance array to track minimum cost to reach each city
18        int[] distances = new int[n];
19        // Backup array to store distances from previous iteration
20        int[] previousDistances = new int[n];
21      
22        // Initially, all cities are unreachable except the source
23        Arrays.fill(distances, INFINITY);
24        distances[src] = 0;
25      
26        // Perform k+1 iterations (allowing at most k stops means k+1 edges)
27        for (int iteration = 0; iteration < k + 1; iteration++) {
28            // Create a snapshot of current distances before updating
29            // This ensures we only consider paths with exactly 'iteration' edges
30            System.arraycopy(distances, 0, previousDistances, 0, n);
31          
32            // Relax all edges based on previous iteration's distances
33            for (int[] flight : flights) {
34                int fromCity = flight[0];
35                int toCity = flight[1];
36                int price = flight[2];
37              
38                // Update distance if we found a cheaper path through fromCity
39                distances[toCity] = Math.min(distances[toCity], 
40                                            previousDistances[fromCity] + price);
41            }
42        }
43      
44        // Return the minimum cost to destination, or -1 if unreachable
45        return distances[dst] == INFINITY ? -1 : distances[dst];
46    }
47}
48
1class Solution {
2public:
3    int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int k) {
4        // Initialize infinity value for unreachable nodes
5        const int INF = 0x3f3f3f3f;
6      
7        // Distance array to store minimum cost to reach each city
8        // Initially all cities are unreachable (infinity cost)
9        vector<int> distance(n, INF);
10      
11        // Backup array to store previous iteration's distances
12        // This prevents using updated values within the same iteration
13        vector<int> previousDistance;
14      
15        // Starting city has cost 0
16        distance[src] = 0;
17      
18        // Perform Bellman-Ford relaxation for at most k+1 edges
19        // k stops means we can use at most k+1 flights
20        for (int iteration = 0; iteration <= k; ++iteration) {
21            // Create backup of current distances
22            previousDistance = distance;
23          
24            // Relax all edges (flights)
25            for (const auto& flight : flights) {
26                int fromCity = flight[0];
27                int toCity = flight[1];
28                int price = flight[2];
29              
30                // Update distance if we found a cheaper path
31                // Use previousDistance[fromCity] to ensure we only use paths from previous iteration
32                distance[toCity] = min(distance[toCity], previousDistance[fromCity] + price);
33            }
34        }
35      
36        // Return the minimum cost to destination, or -1 if unreachable
37        return distance[dst] == INF ? -1 : distance[dst];
38    }
39};
40
1function findCheapestPrice(n: number, flights: number[][], src: number, dst: number, k: number): number {
2    // Initialize infinity value for unreachable nodes
3    const INF = 0x3f3f3f3f;
4  
5    // Distance array to store minimum cost to reach each city
6    // Initially all cities are unreachable (infinity cost)
7    const distance: number[] = new Array(n).fill(INF);
8  
9    // Starting city has cost 0
10    distance[src] = 0;
11  
12    // Perform Bellman-Ford relaxation for at most k+1 edges
13    // k stops means we can use at most k+1 flights
14    for (let iteration = 0; iteration <= k; iteration++) {
15        // Create backup of current distances
16        // This prevents using updated values within the same iteration
17        const previousDistance: number[] = [...distance];
18      
19        // Relax all edges (flights)
20        for (const flight of flights) {
21            const fromCity = flight[0];
22            const toCity = flight[1];
23            const price = flight[2];
24          
25            // Update distance if we found a cheaper path
26            // Use previousDistance[fromCity] to ensure we only use paths from previous iteration
27            distance[toCity] = Math.min(distance[toCity], previousDistance[fromCity] + price);
28        }
29    }
30  
31    // Return the minimum cost to destination, or -1 if unreachable
32    return distance[dst] === INF ? -1 : distance[dst];
33}
34

Time and Space Complexity

Time Complexity: O((k + 1) * m) where m is the number of flights (edges) and k is the maximum number of stops allowed.

The algorithm performs k + 1 iterations of the outer loop. In each iteration, it processes all m flights to update the distance array. Each flight update operation takes O(1) time (comparing and potentially updating the distance). Additionally, creating a backup copy of the distance array takes O(n) time per iteration, where n is the number of nodes. Therefore, the total time complexity is O((k + 1) * (n + m)). However, since in most practical cases m β‰₯ n - 1 for a connected graph, this can be simplified to O((k + 1) * m).

Space Complexity: O(n) where n is the number of nodes.

The algorithm uses two arrays: dist and backup, each of size n. The dist array stores the minimum distances from the source to all nodes, while backup is a temporary copy created in each iteration. No additional data structures scale with the input size, so the space complexity is O(n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Not Using a Backup Array

The Problem: A common mistake is trying to update the distance array in-place without creating a backup:

# INCORRECT implementation
for _ in range(k + 1):
    for f, t, p in flights:
        dist[t] = min(dist[t], dist[f] + p)  # Reading and writing to same array!

This approach fails because within a single iteration, we might use already-updated values from the current iteration. For example, if we update dist[1] early in the iteration and then use dist[1] to update dist[2] later in the same iteration, we've effectively considered a path with more edges than intended for this iteration.

The Solution: Always create a backup copy before each iteration and read from the backup while writing to the original:

# CORRECT implementation
for _ in range(k + 1):
    backup = dist.copy()
    for f, t, p in flights:
        dist[t] = min(dist[t], backup[f] + p)  # Read from backup, write to dist

Pitfall 2: Incorrect Iteration Count

The Problem: Using k iterations instead of k + 1:

# INCORRECT - only iterates k times
for _ in range(k):  # Wrong! This only allows k-1 stops
    backup = dist.copy()
    for f, t, p in flights:
        dist[t] = min(dist[t], backup[f] + p)

Remember that k represents the maximum number of stops (intermediate cities), not the number of edges. If you can have k stops, you can use up to k + 1 edges in your path.

The Solution: Always iterate k + 1 times to allow for paths with up to k + 1 edges:

# CORRECT - iterates k+1 times
for _ in range(k + 1):  # Allows exactly k stops (k+1 edges)
    backup = dist.copy()
    for f, t, p in flights:
        dist[t] = min(dist[t], backup[f] + p)

Pitfall 3: Integer Overflow with Infinity Value

The Problem: Using float('inf') or sys.maxsize can cause issues:

# Potentially problematic
dist = [float('inf')] * n  # Can cause comparison issues
# or
dist = [sys.maxsize] * n   # Might overflow when adding prices

When adding prices to infinity values, you might encounter overflow or unexpected behavior in comparisons.

The Solution: Use a large but reasonable value like 0x3F3F3F3F (approximately 10^9):

# CORRECT - safe infinity value
INF = 0x3F3F3F3F  # Large enough but won't overflow
dist = [INF] * n

This value is large enough to represent unreachable cities but small enough that adding typical flight prices won't cause overflow issues.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of these properties could exist for a graph but not a tree?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More