Facebook Pixel

1775. Equal Sum Arrays With Minimum Number of Operations

MediumGreedyArrayHash TableCounting
Leetcode Link

Problem Description

You have two arrays nums1 and nums2 containing integers between 1 and 6 (inclusive). The arrays can have different lengths.

In each operation, you can pick any number from either array and change it to any value between 1 and 6.

Your goal is to make the sum of all numbers in nums1 equal to the sum of all numbers in nums2 using the minimum number of operations.

Return the minimum number of operations needed to achieve equal sums. If it's impossible to make the sums equal, return -1.

Example scenarios:

  • If nums1 = [1,2,3,4,5,6] and nums2 = [1,1,2,2,2,2], you need to find how many numbers to change to make sum(nums1) = 21 equal to sum(nums2) = 10.
  • To maximize the impact of each operation, you'd want to decrease large numbers in the array with the larger sum (change 6β†’1, 5β†’1, etc.) or increase small numbers in the array with the smaller sum (change 1β†’6, 2β†’6, etc.).

Key insight: Each operation can change the total sum difference by at most 5 (changing a 1 to 6 or a 6 to 1). The solution calculates the maximum possible change for each number in both arrays, sorts these potential changes in descending order, and greedily applies them until the sum difference is eliminated.

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

Intuition

When we need to make two sums equal, we're essentially trying to eliminate the difference between them. Let's say sum(nums1) < sum(nums2), so we have a gap of d = sum(nums2) - sum(nums1) to close.

To close this gap efficiently, we want each operation to have maximum impact. For the smaller sum array (nums1), we want to increase values as much as possible - changing a 1 to 6 gives us +5, changing a 2 to 6 gives us +4, and so on. For the larger sum array (nums2), we want to decrease values as much as possible - changing a 6 to 1 gives us -5 (which helps close the gap by 5), changing a 5 to 1 gives us -4, etc.

The key realization is that we can treat both types of changes uniformly:

  • Increasing a value v in nums1 to 6 contributes 6 - v toward closing the gap
  • Decreasing a value v in nums2 to 1 contributes v - 1 toward closing the gap

By collecting all these potential contributions into a single array and sorting them in descending order, we can greedily pick the most impactful changes first. We keep applying changes until the cumulative contribution meets or exceeds the gap d.

The greedy approach works because each change is independent - we're always better off making the change with the largest impact first. If the gap can be closed at all, this greedy strategy will find the minimum number of operations needed.

The impossibility check (-1 case) happens implicitly: if we've exhausted all possible changes and still haven't closed the gap, it means the gap is too large to bridge even with all values set to their extremes.

Learn more about Greedy patterns.

Solution Approach

The implementation follows the greedy strategy we identified:

  1. Calculate the initial difference: First, compute s1 = sum(nums1) and s2 = sum(nums2). If they're already equal, return 0.

  2. Normalize the problem: To simplify the logic, we ensure s1 < s2 by swapping the arrays if necessary. This way, we always work with the same scenario - trying to increase the smaller sum or decrease the larger sum.

  3. Build the contribution array: Create an array arr that contains all possible contributions:

    • For each value v in nums1 (the smaller sum array), add 6 - v to represent how much we can increase the sum by changing v to 6
    • For each value v in nums2 (the larger sum array), add v - 1 to represent how much we can decrease the sum by changing v to 1
  4. Greedy selection: Sort arr in descending order to prioritize the most impactful changes. The difference to close is d = s2 - s1.

  5. Apply changes until gap is closed: Iterate through the sorted contributions, subtracting each from d. Keep a counter i for the number of operations. Once d <= 0, we've closed the gap and return i.

  6. Handle impossible cases: If we exhaust all possible changes (finish the loop) without closing the gap, return -1.

The time complexity is O((m + n) log(m + n)) where m and n are the lengths of the arrays, dominated by the sorting step. The space complexity is O(m + n) for storing the contribution array.

The elegance of this solution lies in treating increases and decreases uniformly as "contributions" toward closing the gap, allowing us to use a simple greedy approach rather than complex decision-making about which array to modify.

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 small example to illustrate the solution approach.

Example: nums1 = [1, 6, 6] and nums2 = [1, 1, 1]

Step 1: Calculate initial sums

  • s1 = 1 + 6 + 6 = 13
  • s2 = 1 + 1 + 1 = 3
  • Since s1 > s2, we swap the arrays to ensure the first array has the smaller sum
  • After swapping: nums1 = [1, 1, 1] (sum = 3) and nums2 = [1, 6, 6] (sum = 13)
  • Gap to close: d = 13 - 3 = 10

