1595. Minimum Cost to Connect Two Groups of Points


Problem Description

In this problem, we have two distinct groups of points, with the first group containing size1 points and the second group size2 points, and it's given that size1 >= size2. There is a matrix provided where cost[i][j] represents the cost to connect point i from the first group to point j in the second group.

The objective is to connect every point in both groups such that every point in the first group is connected to one or more points in the second group, and vice versa, with the aim to minimize the total cost of all the connections.

To solve this, we are seeking the minimum total cost to connect both groups following the rule that connections can be between any point in the first group to any point in the second group.

Intuition

The intuition behind the solution involves dynamic programming with bitmasking to efficiently compute the minimum cost while keeping track of the connections that have been made.

Given that the first group has at least as many points as the second, we iterate over each point in the first group and for each, we consider different combinations of connections with the points in the second group. For each point in the first group, we utilize a bitmask to represent the connection state of the second group's points (i.e., whether they are connected to the first group or not).

The 'f' array in the code is initialized with infinity values and represents the minimum cost to reach a given state of connections, where the state is encoded as a bitmask. The array 'g' is a copy of 'f' which is used to keep track of the costs of the next state we're computing.

The function iterates over each point in the first group. For each of these points (indexed as i), we explore connecting it to every point k in the second group, updating the minimum cost to achieve different connection states as represented by the bitmask j.

The essence of the dynamic programming lies in the fact that for each possible connection state 'j', where we only consider second group's points that are already connected (encoded by j), we calculate the minimum cost required to connect the current point 'i' to any of the second group's points that the current state 'j' indicates as connected.

The cost is updated based on the previously known minimum costs for other states ('f[j]'), and we consider connecting to a new point in the second group. This keeps track of the minimum cost to maintain a connection from the second group to the first (so that no point is left unconnected).

After iterating over all points in both groups, 'f[-1]' will hold the minimum cost to connect all points in the first group to any point in the second group, having explored all possible connection states.

Learn more about Dynamic Programming and Bitmask patterns.

Solution Approach

To implement the solution, the algorithm leverages dynamic programming and bitwise operations to efficiently track and compute the minimum cost of connecting the groups.

The data structure used to keep track of the state of the second group's connection status is an array f of size 2^n (where n is the number of points in the second group). The reason for using 2^n is to represent all possible combinations of connections between two groups using bit masks. Each bit in the mask can denote whether a respective point in the second group is connected (1 meaning connected and 0 meaning not connected).

The steps followed in the algorithm are:

  1. Initialize f with inf (infinity) values, except f[0] which is set to 0. Here, f is used to store the minimum cost for each state of connections. f[0] = 0 because initially, no connections are made, and thus the cost is zero.

  2. Make a copy of f as g that will be used to calculate the costs for the next iteration without changing the existing values used as part of the calculation.

  3. Iterate through each point i in the first group (from 1 to m), and for each, iterate over all possible states j (which is represented by the bitmasks).

  4. Inside the nested loop, the algorithm iterates across each point k in the second group and each state of connections to calculate the minimum cost.

  5. When considering a connection from point i in the first group to point k in the second group, the code checks the bits of j to determine if point k is already connected; if not, continue to the next possible point.

  6. The minimum cost to add this connection, given the current state j, is computed by c + min(f[j], f[j ^ (1 << k)]), where c is the cost of connecting point i to point k, f[j] is the current minimum cost for the state j, and f[j ^ (1 << k)] is the cost for the state we'd have if point k were not connected (achieved by XOR'ing j with a bitmask with bit k set).

  7. Update the temporary g[j] with the calculated cost for this connection, but choose the minimum between the newly calculated cost x and the existing g[j] to ensure we're keeping the lowest cost as we explore all combinations for this state.

  8. After considering all points in the first group and their connections to the second group, copy the temporary cost array g to f to be used for the next iteration.

  9. Once all points are processed, the last element of f (i.e. f[-1]) will contain the minimum total cost to connect every point in the first group with the points in the second group considering that f[-1] represents the state where all points in the second group are connected.

The algorithm makes sure to find an optimum way to make these connections by considering all possible states of the second group's connectivity for each point in the first group.

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

How would you design a stack which has a function min that returns the minimum element in the stack, in addition to push and pop? All push, pop, min should have running time O(1).

Example Walkthrough

