Facebook Pixel

2247. Maximum Cost of Trip With K Highways 🔒

Problem Description

You are given n cities numbered from 0 to n - 1 that are connected by highways. Each highway is described by highways[i] = [city1_i, city2_i, toll_i], which means there is a bidirectional highway between city1_i and city2_i with a toll cost of toll_i.

You need to plan a trip that:

  • Crosses exactly k highways
  • Can start from any city
  • Can visit each city at most once during the trip

Your goal is to find the maximum total toll cost for such a trip. If no valid trip exists that meets these requirements, return -1.

For example, if you have 4 cities and need to cross exactly 2 highways, you might start at city 0, take a highway to city 1 (paying toll₁), then take another highway to city 3 (paying toll₂). The total cost would be toll₁ + toll₂. You want to find the path that maximizes this total cost while meeting all constraints.

The key constraints are:

  • You must use exactly k highways (not fewer, not more)
  • Each city can be visited at most once (no revisiting cities)
  • The highways are bidirectional (you can travel in either direction)
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The first observation is that if we need to cross exactly k highways and can visit each city at most once, then we can visit at most k + 1 cities (since each highway connects two cities). With n cities total, the maximum number of highways we can cross is n - 1. So if k >= n, it's impossible to meet the requirements.

The second key observation is that n is at most 15, which is a small number. This suggests we can use bitmask dynamic programming to track which cities we've visited. We can represent the set of visited cities as a binary number where bit i is 1 if city i has been visited.

The core insight is that to find the maximum cost path, we need to know:

  1. Which cities we've visited so far (represented by a bitmask)
  2. Which city we're currently at (since we need to know what highways are available from here)

This leads us to define f[mask][j] as the maximum cost to reach city j when we've visited exactly the cities represented by mask.

Starting from any city (with cost 0), we can build up paths by:

  • Looking at each possible set of visited cities (mask)
  • For each city j in that set, considering how we could have arrived there
  • We could have come from any neighboring city h that's also in the visited set
  • The cost would be the maximum cost to reach h with j removed from the visited set, plus the toll from h to j

By tracking the number of 1s in our bitmask (which equals the number of visited cities), we can identify when we've visited exactly k + 1 cities (meaning we've crossed exactly k highways). Among all such valid states, we take the maximum cost.

The beauty of this approach is that it systematically explores all possible paths while avoiding revisiting cities, and the small value of n makes the 2^n state space manageable.

Learn more about Graph, Dynamic Programming and Bitmask patterns.

Solution Approach

We implement the solution using state compression dynamic programming with the following steps:

1. Initial Check: First, we check if k >= n. If true, return -1 immediately since we can cross at most n - 1 highways when visiting each city at most once.

2. Build the Graph: We create an adjacency list g using a defaultdict where g[a] stores all neighbors of city a along with their toll costs. For each highway [a, b, cost], we add (b, cost) to g[a] and (a, cost) to g[b] since highways are bidirectional.

3. Initialize DP Table: We create a 2D array f where f[i][j] represents the maximum cost when:

  • i is a bitmask representing visited cities
  • j is the last city we're currently at

Initialize f[1 << i][i] = 0 for all cities i, meaning we start at city i with cost 0. All other states are initialized to negative infinity.

4. State Transitions: For each possible state i (bitmask of visited cities):

  • For each city j, check if it's in the visited set by testing i >> j & 1
  • If city j is visited, look at all its neighbors h with toll cost
  • If neighbor h is also in the visited set, we could have reached j from h
  • Update: f[i][j] = max(f[i][j], f[i ^ (1 << j)][h] + cost)
    • Here i ^ (1 << j) removes city j from the visited set, giving us the state before visiting j

5. Track the Answer: After updating f[i][j], check if the number of visited cities equals k + 1 using i.bit_count(). If so, update the answer: ans = max(ans, f[i][j]).

6. Return Result: Return the maximum cost found. If no valid path exists, ans remains -1.

The time complexity is O(2^n × n^2) where we iterate through all 2^n states, and for each state and city, we check all neighboring cities. The space complexity is O(2^n × n) for the DP table.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate the solution approach.

