2247. Maximum Cost of Trip With K Highways


Problem Description

The problem presents a scenario where we have n cities and a series of highways that connect certain pairs of these cities. Each highway has an associated toll cost. We are given a map of these connections in the form of a two-dimensional array highways, where each element is a triplet containing two cities and the cost to travel between them via the highway that connects them. Additionally, we have a specific requirement: to plan a trip that crosses exactly k highways, starting from any city but without visiting any city more than once during this trip. The goal is to calculate the maximum possible cost of such a trip, given these constraints. If no such trip exists (i.e., it's not possible to cross exactly k highways without revisiting cities), the function should return -1.

Intuition

The key to solving this problem is recognizing that it is a variation of the classic traveling salesman problem (TSP), which is a well-known problem in computer science. TSP is concerned with finding the shortest possible route that visits each city exactly once and returns to the origin city. In our case, however, we do not need to return to the starting city, and instead of minimizing cost, we are aiming to maximize it, and we only need to cross exactly k highways.

The solution uses a dynamic programming (DP) approach in which we keep track of the maximum cost for each subset of cities visited and ending in a particular city, considering exactly k highways. The DP state is represented by a two-dimensional array f, where f[i][j] keeps the maximum cost to reach city j after visiting a particular subset of cities represented by the bitmask i.

The logic includes these steps:

  • First, initialize all DP states to -infinity except f[1 << i][i], which are set to 0 since it costs nothing to start from any city (without having traveled any highway).
  • Go through all possible subsets of cities represented as bitmasks. For each subset and each city j on the current subset, check if there's a highway from any other city h to j. If so, update the DP state f[i][j] with the maximum cost by comparing the existing cost with the cost to reach h plus the toll cost between h and j. This represents taking a highway from h to j.
  • Keep track of the maximum cost ans when exactly k+1 cities have been visited, which represents k highways crossed.

With this DP approach, we consider all possible paths and each path's accumulated cost to achieve the maximum toll cost for k highway crossings without revisiting cities, which satisfies the constraints of the problem.

Learn more about Graph, Dynamic Programming and Bitmask patterns.

Not Sure What to Study? Take the 2-min Quiz to Find Your Missing Piece

Which algorithm should you use to find a node that is close to the root of the tree?

Solution Approach

The implementation utilizes several programming concepts and data structures such as a graph, dynamic programming, bitmasking, and recursion.

  • Dynamic Programming (DP): This approach helps us to solve complex problems by breaking them down into simpler subproblems. In this case, DP is used to keep track of the maximum cost for reaching each city through a specific subset of cities.

  • Bitmasking: This is a technique used to represent a subset of cities. Each bit in an integer value represents whether a particular city is included in the subset (1 means included, 0 means not included). This is a compact way to handle all combinations of cities.

  • Graph Representation: Each city and its connected highways are represented using a graph data structure. In this implementation, a dictionary of lists (defaultdict(list)) is used where each key corresponds to a city, and the value is a list of tuples representing the connected cities and the associated cost.

Here is how the solution translates these concepts into code:

  1. Initialize the graph g to store the connections and tolls between the cities.

  2. Prepare the dynamic programming table f such that f[i][j] will hold the maximum cost achieved when reaching city j after traveling through cities represented by the bitmask i. Initially, all values except the starting conditions (f[1 << i][i] = 0) are set to -infinity to indicate that those states have not been reached.

  3. Iterate over all possible subsets of cities (i is the loop variable used as a bitmask) and their respective cities (j represents each city in the subset).

  4. For each subset-city pair (i, j), iterate through all connecting highways (neighbors h) to see if it is possible to come to city j from city h. If so, take the maximum between the existing cost in f[i][j] and the cost of reaching city h plus the toll cost between h and j.

  5. Use the bit_count method on the bitmask i to check if exactly k+1 cities have been included in the subset (which means k highways crossed). At this stage, update the answer ans with the maximum value found in the current subset for exactly k highways crossed.

  6. After completing the iterations, return the maximum cost ans. If no valid trip meets the requirements, ans remains -1, which is returned as the result.

In summary, the key algorithmic steps in the solution involve initializing DP states, updating states based on the graph of cities and highways, and tracking the maximum cost for trips that span exactly k highways.

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

Given a sorted array of integers and an integer called target, find the element that equals to the target and return its index. Select the correct code that fills the ___ in the given code snippet.

1def binary_search(arr, target):
2    left, right = 0, len(arr) - 1
3    while left ___ right:
4        mid = (left + right) // 2
5        if arr[mid] == target:
6            return mid
7        if arr[mid] < target:
8            ___ = mid + 1
9        else:
10            ___ = mid - 1
11    return -1
12
1public static int binarySearch(int[] arr, int target) {
2    int left = 0;
3    int right = arr.length - 1;
4
5    while (left ___ right) {
6        int mid = left + (right - left) / 2;
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16
1function binarySearch(arr, target) {
2    let left = 0;
3    let right = arr.length - 1;
4
5    while (left ___ right) {
6        let mid = left + Math.trunc((right - left) / 2);
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16

Example Walkthrough

Let's consider a small example to illustrate the solution approach:

Suppose we have 3 cities and 3 highways between them with the following toll costs:

1highway[0] = [0, 1, 4] // highway from city 0 to 1 with a toll cost of 4
2highway[1] = [1, 2, 5] // highway from city 1 to 2 with a toll cost of 5
3highway[2] = [0, 2, 1] // highway from city 0 to 2 with a toll cost of 1

And we need to find the maximum possible toll cost for a trip that crosses exactly k = 2 highways.

Step-by-Step Solution:

  1. Initialize a graph g to represent the cities and highways. For our example, g will look like this after initialization:

    1g = {
    2    0: [(1, 4), (2, 1)],
    3    1: [(2, 5)],
    4    2: []
    5}
  2. Prepare the DP table f with 1 << total_cities rows (total_cities being 3 here) and total_cities columns, initialized to -infinity except for f[1 << i][i], which are set to 0. The table now looks like:

    1f = [
    2   [-inf,   -inf, -inf], // bitmask 00
    3   [0,       -inf, -inf], // bitmask 01
    4   [-inf,    0,    -inf], // bitmask 10
    5   [-inf,   -inf,    0], // bitmask 11
    6]
  3. Iterate over subsets i (from 1 to (1 << 3) - 1) which are 001, 010, 011, 100, 101, 110, and 111 as bitmasks, and for each subset, iterate over each city j (from 0 to 2).

  4. For each subset-city pair (i, j), iterate through all the highways from cities h to j that are not already included in i. Update f[i][j] accordingly. This is how things evolve:

    • When i = 001 (city 0) and j = 1, we can go from 0 to 1 with a cost of 4. We update f[001 | (1 << 1)][1] = max(f[001 | (1 << 1)][1], f[001][0] + 4) which translates to f[011][1] = max(-inf, 0 + 4) thus f[011][1] = 4.

    • When i = 010 (city 1) and j = 2, we can travel from 1 to 2 with a cost of 5. We update f[010 | (1 << 2)][2] = max(f[010 | (1 << 2)][2], f[010][1] + 5) which translates to f[110][2] = max(-inf, 0 + 5) thus f[110][2] = 5.

    • When i = 100 (city 2) no updates occur since there are no outgoing highways.

    • Continue this process for other subsets.

  5. After examining all routes and updating f, we look for the answer in f where bit_count(i) == k + 1. In our case, k = 2, so we are looking for 3 visited cities (k+1). For every bitmask i that ends on a city j, we check if bit_count(i) == 3. If it is, we consider f[i][j] as a candidate for ans.

  6. For i = 011, we have f[011][1] = 4 and for i = 110, we have f[110][2] = 5. Since bit_count(011) = 2 and bit_count(110) = 2, neither fit our requirement of 3, so we skip them. Thus, we never update ans from its initial value of -1.

  7. In this example, there's no way to travel across exactly 2 highways without visiting a city twice, so the answer remains -1.

Using this approach, the algorithm would evaluate all possible trips that can be made by crossing exactly k highways and thus determine the maximum toll cost for such a trip or return -1 if no such trip exists.

Solution Implementation

1from collections import defaultdict
2
3class Solution:
4    def maximumCost(self, n: int, highways: List[List[int]], k: int) -> int:
5        # If number of planned cities (k) is greater than or equal to the total number of cities (n), it's not possible
6        # to construct k highways, hence return -1.
7        if k >= n:
8            return -1
9      
10        # Initialize the graph as a default dictionary with lists.
11        graph = defaultdict(list)
12        # Construct the graph using the input highways.
13        for u, v, cost in highways:
14            graph[u].append((v, cost))
15            graph[v].append((u, cost))
16      
17        # Initialize the DP table with -inf (Negative Infinity).
18        # f[mask][i] represents the maximum cost of visiting cities represented by mask, ending in city i.
19        dp_table = [[float('-inf')] * n for _ in range(1 << n)]
20        for i in range(n):
21            dp_table[1 << i][i] = 0  # Base case: the cost of a city to itself is 0.
22      
23        # Initialize the variable to store the answer.
24        answer = -1
25      
26        # Iterate over all possible combinations of cities.
27        for mask in range(1 << n):
28            # Check each city for the current combination.
29            for j in range(n):
30                # If the j-th city is part of the current combination (mask).
31                if mask & (1 << j):
32                    # To update dp[mask][j], we should have come from a previous city (h).
33                    for h, cost in graph[j]:
34                        if mask & (1 << h):
35                            # Update the DP table with the max cost of getting to city j from city h.
36                            dp_table[mask][j] = max(dp_table[mask][j], dp_table[mask ^ (1 << j)][h] + cost)
37              
38                # If the current combination includes exactly k + 1 cities.
39                if bin(mask).count('1') == k + 1:
40                    # Update the answer with the maximum cost of this combination.
41                    answer = max(answer, dp_table[mask][j])
42      
43        # Return the maximum cost after checking all combinations.
44        return answer
45
1class Solution {
2    // Maximum cost calculation method, for a given number of cities (n), a list of highways with costs,
3    // and the number of connecting highways to consider (k).
4    public int maximumCost(int numCities, int[][] highways, int maxHighways) {
5        // If maxHighways >= numCities, a valid path that uses maxHighways does not exist
6        if (maxHighways >= numCities) {
7            return -1;
8        }
9      
10        // Graph representation: an array of lists where each list contains pairs [city, cost]
11        List<int[]>[] graph = new List[numCities];
12        // Initialize the adjacency lists for each city
13        Arrays.setAll(graph, x -> new ArrayList<>());
14      
15        // Populate the graph with the given highways data
16        for (int[] highway : highways) {
17            int cityA = highway[0], cityB = highway[1], cost = highway[2];
18            graph[cityA].add(new int[] {cityB, cost});
19            graph[cityB].add(new int[] {cityA, cost});
20        }
21      
22        // Dynamic programming matrix to keep track of max costs f[state][city]
23        int[][] dp = new int[1 << numCities][numCities];
24      
25        // Initializing the dp matrix with minimum values
26        for (int[] row : dp) {
27            Arrays.fill(row, Integer.MIN_VALUE / 2); // Using MIN_VALUE/2 to prevent overflow when adding
28        }
29      
30        // Base cases where each city is the only one in the set, cost is 0
31        for (int i = 0; i < numCities; ++i) {
32            dp[1 << i][i] = 0;
33        }
34      
35        // Variable to keep track of the maximum cost found
36        int maxCost = -1;
37      
38        // Iterate over all sets of cities (states)
39        for (int state = 0; state < (1 << numCities); ++state) {
40            // Iterate over all cities
41            for (int currentCity = 0; currentCity < numCities; ++currentCity) {
42                // Check if the current city is included in the current state
43                if ((state & (1 << currentCity)) != 0) {
44                    // Explore all the highways connected to the current city 
45                    for (int[] edge : graph[currentCity]) {
46                        int nextCity = edge[0], nextCost = edge[1];
47                        // Check if the next city is in the current state
48                        if ((state & (1 << nextCity)) != 0) {
49                            // Update the maximum cost for the current state and city
50                            dp[state][currentCity] = Math.max(
51                                dp[state][currentCity],
52                                dp[state ^ (1 << currentCity)][nextCity] + nextCost
53                            );
54                        }
55                    }
56                }
57                // Once we have a state with exactly k + 1 cities, update the maximum cost
58                if (Integer.bitCount(state) == maxHighways + 1) {
59                    maxCost = Math.max(maxCost, dp[state][currentCity]);
60                }
61            }
62        }
63        // Return the maximum cost found, or -1 if no path with exact k highways is found
64        return maxCost;
65    }
66}
67
1class Solution {
2public:
3    // Function to find the maximum cost of highways to visit k+1 towns
4    int maximumCost(int n, vector<vector<int>>& highways, int k) {
5        // If k is greater or equal to the number of towns, return -1
6        // as the problem is not solvable in this situation
7        if (k >= n) {
8            return -1;
9        }
10     
11        // Create an adjacency list to represent the graph
12        vector<pair<int, int>> graph[n];
13        // Populate the graph with the given highways and their costs
14        for (auto& highway : highways) {
15            int town1 = highway[0], town2 = highway[1], cost = highway[2];
16            graph[town1].emplace_back(town2, cost);
17            graph[town2].emplace_back(town1, cost);
18        }
19      
20        // Initialize the dp table with negative infinity (effectively -infinity)
21        int dp[1 << n][n];
22        memset(dp, -0x3f, sizeof(dp));
23        // Set the starting cost to 0 for visiting each town individually
24        for (int i = 0; i < n; ++i) {
25            dp[1 << i][i] = 0;
26        }
27      
28        // Variable to store the maximum cost found
29        int maxCost = -1;
30        // Iterate over all combinations of towns
31        for (int i = 0; i < 1 << n; ++i) {
32            // Iterate over each town
33            for (int j = 0; j < n; ++j) {
34                // If the current combination includes the town j
35                if (i >> j & 1) {
36                    // Iterate over neighbors of town j
37                    for (auto& [neighboringTown, cost] : graph[j]) {
38                        // If the current combination includes the neighbor
39                        if (i >> neighboringTown & 1) {
40                            // Update the dp cost for combination i ending in town j
41                            dp[i][j] = max(dp[i][j], dp[i ^ (1 << j)][neighboringTown] + cost);
42                        }
43                    }
44                }
45                // If the current combination has exactly k+1 towns
46                if (__builtin_popcount(i) == k + 1) {
47                    // Update the maximum cost if a new maximum is found
48                    maxCost = max(maxCost, dp[i][j]);
49                }
50            }
51        }
52        // Return the maximum cost after processing all combinations
53        return maxCost;
54    }
55};
56
1function maximumCost(totalCities: number, highways: number[][], targetHighwayCount: number): number {
2    // If the target number of highways is greater than or equal to the total number of cities, return -1.
3    if (targetHighwayCount >= totalCities) {
4        return -1;
5    }
6  
7    // Graph representation where `graph` stores adjacency lists of [destination, cost] for each city.
8    const graph: [number, number][][] = Array.from({ length: totalCities }, () => []);
9    for (const [city1, city2, cost] of highways) {
10        graph[city1].push([city2, cost]);
11        graph[city2].push([city1, cost]);
12    }
13  
14    // Initialize the DP table with negative infinity values.
15    const dp: number[][] = Array(1 << totalCities)
16        .fill(0)
17        .map(() => Array(totalCities).fill(-(1 << 30)));
18  
19    // Set the starting cost of each city to 0 in the DP table.
20    for (let i = 0; i < totalCities; ++i) {
21        dp[1 << i][i] = 0;
22    }
23  
24    // Variable to track the maximum cost.
25    let maxCost = -1;
26  
27    // Evaluate all combinations of cities.
28    for (let mask = 0; mask < 1 << totalCities; ++mask) {
29        for (let currentCity = 0; currentCity < totalCities; ++currentCity) {
30            // If the current city is included in the mask.
31            if ((mask >> currentCity) & 1) {
32                for (const [nextCity, cost] of graph[currentCity]) {
33                    // If the next city is included in the mask.
34                    if ((mask >> nextCity) & 1) {
35                        const prevState = mask ^ (1 << currentCity);
36                        dp[mask][currentCity] = Math.max(dp[mask][currentCity], dp[prevState][nextCity] + cost);
37                    }
38                }
39            }
40            // If the bit count of the combination is equal to targetHighwayCount + 1.
41            if (bitCount(mask) === targetHighwayCount + 1) {
42                maxCost = Math.max(maxCost, dp[mask][currentCity]);
43            }
44        }
45    }
46  
47    // Return the maximum cost found; -1 if no such combination exists.
48    return maxCost;
49}
50
51// Helper function to count the number of bits set to 1 in an integer (Hamming Weight or Population Count).
52function bitCount(value: number): number {
53    value = value - ((value >>> 1) & 0x55555555);
54    value = (value & 0x33333333) + ((value >>> 2) & 0x33333333);
55    value = (value + (value >>> 4)) & 0x0f0f0f0f;
56    value = value + (value >>> 8);
57    value = value + (value >>> 16);
58    return value & 0x3f; // Return the count (last 6 bits are sufficient since the count does not exceed the number of cities).
59}
60
Not Sure What to Study? Take the 2-min Quiz

Which data structure is used to implement recursion?

Time and Space Complexity

The given Python code is an implementation of the bitmask dynamic programming algorithm for the problem of finding the maximum total cost of servicing k highways among n different cities. Here's an analysis of the time complexity and space complexity of the code:

Time Complexity

The time complexity is determined by the nested loops in the code and the operations within those loops.

  • The outer loop runs for every subset of the n cities, hence it goes through 2^n iterations as it considers every possible combination of cities.
  • Inside the outer loop, there is another loop that iterates over n cities.
  • Inside the second loop, there is an inner loop traversing the adjacency list of each city. In the worst case, this could be n-1 edges for a complete graph.

Putting it together, the worst-case time complexity is O(n * 2^n * n) = O(n^2 * 2^n).

Space Complexity

The space complexity is determined by the space used to store all the information needed during computation.

  • The 2D array f has a size of (2^n) * n, used to store the maximum costs of traveling through subsets of cities, resulting in a space complexity of O(n * 2^n).
  • The graph g stores the edges between cities. In the worst case, it can store up to n * (n - 1) / 2 edges for a complete graph. However, the space used for g is not dominant compared to f.

Therefore, the overall space complexity is O(n * 2^n) as it is the largest memory allocation in the code.

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

Fast Track Your Learning with Our Quick Skills Quiz:

Which of the following shows the order of node visit in a Breadth-first Search?


Recommended Readings


Got a question? Ask the Teaching Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.

←
↑TA đŸ‘šâ€đŸ«