Let's walk through a small example to illustrate the solution approach. For simplicity, let's say we have the first group of points with size1 = 3 and the second group with size2 = 2. The cost matrix is as follows:

1cost = [
2    [1, 2],
3    [2, 3],
4    [3, 1]
5]

Where cost[i][j] is the cost of connecting point i from the first group to point j in the second group.

Following the solution approach:

  1. We initialize f with inf values such that f = [inf, inf, inf, inf], but set f[0] = 0.

  2. We create a copy of f and call it g. At the start, g is the same as f: g = [inf, inf, inf, inf], but with g[0] = 0.

  3. We start by iterating over points in the first group. For the first point i = 0, we consider each possible second group connection state represented by j. We look at all possible bitmasks from 0 to 3 (0b00 to 0b11 in binary, because size2 = 2).

  4. While at point i = 0, for each state, we examine potential connections to each point k in the second group. Point k ranges from 0 to 1.

  5. If state j indicates point k is not already connected, we calculate the cost to connect and update g[j].

  6. For example, group 1 point 0 connecting to group 2 point 0 is considered. The bitmask j is 0 (no connections yet), so we calculate a new cost c = 1 (from cost[0][0]). The new state after connection is j ^ (1 << 0) = 1 (binary 0b01), and we update g[1] = min(inf, 0 + 1) = 1.

  7. We repeat this for all connections from group 1 point 0. If we connect to the second point in group 2, the new state would be j ^ (1 << 1) = 2 (binary 0b10), and we update g[2] = min(inf, 0 + 2) = 2.

  8. After processing all connections from the first point in group 1, we copy the costs from g to f, making f = [0, 1, 2, inf].

  9. We now repeat the steps for the next point in the first group (point i = 1). We consider the same possible states and update g accordingly.

  10. After processing all points in the first group, each state j of the second group's points indicates what the minimum cost is to reach that state.

  11. The final minimum cost to connect all points with this example would be found in f[-1].

For this simple example, the final state of f after processing all points could be something like [0, 1, 2, 4], indicating that the minimum cost to connect every point in the first group with every point in the second group is 4. This represents the state where both points in the second group are connected to at least one point in the first group.

Solution Implementation