Example Input:

  • n = 4 cities (numbered 0, 1, 2, 3)
  • k = 2 (we need to cross exactly 2 highways)
  • highways = [[0,1,4], [1,2,3], [2,3,5], [0,3,2]]

This creates the following graph:

    0 ---4--- 1
    |         |
    2         3
    |         |
    3 ---5--- 2

Step 1: Initial Check

  • Is k >= n? No, 2 < 4, so we can proceed.

Step 2: Build Graph

g[0] = [(1,4), (3,2)]
g[1] = [(0,4), (2,3)]
g[2] = [(1,3), (3,5)]
g[3] = [(2,5), (0,2)]

Step 3: Initialize DP Table We initialize f[mask][city] where mask represents visited cities:

  • f[0001][0] = 0 (start at city 0, mask = 1)
  • f[0010][1] = 0 (start at city 1, mask = 2)
  • f[0100][2] = 0 (start at city 2, mask = 4)
  • f[1000][3] = 0 (start at city 3, mask = 8)

Step 4: State Transitions Let's trace one successful path: Start at city 2 → city 3 → city 0

First transition (city 2 to city 3):

  • Current state: mask = 0100 (only city 2 visited)
  • We want to visit city 3 next
  • New mask = 1100 (cities 2 and 3 visited)
  • From city 2, we can reach city 3 with toll 5
  • Update: f[1100][3] = f[0100][2] + 5 = 0 + 5 = 5

Second transition (city 3 to city 0):

  • Current state: mask = 1100 (cities 2 and 3 visited)
  • We want to visit city 0 next
  • New mask = 1101 (cities 0, 2, and 3 visited)
  • From city 3, we can reach city 0 with toll 2
  • Update: f[1101][0] = f[1100][3] + 2 = 5 + 2 = 7

Step 5: Track Answer When we have mask = 1101:

  • bit_count(1101) = 3 cities visited
  • Since we need k + 1 = 3 cities for exactly 2 highways, this is valid
  • Update answer: ans = max(-1, 7) = 7

Other Valid Paths: The algorithm explores all possibilities:

  • Path 0→1→2: cost = 4 + 3 = 7
  • Path 1→0→3: cost = 4 + 2 = 6
  • Path 3→2→1: cost = 5 + 3 = 8 ✓ (maximum)

Step 6: Return Result The maximum toll cost found is 8 (from path 3→2→1).

The algorithm systematically explores all possible combinations of visited cities and tracks the maximum cost for each valid configuration where exactly k highways are crossed.

Solution Implementation

