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
exceptf[1 << i][i]
, which are set to0
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 cityh
toj
. If so, update the DP statef[i][j]
with the maximum cost by comparing the existing cost with the cost to reachh
plus the toll cost betweenh
andj
. This represents taking a highway fromh
toj
. - Keep track of the maximum cost
ans
when exactlyk+1
cities have been visited, which representsk
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.
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:
-
Initialize the graph
g
to store the connections and tolls between the cities. -
Prepare the dynamic programming table
f
such thatf[i][j]
will hold the maximum cost achieved when reaching cityj
after traveling through cities represented by the bitmaski
. 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. -
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). -
For each subset-city pair (
i
,j
), iterate through all connecting highways (neighborsh
) to see if it is possible to come to cityj
from cityh
. If so, take the maximum between the existing cost inf[i][j]
and the cost of reaching cityh
plus the toll cost betweenh
andj
. -
Use the
bit_count
method on the bitmaski
to check if exactlyk+1
cities have been included in the subset (which meansk
highways crossed). At this stage, update the answerans
with the maximum value found in the current subset for exactlyk
highways crossed. -
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.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample 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:
highway[0] = [0, 1, 4] // highway from city 0 to 1 with a toll cost of 4 highway[1] = [1, 2, 5] // highway from city 1 to 2 with a toll cost of 5 highway[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:
-
Initialize a graph
g
to represent the cities and highways. For our example,g
will look like this after initialization:g = { 0: [(1, 4), (2, 1)], 1: [(2, 5)], 2: [] }
-
Prepare the DP table
f
with1 << total_cities
rows (total_cities
being3
here) andtotal_cities
columns, initialized to-infinity
except forf[1 << i][i]
, which are set to0
. The table now looks like:f = [ [-inf, -inf, -inf], // bitmask 00 [0, -inf, -inf], // bitmask 01 [-inf, 0, -inf], // bitmask 10 [-inf, -inf, 0], // bitmask 11 ]
-
Iterate over subsets
i
(from1
to(1 << 3) - 1
) which are001
,010
,011
,100
,101
,110
, and111
as bitmasks, and for each subset, iterate over each cityj
(from0
to2
). -
For each subset-city pair (
i
,j
), iterate through all the highways from citiesh
toj
that are not already included ini
. Updatef[i][j]
accordingly. This is how things evolve:-
When
i = 001
(city0
) andj = 1
, we can go from0
to1
with a cost of4
. We updatef[001 | (1 << 1)][1] = max(f[001 | (1 << 1)][1], f[001][0] + 4)
which translates tof[011][1] = max(-inf, 0 + 4)
thusf[011][1] = 4
. -
When
i = 010
(city1
) andj = 2
, we can travel from1
to2
with a cost of5
. We updatef[010 | (1 << 2)][2] = max(f[010 | (1 << 2)][2], f[010][1] + 5)
which translates tof[110][2] = max(-inf, 0 + 5)
thusf[110][2] = 5
. -
When
i = 100
(city2
) no updates occur since there are no outgoing highways. -
Continue this process for other subsets.
-
-
After examining all routes and updating
f
, we look for the answer inf
wherebit_count(i) == k + 1
. In our case,k = 2
, so we are looking for 3 visited cities (k+1
). For every bitmaski
that ends on a cityj
, we check ifbit_count(i) == 3
. If it is, we considerf[i][j]
as a candidate forans
. -
For
i = 011
, we havef[011][1] = 4
and fori = 110
, we havef[110][2] = 5
. Sincebit_count(011) = 2
andbit_count(110) = 2
, neither fit our requirement of3
, so we skip them. Thus, we never updateans
from its initial value of-1
. -
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
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 through2^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 ofO(n * 2^n)
. - The graph
g
stores the edges between cities. In the worst case, it can store up ton * (n - 1) / 2
edges for a complete graph. However, the space used forg
is not dominant compared tof
.
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.
Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?
Recommended Readings
https algomonster s3 us east 2 amazonaws com cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Bitmask and Dynamic Programming Bit manipulation is a crucial aspect of computer programming and one of the most powerful tools for bit manipulation is bitmasks Let's first understand what a bit is A bit is a binary digit It's the smallest piece of data in a computer and can be
Want a Structured Path to Master System Design Too? Don’t Miss This!