1class Solution:
2    def connectTwoGroups(self, cost: List[List[int]]) -> int:
3        # Get the size of group A and group B
4        size_group_a, size_group_b = len(cost), len(cost[0])
5
6        # Initialize the dp array f with infinite value for set states
7        dp = [float('inf')] * (1 << size_group_b)
8        # Initially, the cost to connect no elements from group B is 0
9        dp[0] = 0
10
11        # Initialize a temporary array 'next_dp' the same as 'dp' to store next state values
12        next_dp = dp[:]
13
14        # Loop for each element in group A
15        for i in range(1, size_group_a + 1):
16            # Update states for the new element of group A
17            for state in range(1 << size_group_b):
18                next_dp[state] = float('inf')
19              
20                # Try to connect the i-th element of group A with each element of group B
21                for j in range(size_group_b):
22                    # If the j-th element of group B is not in the current state, skip
23                    if (state >> j & 1) == 0:
24                        continue
25                  
26                    # Calculate the cost to connect the i-th element of group A with the j-th element of group B
27                    c = cost[i - 1][j]
28                  
29                    # Determine the new state by removing the j-th element from the current state
30                    new_state = state ^ (1 << j)
31                  
32                    # Compute the total cost for the new state
33                    # and compare with previous costs to find the minimum
34                    total_cost = min(next_dp[new_state], dp[state], dp[new_state]) + c
35                  
36                    # Update the cost for the current state
37                    next_dp[state] = min(next_dp[state], total_cost)
38          
39            # Move to the next state
40            dp = next_dp[:]
41
42        # Return the minimum total cost to connect all elements from both groups
43        return dp[-1]
44
1class Solution {
2    public int connectTwoGroups(List<List<Integer>> cost) {
3        int numRows = cost.size();                // Number of rows in the cost matrix
4        int numCols = cost.get(0).size();        // Number of columns in the cost matrix
5        final int INFINITY = 1 << 30;            // Define an "infinity" value to be used for comparison
6      
7        int[] dp = new int[1 << numCols];        // Dynamic programming (dp) array for the previous round
8        Arrays.fill(dp, INFINITY);               // Initialize all values to infinity
9        dp[0] = 0;                               // Starting with no connections, the cost is zero
10        int[] dpNext = dp.clone();               // Clone the dp array for processing the next row
11      
12        // Outer loop processes each row of the cost matrix
13        for (int i = 1; i <= numRows; ++i) {
14            // Initialize dpNext for each row calculation
15            Arrays.fill(dpNext, INFINITY);
16          
17            // Go through all the possible states (combinations of connections) in dpNext
18            for (int state = 0; state < (1 << numCols); ++state) {
19
20                // Check each column in the current row
21                for (int col = 0; col < numCols; ++col) {
22                    // If the current state includes a connection to the col-th column
23                    if ((state >> col & 1) == 1) {
24                        int connectionCost = cost.get(i - 1).get(col);                        // Get the cost for connecting the current row with col-th column
25                      
26                        // Update dpNext[state] with the minimum of three possible scenarios:
27                        // 1. Connect the current row's col-th column with previous state excluding col-th column and add the cost
28                        dpNext[state] = Math.min(dpNext[state], dpNext[state ^ (1 << col)] + connectionCost);
29                        // 2. Connect the current row's col-th column with the previous row's state and add the cost
30                        dpNext[state] = Math.min(dpNext[state], dp[state] + connectionCost);
31                        // 3. Connect the current row's col-th column with the previous row's state excluding col-th column and add the cost
32                        dpNext[state] = Math.min(dpNext[state], dp[state ^ (1 << col)] + connectionCost);
33                    }
34                }
35            }
36
37            // Copy the dpNext into dp for the next round/row
38            System.arraycopy(dpNext, 0, dp, 0, 1 << numCols);
39        }
40
41        // Final answer is at dp[(1 << numCols) - 1], which represents all columns connected
42        return dp[(1 << numCols) - 1];
43    }
44}
45
1class Solution {
2public:
3    int connectTwoGroups(vector<vector<int>>& costs) {
4        int numLeft = costs.size(); // Number of nodes on the left
5        int numRight = costs[0].size(); // Number of nodes on the right
6        const int INF = 1 << 30; // Define an "infinity" value for comparison purposes
7        vector<int> currDp(1 << numRight, INF); // Current DP state, initially set to "infinity"
8        currDp[0] = 0; // The cost to connect an empty subset is 0
9        vector<int> nextDp = currDp; // The next DP state to be calculated
10
11        // Loop through each node on the left
12        for (int i = 1; i <= numLeft; ++i) {
13            // Calculate next DP state for every subset of right nodes
14            for (int mask = 0; mask < (1 << numRight); ++mask) {
15                nextDp[mask] = INF; // Reset the next DP state to "infinity"
16                // Iterate through each bit/node in the mask
17                for (int k = 0; k < numRight; ++k) {
18                    // Check if the k-th node on the right is included in the subset
19                    if (mask & (1 << k)) {
20                        // The cost to connect the i-1-th node on the left and k-th node on the right
21                        int connectionCost = costs[i - 1][k];
22
23                        // The new cost is the minimum of three potential previous states plus the new connectionCost
24                        int newCost = min({nextDp[mask ^ (1 << k)], currDp[mask], currDp[mask ^ (1 << k)]}) + connectionCost;
25
26                        // Update the next DP state with the new cost if it's smaller than the current cost
27                        nextDp[mask] = min(nextDp[mask], newCost);
28                    }
29                }
30            }
31            // Swap the current DP state with the next one for the subsequent iteration
32            currDp.swap(nextDp);
33        }
34        // Return the minimum cost to connect all nodes on the left to some subset of nodes on the right
35        return currDp[(1 << numRight) - 1];
36    }
37};
38
1function connectTwoGroups(cost: number[][]): number {
2    const groupSizeA = cost.length; // Size of the first group
3    const groupSizeB = cost[0].length; // Size of the second group
4    const infinity = 1 << 30; // A large number to represent infinity
5    const dpCurrent = new Array(1 << groupSizeB).fill(infinity); // DP array to keep track of minimum costs for current iteration
6    dpCurrent[0] = 0; // Base case: cost for connecting no elements from Group B
7    const dpNext = new Array(1 << groupSizeB).fill(0); // DP array for the next iteration
8  
9    // Loop through each element in Group A
10    for (let i = 1; i <= groupSizeA; ++i) {
11        // Loop through all subsets of Group B
12        for (let subsetMask = 0; subsetMask < (1 << groupSizeB); ++subsetMask) {
13            dpNext[subsetMask] = infinity; // Reset the next DP array entry to infinity
14          
15            // For each element in Group B, try to make connections
16            for (let j = 0; j < groupSizeB; ++j) {
17                // Check if element j is in the current subset mask
18                if (((subsetMask >> j) & 1) === 1) {
19                    const currentCost = cost[i - 1][j];
20                  
21                    // Update dpNext for removing element j from the subset
22                    dpNext[subsetMask] = Math.min(dpNext[subsetMask], dpNext[subsetMask ^ (1 << j)] + currentCost);
23                  
24                    // Update dpNext based on the current dp (previous iteration) keeping element j
25                    dpNext[subsetMask] = Math.min(dpNext[subsetMask], dpCurrent[subsetMask] + currentCost);
26                  
27                    // Update dpNext based on the current dp (previous iteration) removing element j
28                    dpNext[subsetMask] = Math.min(dpNext[subsetMask], dpCurrent[subsetMask ^ (1 << j)] + currentCost);
29                }
30            }
31        }
32      
33        // Copy the dpNext values to dpCurrent to be used in the next iteration
34        dpCurrent.splice(0, dpCurrent.length, ...dpNext);
35    }
36  
37    // Return the minimum cost for connecting all elements in both groups
38    return dpCurrent[(1 << groupSizeB) - 1];
39}
40