1from collections import defaultdict
2from typing import List
3
4class Solution:
5    def maximumCost(self, n: int, highways: List[List[int]], k: int) -> int:
6        # If k >= n, it's impossible to visit k+1 distinct cities
7        if k >= n:
8            return -1
9      
10        # Build adjacency list representation of the graph
11        graph = defaultdict(list)
12        for city1, city2, cost in highways:
13            graph[city1].append((city2, cost))
14            graph[city2].append((city1, cost))
15      
16        # dp[mask][city] = maximum cost to reach 'city' with cities in 'mask' visited
17        # Initialize with negative infinity to represent unreachable states
18        dp = [[float('-inf')] * n for _ in range(1 << n)]
19      
20        # Base case: start from any city with cost 0
21        for city in range(n):
22            dp[1 << city][city] = 0
23      
24        max_cost = -1
25      
26        # Iterate through all possible subsets of cities (represented as bitmasks)
27        for mask in range(1 << n):
28            for current_city in range(n):
29                # Check if current_city is in the current subset
30                if (mask >> current_city) & 1:
31                    # Try to extend the path from neighboring cities
32                    for neighbor, edge_cost in graph[current_city]:
33                        # Check if neighbor is also in the current subset
34                        if (mask >> neighbor) & 1:
35                            # Update maximum cost to reach current_city
36                            # Previous state: mask without current_city, ending at neighbor
37                            prev_mask = mask ^ (1 << current_city)
38                            dp[mask][current_city] = max(
39                                dp[mask][current_city], 
40                                dp[prev_mask][neighbor] + edge_cost
41                            )
42              
43                # If we've visited exactly k+1 cities, update the answer
44                if bin(mask).count('1') == k + 1:
45                    max_cost = max(max_cost, dp[mask][current_city])
46      
47        return max_cost
48
1class Solution {
2    public int maximumCost(int n, int[][] highways, int k) {
3        // If k >= n, it's impossible to visit k+1 distinct cities
4        if (k >= n) {
5            return -1;
6        }
7      
8        // Build adjacency list representation of the graph
9        // graph[i] contains list of [neighbor, cost] pairs for city i
10        List<int[]>[] graph = new List[n];
11        Arrays.setAll(graph, index -> new ArrayList<>());
12      
13        // Populate the graph with bidirectional edges
14        for (int[] highway : highways) {
15            int cityA = highway[0];
16            int cityB = highway[1];
17            int cost = highway[2];
18            graph[cityA].add(new int[] {cityB, cost});
19            graph[cityB].add(new int[] {cityA, cost});
20        }
21      
22        // Dynamic programming table
23        // dp[mask][lastCity] = maximum cost to reach lastCity with cities in mask visited
24        // mask uses bit representation where bit i = 1 means city i is visited
25        int[][] dp = new int[1 << n][n];
26      
27        // Initialize all values to negative infinity (impossible state)
28        for (int[] row : dp) {
29            Arrays.fill(row, -(1 << 30));
30        }
31      
32        // Base case: starting from each city with only that city visited costs 0
33        for (int city = 0; city < n; ++city) {
34            dp[1 << city][city] = 0;
35        }
36      
37        int maxCost = -1;
38      
39        // Iterate through all possible subsets of cities (represented by bitmask)
40        for (int mask = 0; mask < (1 << n); ++mask) {
41            // Try ending at each city
42            for (int currentCity = 0; currentCity < n; ++currentCity) {
43                // Check if currentCity is in the current mask
44                if ((mask >> currentCity & 1) == 1) {
45                    // Try to reach currentCity from each of its neighbors
46                    for (int[] edge : graph[currentCity]) {
47                        int neighbor = edge[0];
48                        int edgeCost = edge[1];
49                      
50                        // Check if neighbor is also in the mask (valid previous city)
51                        if ((mask >> neighbor & 1) == 1) {
52                            // Update maximum cost to reach currentCity in this mask
53                            // by coming from neighbor (mask without currentCity) plus edge cost
54                            dp[mask][currentCity] = Math.max(
55                                dp[mask][currentCity], 
56                                dp[mask ^ (1 << currentCity)][neighbor] + edgeCost
57                            );
58                        }
59                    }
60                }
61              
62                // If we've visited exactly k+1 cities, update the answer
63                if (Integer.bitCount(mask) == k + 1) {
64                    maxCost = Math.max(maxCost, dp[mask][currentCity]);
65                }
66            }
67        }
68      
69        return maxCost;
70    }
71}
72
1class Solution {
2public:
3    int maximumCost(int n, vector<vector<int>>& highways, int k) {
4        // If k >= n, it's impossible to visit exactly k+1 cities
5        if (k >= n) {
6            return -1;
7        }
8      
9        // Build adjacency list representation of the graph
10        // graph[i] contains pairs of (neighbor_city, cost)
11        vector<vector<pair<int, int>>> graph(n);
12        for (auto& highway : highways) {
13            int city1 = highway[0];
14            int city2 = highway[1];
15            int cost = highway[2];
16            graph[city1].emplace_back(city2, cost);
17            graph[city2].emplace_back(city1, cost);
18        }
19      
20        // Dynamic programming table
21        // dp[mask][city] = maximum cost to reach 'city' visiting cities in 'mask'
22        // mask uses bit representation where bit i indicates if city i is visited
23        vector<vector<int>> dp(1 << n, vector<int>(n, INT_MIN / 2));
24      
25        // Initialize: each city can be the starting point with cost 0
26        for (int city = 0; city < n; ++city) {
27            dp[1 << city][city] = 0;
28        }
29      
30        int maxCost = -1;
31      
32        // Iterate through all possible subsets of cities (represented by bitmask)
33        for (int mask = 0; mask < (1 << n); ++mask) {
34            for (int currentCity = 0; currentCity < n; ++currentCity) {
35                // Check if current city is in the current subset
36                if ((mask >> currentCity) & 1) {
37                    // Try to reach currentCity from each of its neighbors
38                    for (auto& [neighborCity, edgeCost] : graph[currentCity]) {
39                        // Check if neighbor is also in the current subset
40                        if ((mask >> neighborCity) & 1) {
41                            // Update maximum cost to reach currentCity
42                            // by coming from neighborCity (excluding currentCity from mask)
43                            int prevMask = mask ^ (1 << currentCity);
44                            dp[mask][currentCity] = max(dp[mask][currentCity], 
45                                                       dp[prevMask][neighborCity] + edgeCost);
46                        }
47                    }
48                }
49              
50                // If we've visited exactly k+1 cities, update the answer
51                if (__builtin_popcount(mask) == k + 1) {
52                    maxCost = max(maxCost, dp[mask][currentCity]);
53                }
54            }
55        }
56      
57        return maxCost;
58    }
59};
60
1/**
2 * Finds the maximum cost of a path that visits exactly k+1 cities
3 * @param n - Total number of cities (0 to n-1)
4 * @param highways - Array of [cityA, cityB, cost] representing bidirectional roads
5 * @param k - Number of highways to traverse (visit k+1 cities total)
6 * @returns Maximum cost of valid path, or -1 if impossible
7 */
8function maximumCost(n: number, highways: number[][], k: number): number {
9    // If k >= n, impossible to visit k+1 distinct cities
10    if (k >= n) {
11        return -1;
12    }
13  
14    // Build adjacency list representation of the graph
15    // graph[i] contains array of [neighbor, cost] pairs for city i
16    const graph: [number, number][][] = Array.from({ length: n }, () => []);
17    for (const [cityA, cityB, cost] of highways) {
18        graph[cityA].push([cityB, cost]);
19        graph[cityB].push([cityA, cost]);
20    }
21  
22    // Dynamic programming table
23    // dp[mask][city] = maximum cost to reach 'city' visiting cities in 'mask'
24    // Initialize with very small values (negative infinity)
25    const dp: number[][] = Array(1 << n)
26        .fill(0)
27        .map(() => Array(n).fill(-(1 << 30)));
28  
29    // Base case: each city can be a starting point with cost 0
30    for (let city = 0; city < n; ++city) {
31        dp[1 << city][city] = 0;
32    }
33  
34    let maxCost = -1;
35  
36    // Iterate through all possible subsets of cities (bitmask)
37    for (let mask = 0; mask < (1 << n); ++mask) {
38        // Try extending path from each city in current subset
39        for (let currentCity = 0; currentCity < n; ++currentCity) {
40            // Check if current city is in the subset
41            if ((mask >> currentCity) & 1) {
42                // Try to reach current city from each neighbor
43                for (const [neighbor, edgeCost] of graph[currentCity]) {
44                    // Check if neighbor is also in the subset
45                    if ((mask >> neighbor) & 1) {
46                        // Update maximum cost to reach currentCity
47                        // Previous state: mask without currentCity, ending at neighbor
48                        const previousMask = mask ^ (1 << currentCity);
49                        dp[mask][currentCity] = Math.max(
50                            dp[mask][currentCity], 
51                            dp[previousMask][neighbor] + edgeCost
52                        );
53                    }
54                }
55            }
56          
57            // If we've visited exactly k+1 cities, update answer
58            if (bitCount(mask) === k + 1) {
59                maxCost = Math.max(maxCost, dp[mask][currentCity]);
60            }
61        }
62    }
63  
64    return maxCost;
65}
66
67/**
68 * Counts the number of set bits (1s) in binary representation of a number
69 * Uses bit manipulation tricks for efficient counting
70 * @param i - Input number
71 * @returns Number of set bits
72 */
73function bitCount(i: number): number {
74    // Count bits in groups of 2
75    i = i - ((i >>> 1) & 0x55555555);
76    // Count bits in groups of 4
77    i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
78    // Count bits in groups of 8
79    i = (i + (i >>> 4)) & 0x0f0f0f0f;
80    // Count bits in groups of 16
81    i = i + (i >>> 8);
82    // Count bits in groups of 32
83    i = i + (i >>> 16);
84    // Return final count (max 32 bits, so & 0x3f is sufficient)
85    return i & 0x3f;
86}
87

