1674. Minimum Moves to Make Array Complementary


Problem Description

You are presented with an integer array nums with an even length n and an integer limit. You're able to perform operations where you replace any integer in the array with another integer that is within the inclusive range from 1 to limit. The goal is to make the array complementary.

An array is complementary if for every index i (0-indexed), the sum nums[i] + nums[n - 1 - i] is the same across the entire array. For instance, the array [1,2,3,4] is complementary because every pair of numbers adds up to 5.

Your task is to determine the minimum number of moves required to make the array nums complementary.

Intuition

To solve this problem efficiently, we need to think in terms of pairs and how changing an element impacts the sums across the array. We also need to record the minimum operations required to achieve a target sum for each pair at various intervals. The strategy is to use a difference array d to record the number of operations required to achieve each possible sum.

Here's the process of thinking that leads to the solution approach:

  1. Since nums has an even length, we only need to consider the first half of the array when paired with their corresponding element from the other end because we want to make nums[i] + nums[n - 1 - i] equal for all i.

  2. Each pair (nums[i], nums[n - 1 - i]) can contribute to the sum in three different ways:

    • No operation is needed if the sum of this pair already equals the target complementary sum.
    • One operation is needed if we can add a number to one of the elements in the pair to achieve the target sum.
    • Two operations when neither of the elements in the pair can directly contribute to the target sum and both need to be replaced.
  3. We use a trick called "difference array" or "prefix sum array" to efficiently update the range of sums with the respective counts of operations. We track the changes in the required operations as we move through different sum ranges.

  4. We increment moves for all the sums and later decrement as per the pair values and the allowed limits.

  5. Finally, we iterate over the sum range from 2 to 2 * limit and accumulate the changes recorded in our difference array. This reveals the total moves required for each sum to be the target sum.

  6. Our goal is to find the minimum of these accumulated moves, which represents the least number of operations needed to make the array complementary.

Understanding the optimized approach is a bit tricky, but it cleverly manages the different cases to minimize the total number of moves.

Learn more about Prefix Sum patterns.

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

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

Solution Approach

The solution implements an optimization strategy called difference array, which is a form of prefix sum optimization. Here's the step by step explanation of how the algorithm works by walking through the code:

  1. Initialize a difference array d with a length of 2 * limit + 2. This array will store the changes in the number of moves required to make each sum complementary. The extra indices account for all possible sums which can occur from 2 (minimum sum) to 2 * limit (maximum sum).
1d = [0] * (limit * 2 + 2)
  1. Loop through the first half of the nums array to check pairs of numbers. Here n >> 1 is a bitwise operation equivalent to dividing n by 2. For each pair (nums[i], nums[n - 1 - i]), determine their minimum (a) and maximum (b) values.
1for i in range(n >> 1):
2    a, b = min(nums[i], nums[n - i - 1]), max(nums[i], nums[n - i - 1])
  1. Update the difference array to reflect the default case where 2 moves are needed for all sums from 2 to 2 * limit.
1d[2] += 2
2d[limit * 2 + 1] -= 2
  1. Update the difference array for scenarios where only 1 move is required. This happens when adding a number to the minimum of the pair, up to the maximum of the pair with limit.
1d[a + 1] -= 1
2d[b + limit + 1] += 1
  1. For the exact sum a + b, decrease the number of moves by 1 as this is already part of the complementary sum. For a + b + 1, revert this operation since we consider the sum a + b only.
1d[a + b] -= 1
2d[a + b + 1] += 1
  1. Initialize ans with n, representing the maximum possible number of moves (every element needing 2 moves), and a sum s which will be used to accumulate the total moves from the difference array.
1ans, s = n, 0
  1. Loop through the potential sums to calculate the prefix sum (which represents the cumulative number of moves) at each value. Update ans to hold the minimum number of moves required.
1for v in d[2 : limit * 2 + 1]:
2    s += v
3    if ans > s:
4        ans = s
  1. Finally, we return ans, which now contains the minimum number of moves required to make nums complementary.

By leveraging the difference array technique, the algorithm efficiently calculates the minimum number of operations required by reducing the problem to range updates and point queries, which can be processed in a linear time complexity relative to the limit.

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

What's the relationship between a tree and a graph?

Example Walkthrough

Let's walk through an example using the above solution approach. Suppose we have the following:

  • Integer array nums: [1, 2, 4, 3]
  • Integer limit: 4