Time and Space Complexity

The given code is an implementation of a dynamic programming algorithm designed to solve a problem that seems to involve connecting two groups with certain costs. It uses bit masking to represent subsets of connections between the groups.

Time Complexity:

To calculate the time complexity, let's break down the nested loops in the code:

  • There is an outer loop that runs m times, where m is the length of the first dimension of cost.
  • Inside this loop, there is another loop that runs for all subsets of the second group, which is 2^n times, where n is the length of the second dimension of cost.
  • Finally, within the second loop, there is an innermost loop that iterates over all n elements of the second group.

The innermost loop checks and updates the best cost to connect elements of the first group to a subset of elements of the second group.

Therefore, the time complexity is O(m * n * 2^n), as for each of the m elements in the first group and for each of the 2^n subsets of the second group, we perform n checks and updates.

Space Complexity:

As for the space complexity, there are two main data structures to consider: the f and g arrays. Each of these arrays has a length of 2^n, as they store subsets of the second group.

Hence, the space complexity is O(2^n), as this is the largest amount of space used by the algorithm at any point in time.

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


Fast Track Your Learning with Our Quick Skills Quiz:

What's the output of running the following function using input 56?

1KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13    def dfs(path, res):
14        if len(path) == len(digits):
15            res.append(''.join(path))
16            return
17
18        next_number = digits[len(path)]
19        for letter in KEYBOARD[next_number]:
20            path.append(letter)
21            dfs(path, res)
22            path.pop()
23
24    res = []
25    dfs([], res)
26    return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2    '2', "abc".toCharArray(),
3    '3', "def".toCharArray(),
4    '4', "ghi".toCharArray(),
5    '5', "jkl".toCharArray(),
6    '6', "mno".toCharArray(),
7    '7', "pqrs".toCharArray(),
8    '8', "tuv".toCharArray(),
9    '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13    List<String> res = new ArrayList<>();
14    dfs(new StringBuilder(), res, digits.toCharArray());
15    return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19    if (path.length() == digits.length) {
20        res.add(path.toString());
21        return;
22    }
23    char next_digit = digits[path.length()];
24    for (char letter : KEYBOARD.get(next_digit)) {
25        path.append(letter);
26        dfs(path, res, digits);
27        path.deleteCharAt(path.length() - 1);
28    }
29}
30
1const KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13    let res = [];
14    dfs(digits, [], res);
15    return res;
16}
17
18function dfs(digits, path, res) {
19    if (path.length === digits.length) {
20        res.push(path.join(''));
21        return;
22    }
23    let next_number = digits.charAt(path.length);
24    for (let letter of KEYBOARD[next_number]) {
25        path.push(letter);
26        dfs(digits, path, res);
27        path.pop();
28    }
29}
30

Recommended Readings


Got a question? Ask the Monster 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.

Tired of the LeetCode Grind?

Our structured approach teaches you the patterns behind problems, so you can confidently solve any challenge. Get started now to land your dream tech job.

Get Started

🪄