Time and Space Complexity

Time Complexity: O(2^n × n^2)

The algorithm uses dynamic programming with bitmask to track visited cities. The main computation involves:

  • Iterating through all possible subsets of cities: 2^n iterations (outer loop over i)
  • For each subset, iterating through all cities: n iterations (loop over j)
  • For each city in the subset, iterating through its neighbors: up to n-1 neighbors in the worst case (loop over g[j])

Therefore, the total time complexity is O(2^n × n × n) = O(2^n × n^2).

Space Complexity: O(2^n × n)

The space is dominated by:

  • The DP table f: a 2D array with dimensions 2^n × n, storing the maximum cost for each combination of visited cities (represented by bitmask) and current city
  • The adjacency list g: stores all edges, which takes O(E) space where E is the number of highways, but this is bounded by O(n^2) in the worst case

Since the DP table dominates the space usage, the overall space complexity is O(2^n × n).

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

Common Pitfalls

1. Incorrect State Transition Logic

A critical pitfall in this solution is the state transition logic when updating the DP table. The current implementation has a subtle bug in how it processes state transitions.

The Problem: When iterating through states with for mask in range(1 << n), we're processing masks in increasing order. However, when we update dp[mask][current_city] based on dp[prev_mask][neighbor], we're trying to use a state (prev_mask) that hasn't been fully computed yet if prev_mask < mask.