The length of the array n is 4, which is even, and the limit is 4, so any replacement operations must yield a number between 1 to 4.

  1. We initialize our difference array d with a size of 2 * limit + 2 to account for all possible sums.

    1d = [0] * (4 * 2 + 2)  # [0, 0, 0, 0, 0, 0, 0, 0, 0]
  2. There are two pairs to consider: (1, 3) and (2, 4).

  3. The difference array is first updated to indicate that, by default, 2 moves are needed for each sum.

    1d[2] += 2
    2d[9] -= 2  # Since our limit is 4, `limit * 2 + 1` is 9.
  4. Now, we loop through each pair. For the first pair (1, 3), the minimum is a = 1 and the maximum is b = 3. We then update the difference array based on the cases where 1 move is required.

    1d[a + 1] -= 1  # d[2] becomes 1
    2d[b + limit + 1] += 1  # d[8] becomes 1
  5. For the exact sum a + b, which is 4, we decrease the number of moves by 1 as this is already part of the complementary sum. For the value after a + b, which is 5, we revert this operation.

    1d[a + b] -= 1  # d[4] becomes -1
    2d[a + b + 1] += 1  # d[5] becomes 1
  6. We repeat steps 4 and 5 for the second pair (2, 4). The minimum is a = 2 and the maximum is b = 4.

    1d[a + 1] -= 1  # d[3] becomes 1
    2d[b + limit + 1] += 1  # d[9] stays at -1 because of the earlier decrement
    3
    4d[a + b] -= 1  # d[6] becomes -1
    5d[a + b + 1] += 1  # d[7] becomes 1
  7. After updating the difference array with all pairs, we initialize ans with the maximum possible number of moves and then accumulate the changes to find the total moves required for each sum.

    1ans, s = n, 0  # ans = 4, s = 0
  8. Finally, we iterate through the possible sums using the accumulated changes and find the minimum number of moves.

    1for v in d[2:8 + 1]:
    2    s += v
    3    ans = min(ans, s)

The final accumulated changes in the difference array would be something like [0, 0, 1, 1, 0, 1, -1, 0, 2, -1]. We add these to our running total s and continuously update ans to find that the minimum number of moves required to make nums complementary is 1.

Therefore, the minimum number of moves required to make the array [1, 2, 4, 3] complementary with a limit of 4 is 1, which could be accomplished by changing the 4 to a 2.

Solution Implementation