Step 2: Build the contribution array For each element, calculate how much it can contribute to closing the gap:

  • From nums1 = [1, 1, 1] (we want to increase these):
    • 6 - 1 = 5 (changing 1β†’6 adds 5 to the sum)
    • 6 - 1 = 5 (changing 1β†’6 adds 5 to the sum)
    • 6 - 1 = 5 (changing 1β†’6 adds 5 to the sum)
  • From nums2 = [1, 6, 6] (we want to decrease these):
    • 1 - 1 = 0 (changing 1β†’1 contributes nothing)
    • 6 - 1 = 5 (changing 6β†’1 subtracts 5 from the sum)
    • 6 - 1 = 5 (changing 6β†’1 subtracts 5 from the sum)
  • Combined contributions: arr = [5, 5, 5, 0, 5, 5]

Step 3: Sort contributions in descending order

  • Sorted arr = [5, 5, 5, 5, 5, 0]

Step 4: Greedily apply changes

  • Start with gap d = 10
  • Operation 1: Use contribution of 5, d = 10 - 5 = 5
  • Operation 2: Use contribution of 5, d = 5 - 5 = 0
  • Gap closed! We need 2 operations

Verification: We could either:

  • Change two 1s in the original nums1 to 6s: [1, 6, 6] β†’ [6, 6, 6], sum becomes 18
  • Change both 6s in the original nums2 to 1s: [1, 1, 1] β†’ [1, 1, 1], sum stays 3 (Wait, that doesn't work since we need sum = 8 for both)

Actually, let me recalculate - if we change one 1β†’6 in the smaller array and one 6β†’1 in the larger array:

  • Original nums1 = [1, 1, 1] becomes [6, 1, 1], sum = 8
  • Original nums2 = [1, 6, 6] becomes [1, 1, 6], sum = 8
  • Both sums equal 8, achieved in 2 operations βœ“

Solution Implementation

1class Solution:
2    def minOperations(self, nums1: List[int], nums2: List[int]) -> int:
3        # Calculate the sum of both arrays
4        sum1, sum2 = sum(nums1), sum(nums2)
5      
6        # If sums are already equal, no operations needed
7        if sum1 == sum2:
8            return 0
9      
10        # Ensure sum1 is always the smaller sum for consistency
11        # If sum1 > sum2, swap the arrays and recur
12        if sum1 > sum2:
13            return self.minOperations(nums2, nums1)
14      
15        # Calculate maximum possible increase for each element in nums1 (can change to 6)
16        # and maximum possible decrease for each element in nums2 (can change to 1)
17        max_changes = [6 - value for value in nums1] + [value - 1 for value in nums2]
18      
19        # Calculate the difference we need to make up
20        difference = sum2 - sum1
21      
22        # Sort changes in descending order to greedily use largest changes first
23        # Enumerate starting from 1 to count operations
24        for operation_count, change_value in enumerate(sorted(max_changes, reverse=True), 1):
25            # Apply the change and reduce the difference
26            difference -= change_value
27          
28            # If we've made up the difference, return the operation count
29            if difference <= 0:
30                return operation_count
31      
32        # If we can't make the sums equal, return -1
33        return -1
34
1class Solution {
2    public int minOperations(int[] nums1, int[] nums2) {
3        // Calculate the sum of both arrays
4        int sum1 = Arrays.stream(nums1).sum();
5        int sum2 = Arrays.stream(nums2).sum();
6      
7        // If sums are already equal, no operations needed
8        if (sum1 == sum2) {
9            return 0;
10        }
11      
12        // Ensure sum1 is always smaller than sum2 for consistency
13        // If not, swap the arrays and process recursively
14        if (sum1 > sum2) {
15            return minOperations(nums2, nums1);
16        }
17      
18        // Calculate the difference we need to bridge
19        int difference = sum2 - sum1;
20      
21        // Create an array to store all possible changes
22        // Each element represents how much we can change in one operation
23        int[] possibleChanges = new int[nums1.length + nums2.length];
24        int index = 0;
25      
26        // For nums1: we can increase each element to 6 (maximum value)
27        // So the change is (6 - currentValue)
28        for (int value : nums1) {
29            possibleChanges[index++] = 6 - value;
30        }
31      
32        // For nums2: we can decrease each element to 1 (minimum value)
33        // So the change is (currentValue - 1)
34        for (int value : nums2) {
35            possibleChanges[index++] = value - 1;
36        }
37      
38        // Sort the changes in ascending order
39        Arrays.sort(possibleChanges);
40      
41        // Greedily use the largest changes first to minimize operations
42        int operationCount = 1;
43        for (int i = possibleChanges.length - 1; i >= 0; i--) {
44            difference -= possibleChanges[i];
45          
46            // If we've bridged the gap, return the number of operations
47            if (difference <= 0) {
48                return operationCount;
49            }
50          
51            operationCount++;
52        }
53      
54        // If we can't make the sums equal, return -1
55        return -1;
56    }
57}
58
1class Solution {
2public:
3    int minOperations(vector<int>& nums1, vector<int>& nums2) {
4        // Calculate the sum of both arrays
5        int sum1 = accumulate(nums1.begin(), nums1.end(), 0);
6        int sum2 = accumulate(nums2.begin(), nums2.end(), 0);
7      
8        // If sums are already equal, no operations needed
9        if (sum1 == sum2) {
10            return 0;
11        }
12      
13        // Ensure sum1 is always the smaller sum for consistent processing
14        // If sum1 > sum2, swap the arrays and recursively call
15        if (sum1 > sum2) {
16            return minOperations(nums2, nums1);
17        }
18      
19        // Calculate the difference we need to eliminate
20        int difference = sum2 - sum1;
21      
22        // Create an array to store the maximum change possible for each element
23        // For nums1: we can increase each element to 6 (maximum change: 6 - element)
24        // For nums2: we can decrease each element to 1 (maximum change: element - 1)
25        int totalSize = nums1.size() + nums2.size();
26        int maxChanges[totalSize];
27        int index = 0;
28      
29        // Add maximum possible increases from nums1 (changing each to 6)
30        for (int& value : nums1) {
31            maxChanges[index++] = 6 - value;
32        }
33      
34        // Add maximum possible decreases from nums2 (changing each to 1)
35        for (int& value : nums2) {
36            maxChanges[index++] = value - 1;
37        }
38      
39        // Sort the changes in descending order to use the largest changes first
40        sort(maxChanges, maxChanges + index, greater<>());
41      
42        // Greedily apply the largest changes until the difference is eliminated
43        for (int i = 0; i < index; ++i) {
44            difference -= maxChanges[i];
45            if (difference <= 0) {
46                return i + 1;  // Return the number of operations performed
47            }
48        }
49      
50        // If we cannot eliminate the difference, return -1
51        return -1;
52    }
53};
54
1function minOperations(nums1: number[], nums2: number[]): number {
2    // Calculate the sum of both arrays
3    let sum1 = nums1.reduce((acc, val) => acc + val, 0);
4    let sum2 = nums2.reduce((acc, val) => acc + val, 0);
5  
6    // If sums are already equal, no operations needed
7    if (sum1 === sum2) {
8        return 0;
9    }
10  
11    // Ensure sum1 is always the smaller sum for consistent processing
12    // If sum1 > sum2, swap the arrays and recursively call
13    if (sum1 > sum2) {
14        return minOperations(nums2, nums1);
15    }
16  
17    // Calculate the difference we need to eliminate
18    let difference = sum2 - sum1;
19  
20    // Create an array to store the maximum change possible for each element
21    // For nums1: we can increase each element to 6 (maximum change: 6 - element)
22    // For nums2: we can decrease each element to 1 (maximum change: element - 1)
23    const maxChanges: number[] = [];
24  
25    // Add maximum possible increases from nums1 (changing each to 6)
26    for (const value of nums1) {
27        maxChanges.push(6 - value);
28    }
29  
30    // Add maximum possible decreases from nums2 (changing each to 1)
31    for (const value of nums2) {
32        maxChanges.push(value - 1);
33    }
34  
35    // Sort the changes in descending order to use the largest changes first
36    maxChanges.sort((a, b) => b - a);
37  
38    // Greedily apply the largest changes until the difference is eliminated
39    for (let i = 0; i < maxChanges.length; i++) {
40        difference -= maxChanges[i];
41        if (difference <= 0) {
42            return i + 1;  // Return the number of operations performed
43        }
44    }
45  
46    // If we cannot eliminate the difference, return -1
47    return -1;
48}
49

Time and Space Complexity

Time Complexity: O(n log n + m log m) where n is the length of nums1 and m is the length of nums2.

  • Calculating the sums s1 and s2 takes O(n) + O(m) time.
  • Creating the arr list with list comprehensions takes O(n) + O(m) time.
  • Sorting the combined array arr of size n + m takes O((n + m) log(n + m)) time.
  • The iteration through the sorted array takes at most O(n + m) time.
  • The recursive call (if triggered) doesn't add to the complexity as it only swaps the parameters once and won't recurse again.
  • Overall, the dominant operation is sorting, giving us O((n + m) log(n + m)) which simplifies to O(n log n + m log m) when n and m are of similar magnitude.

Space Complexity: O(n + m)

  • The arr list stores n + m elements, requiring O(n + m) space.
  • The sorting operation may require O(n + m) additional space depending on the implementation.
  • The recursion depth is at most 1 (only one recursive call possible), adding O(1) to the call stack.
  • Therefore, the total space complexity is O(n + m).

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

Common Pitfalls

1. Forgetting to Check if Equal Sums are Achievable

A critical pitfall is not verifying whether it's mathematically possible to make the sums equal before attempting the greedy approach. This can lead to incorrect results or unnecessary computation.

The Problem: Consider extreme cases like nums1 = [1] and nums2 = [6,6,6,6,6,6]. Here:

  • sum(nums1) = 1 (can be at most 6 after changes)
  • sum(nums2) = 36 (can be at least 6 after changes)

Even with all possible changes, the maximum achievable sum for nums1 is 6, while the minimum achievable sum for nums2 is 6. These ranges don't overlap sufficiently to reach equality.

The Solution: Add a feasibility check at the beginning:

class Solution:
    def minOperations(self, nums1: List[int], nums2: List[int]) -> int:
        # Check if it's possible to make sums equal
        len1, len2 = len(nums1), len(nums2)
      
        # Maximum possible sum for each array is length * 6
        # Minimum possible sum for each array is length * 1
        if 6 * len1 < len2 or 6 * len2 < len1:
            return -1
      
        # Rest of the original solution continues...
        sum1, sum2 = sum(nums1), sum(nums2)
        # ... (rest of the code)

2. Inefficient Array Swapping

The recursive call return self.minOperations(nums2, nums1) when sum1 > sum2 creates unnecessary overhead and potential stack usage.

The Solution: Instead of recursion, simply swap the references:

class Solution:
    def minOperations(self, nums1: List[int], nums2: List[int]) -> int:
        sum1, sum2 = sum(nums1), sum(nums2)
      
        if sum1 == sum2:
            return 0
      
        # Swap references instead of recursive call
        if sum1 > sum2:
            nums1, nums2 = nums2, nums1
            sum1, sum2 = sum2, sum1
      
        # Continue with the logic...

3. Not Handling Edge Cases with Empty or Single-Element Arrays

While the problem states arrays contain integers between 1 and 6, it's good practice to handle edge cases explicitly, especially when array lengths might be very different.

Enhanced Solution with All Fixes:

class Solution:
    def minOperations(self, nums1: List[int], nums2: List[int]) -> int:
        # Early feasibility check
        len1, len2 = len(nums1), len(nums2)
        if 6 * min(len1, len2) < max(len1, len2):
            return -1
      
        sum1, sum2 = sum(nums1), sum(nums2)
      
        if sum1 == sum2:
            return 0
      
        # Ensure sum1 < sum2 without recursion
        if sum1 > sum2:
            nums1, nums2 = nums2, nums1
            sum1, sum2 = sum2, sum1
      
        # Calculate possible changes
        max_changes = [6 - v for v in nums1] + [v - 1 for v in nums2]
      
        difference = sum2 - sum1
      
        # Greedily apply largest changes
        for operation_count, change in enumerate(sorted(max_changes, reverse=True), 1):
            difference -= change
            if difference <= 0:
                return operation_count
      
        return -1

This enhanced version avoids recursion overhead, checks feasibility early to avoid unnecessary computation, and handles the problem more efficiently.

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

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

1def serialize(root):
2    res = []
3    def dfs(root):
4        if not root:
5            res.append('x')
6            return
7        res.append(root.val)
8        dfs(root.left)
9        dfs(root.right)
10    dfs(root)
11    return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4    StringJoiner res = new StringJoiner(" ");
5    serializeDFS(root, res);
6    return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10    if (root == null) {
11        result.add("x");
12        return;
13    }
14    result.add(Integer.toString(root.val));
15    serializeDFS(root.left, result);
16    serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2    let res = [];
3    serialize_dfs(root, res);
4    return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8    if (!root) {
9        res.push("x");
10        return;
11    }
12    res.push(root.val);
13    serialize_dfs(root.left, res);
14    serialize_dfs(root.right, res);
15}
16

Recommended Readings

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

Load More