Example of the Issue:

  • Suppose mask = 0b111 (cities 0, 1, 2 visited) and current_city = 2
  • We want to update based on prev_mask = 0b011 (cities 0, 1 visited)
  • But when we're at mask = 0b111, we haven't finished computing all possible ways to reach states with mask = 0b011

The Solution: Process states by the number of cities visited (in increasing order) to ensure all smaller states are fully computed before using them:

def maximumCost(self, n: int, highways: List[List[int]], k: int) -> int:
    if k >= n:
        return -1
  
    graph = defaultdict(list)
    for city1, city2, cost in highways:
        graph[city1].append((city2, cost))
        graph[city2].append((city1, cost))
  
    dp = [[float('-inf')] * n for _ in range(1 << n)]
  
    # Base case
    for city in range(n):
        dp[1 << city][city] = 0
  
    max_cost = -1
  
    # Process states by number of cities visited
    for num_cities in range(1, k + 2):  # Need k+1 cities for k highways
        for mask in range(1 << n):
            if bin(mask).count('1') != num_cities:
                continue
              
            for current_city in range(n):
                if not ((mask >> current_city) & 1):
                    continue
              
                # Try to arrive at current_city from any neighbor
                prev_mask = mask ^ (1 << current_city)
                for neighbor, edge_cost in graph[current_city]:
                    if (prev_mask >> neighbor) & 1:
                        dp[mask][current_city] = max(
                            dp[mask][current_city],
                            dp[prev_mask][neighbor] + edge_cost
                        )
              
                # Check if we have exactly k+1 cities
                if num_cities == k + 1:
                    max_cost = max(max_cost, dp[mask][current_city])
  
    return max_cost

2. Edge Case: No Valid Path Exists

Another pitfall is not properly handling the case where no valid path with exactly k highways exists, even when k < n.

Example Scenario:

  • n = 4 cities, k = 2 (need exactly 2 highways)
  • highways = [[0, 1, 10], [2, 3, 20]]
  • The graph has two disconnected components, so you can't traverse 2 highways in a single path

The current solution handles this correctly by initializing max_cost = -1 and only updating it when valid paths are found. However, developers might accidentally initialize it to 0, which would incorrectly return 0 instead of -1 for impossible cases.

Best Practice: Always initialize the result variable to -1 (or the specified "impossible" value) and only update it when valid solutions are found.

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

Which two pointer techniques do you use to check if a string is a palindrome?


Recommended Readings

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

Load More