1class Solution:
2    def minMoves(self, nums: List[int], limit: int) -> int:
3        delta = [0] * (limit * 2 + 2)  # Array to track the changes in the number of moves
4        n = len(nums)  # Number of elements in the list
5
6        # Iterate over the first half of the list
7        for i in range(n // 2):  
8            # Find the minimum and maximum of the pair (i-th and mirrored i-th elements)
9            min_val, max_val = min(nums[i], nums[n - i - 1]), max(nums[i], nums[n - i - 1])
10
11            # Always counting 2 moves since every pair needs at least one move
12            delta[2] += 2
13            delta[limit * 2 + 1] -= 2
14
15            # Reducing by 1 move if only one element of the pair is changed to match the other
16            delta[min_val + 1] -= 1
17            delta[max_val + limit + 1] += 1
18
19            # Reducing by another move if the sum can be achieved without altering either element
20            delta[min_val + max_val] -= 1
21            delta[min_val + max_val + 1] += 1
22
23        min_moves, current_moves = n, 0  # Initialize min_moves to maximum possible and current_moves
24        # Accumulate the deltas to include the total move changes up to each sum
25        for moves_change in delta[2: limit * 2 + 1]:  
26            current_moves += moves_change
27            # If the current number of moves is less than the previous minimum, update min_moves
28            if min_moves > current_moves:
29                min_moves = current_moves
30        return min_moves  # Return the minimum moves required
31
1class Solution {
2    public int minMoves(int[] nums, int limit) {
3        int pairsCount = nums.length / 2;// Store the number of pairs
4        int[] delta = new int[limit * 2 + 2]; // Delta array to store the cost changes
5      
6        // Iterate over each pair
7        for (int i = 0; i < pairsCount; ++i) {
8            // Taking the minimum and maximum of the pair for optimization
9            int minOfPair = Math.min(nums[i], nums[nums.length - i - 1]);
10            int maxOfPair = Math.max(nums[i], nums[nums.length - i - 1]);
11
12            // Always needs 2 moves if sum is larger than max possible sum 'limit * 2'
13            delta[2] += 2;
14            delta[limit * 2 + 1] -= 2;
15
16            // If we make minOfPair + 1 the new sum, we would need one less move
17            delta[minOfPair + 1] -= 1;
18            // If the sum is greater than maxOfPair + limit, then we'll need an additional move
19            delta[maxOfPair + limit + 1] += 1;
20
21            // We need one less move for making the sum equals minOfPair + maxOfPair (to make a straight sum)
22            delta[minOfPair + maxOfPair] -= 1;
23            delta[minOfPair + maxOfPair + 1] += 1;
24        }
25      
26        int minMoves = pairsCount * 2; // Initialize minMoves to the maximum possible value
27        int currentCost = 0; // Variable to accumulate current cost
28      
29        // Iterate over possible sums
30        for (int sum = 2; sum <= limit * 2; ++sum) {
31            currentCost += delta[sum]; // Update current cost
32            // If current cost is less than minMoves, update minMoves
33            if (currentCost < minMoves) {
34                minMoves = currentCost;
35            }
36        }
37      
38        return minMoves; // Return the minimum moves required
39    }
40}
41
1#include <vector>
2#include <algorithm>
3
4class Solution {
5public:
6    // Function to determine the minimum number of moves required
7    // to make all the sums of pairs in nums be less than or equal to limit
8    int minMoves(std::vector<int>& nums, int limit) {
9        int n = nums.size(); // Number of elements in nums
10        std::vector<int> delta(limit * 2 + 2, 0); // Array to track the changes in moves
11      
12        // Calculate the change in moves for each pair
13        for (int i = 0; i < n / 2; ++i) {
14            int lower = std::min(nums[i], nums[n - i - 1]); // Lower of the pair
15            int upper = std::max(nums[i], nums[n - i - 1]); // Upper of the pair
16
17            // Increment by two for the worst case (both need to change)
18            delta[2] += 2;
19            delta[limit * 2 + 1] -= 2;
20
21            // Decrement by one as at least one of the numbers need to change
22            delta[lower + 1] -= 1;
23            delta[upper + limit + 1] += 1;
24
25            // No operation needed when the sum equals lower+upper
26            delta[lower + upper] -= 1;
27            delta[lower + upper + 1] += 1;
28        }
29      
30        int minMoves = n; // Start with the worst case moves count
31        int currentMoves = 0; // Initialize a counter for the current moves
32      
33        // Accumulate the delta values and find the minimum moves
34        for (int i = 2; i <= limit * 2; ++i) {
35            currentMoves += delta[i];
36            if (minMoves > currentMoves) {
37                minMoves = currentMoves; // Keep track of the minimum moves
38            }
39        }
40      
41        // Return the minimum moves to make all pair sums within the limit
42        return minMoves;
43    }
44};
45
1function minMoves(nums: number[], limit: number): number {
2    const n = nums.length; // Number of elements in nums
3    const delta: number[] = new Array(limit * 2 + 2).fill(0); // Array to track the changes in moves
4  
5    // Calculate the change in moves for each pair
6    for (let i = 0; i < Math.floor(n / 2); ++i) {
7        const lower = Math.min(nums[i], nums[n - i - 1]); // Lower of the pair
8        const upper = Math.max(nums[i], nums[n - i - 1]); // Upper of the pair
9
10        // Increment by two for the case where both numbers need to change
11        delta[2] += 2;
12        delta[limit * 2 + 1] -= 2;
13
14        // Decrement by one because at least one of the numbers need to change
15        delta[lower + 1] -= 1;
16        delta[upper + limit + 1] += 1;
17
18        // No changes needed if the sum equals the sum of the lower, upper pair
19        delta[lower + upper] -= 1;
20        delta[lower + upper + 1] += 1;
21    }
22  
23    let minMoves = n; // Initialize minimum moves with the worst case, which is n moves
24    let currentMoves = 0; // Initialize the counter for the current moves
25  
26    // Accumulate the delta values and find the minimum moves required
27    for (let i = 2; i <= limit * 2; ++i) {
28        currentMoves += delta[i];
29        if (minMoves > currentMoves) {
30            minMoves = currentMoves; // Update minimum moves if a new minimum is found
31        }
32    }
33  
34    // Return the minimum moves to make all pair sums within the limit
35    return minMoves;
36}
37
Not Sure What to Study? Take the 2-min Quiz:

In a binary min heap, the minimum element can be found in:

Time and Space Complexity

Time Complexity:

The given code performs its core logic in a for-loop that iterates n/2 times, where n is the length of the list nums. For each iteration of the loop, the code performs a constant number of arithmetic operations which are independent of the size of n. So, the time complexity of the for-loop is O(n/2) which simplifies to O(n).

After updating the d list, the code runs another loop that iterates 2*limit - 1 times. Each iteration of this loop performs a constant number of operations, contributing O(limit) to the complexity.

Thus, the total time complexity of the code is O(n) + O(limit), which is the linear time complexity dominated by the larger of n and limit.

Space Complexity:

The space complexity of the code is governed by the size of the list d, which is initialized as 0 * (limit * 2 + 2). This means the space used by d is proportional to 2*limit + 2, which is O(limit).

Therefore, the space complexity of the given code is O(limit).

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

Fast Track Your Learning with Our Quick Skills Quiz:

Which two pointer technique does Quick Sort use?


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 👨‍🏫