Facebook Pixel

945. Minimum Increment to Make Array Unique

Problem Description

You have an integer array nums. You can perform moves on this array where in each move, you select any index i (where 0 <= i < nums.length) and increase nums[i] by 1.

Your goal is to make all values in the array unique (no duplicate values). You need to find the minimum number of moves required to achieve this.

For example, if you have nums = [1, 2, 2], you would need to increment one of the 2s at least once to make it 3, resulting in [1, 2, 3] with all unique values. This would take 1 move.

The problem guarantees that the answer will fit in a 32-bit integer.

The key insight is that you can only increment values (never decrement), so you need to strategically choose which elements to increment and by how much to minimize the total number of operations while ensuring all final values are distinct.

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

Intuition

Since we can only increment values and not decrement them, the optimal strategy is to process numbers from smallest to largest. This way, we can ensure that each number is placed at the smallest possible unique position.

Think about it this way: if we have duplicate values or values that are too close together, we need to "push" some of them forward (increase them) to create gaps. By sorting the array first, we can process elements in order and maintain a running minimum value that each subsequent element must be at least equal to.

The key observation is that for each element in the sorted array, it should be at least one greater than the previous element to maintain uniqueness. If we track the minimum value y that the current element should become, we can calculate:

  • If the current element x is already greater than y, we can keep it as is (set y = x)
  • If the current element x is less than or equal to y, we must increment it to y + 1

For example, with sorted array [1, 1, 2, 2, 3]:

  • First 1 stays as 1 (y becomes 1)
  • Second 1 must become 2 (y becomes 2, we need 1 move)
  • First 2 must become 3 (y becomes 3, we need 1 move)
  • Second 2 must become 4 (y becomes 4, we need 2 moves)
  • The 3 must become 5 (y becomes 5, we need 2 moves)

The formula y = max(y + 1, x) elegantly captures this logic: we either take the current element if it's already large enough, or we take the next available position y + 1.

Learn more about Greedy and Sorting patterns.

Solution Approach

The implementation follows a sorting + greedy approach:

  1. Sort the array: First, we sort nums in ascending order. This allows us to process elements from smallest to largest, ensuring we can assign the minimum possible values to maintain uniqueness.

  2. Initialize tracking variables:

    • ans = 0: Accumulates the total number of moves needed
    • y = -1: Tracks the minimum value the current element should become (initialized to -1 so the first element can be 0 if needed)
  3. Iterate through sorted array: For each element x in the sorted array:

    • Calculate the minimum value this element should become: y = max(y + 1, x)
      • If x > y, then x is already large enough and doesn't conflict with previous elements, so y = x
      • If x <= y, then x needs to be incremented to at least y + 1 to maintain uniqueness
    • Add the number of moves needed: ans += y - x
      • This represents how many times we need to increment x to reach value y
  4. Return the result: After processing all elements, ans contains the minimum total moves needed.

Example walkthrough with nums = [3, 2, 1, 2, 1, 7]:

  • After sorting: [1, 1, 2, 2, 3, 7]
  • Process 1: y = max(-1 + 1, 1) = 1, moves = 1 - 1 = 0
  • Process 1: y = max(1 + 1, 1) = 2, moves = 2 - 1 = 1
  • Process 2: y = max(2 + 1, 2) = 3, moves = 3 - 2 = 1
  • Process 2: y = max(3 + 1, 2) = 4, moves = 4 - 2 = 2
  • Process 3: y = max(4 + 1, 3) = 5, moves = 5 - 3 = 2
  • Process 7: y = max(5 + 1, 7) = 7, moves = 7 - 7 = 0
  • Total moves = 0 + 1 + 1 + 2 + 2 + 0 = 6

The time complexity is O(n log n) due to sorting, and the space complexity is O(1) if we don't count the sorting space.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example with nums = [2, 2, 1] to illustrate the solution approach.

Step 1: Sort the array

  • Original: [2, 2, 1]
  • Sorted: [1, 2, 2]

Step 2: Initialize variables

  • ans = 0 (total moves counter)
  • y = -1 (tracks minimum value for current element)

Step 3: Process each element

Processing element 1 (index 0):

  • Calculate: y = max(-1 + 1, 1) = max(0, 1) = 1
  • This element stays at value 1
  • Moves needed: 1 - 1 = 0
  • Running total: ans = 0

Processing element 2 (index 1):

  • Calculate: y = max(1 + 1, 2) = max(2, 2) = 2
  • This element stays at value 2
  • Moves needed: 2 - 2 = 0
  • Running total: ans = 0

Processing element 2 (index 2):

  • Calculate: y = max(2 + 1, 2) = max(3, 2) = 3
  • This element must be incremented to 3 (since 2 is already taken)
  • Moves needed: 3 - 2 = 1
  • Running total: ans = 1

Step 4: Result

  • Final array values would be: [1, 2, 3] (all unique)
  • Minimum moves required: 1

The algorithm ensures each element gets the smallest possible unique value by processing them in sorted order and tracking the minimum available position with variable y.

Solution Implementation

1class Solution:
2    def minIncrementForUnique(self, nums: List[int]) -> int:
3        # Sort the array to process numbers in ascending order
4        nums.sort()
5      
6        # Initialize total moves counter and the minimum next available value
7        total_moves = 0
8        next_available = -1
9      
10        # Process each number in sorted order
11        for current_num in nums:
12            # The next available position is either:
13            # 1. The previous number + 1 (to maintain uniqueness)
14            # 2. The current number itself (if it's already larger)
15            next_available = max(next_available + 1, current_num)
16          
17            # Add the difference between where we place the number and its original value
18            # If next_available > current_num, we need to increment current_num
19            # If next_available == current_num, no increment needed (difference is 0)
20            total_moves += next_available - current_num
21      
22        return total_moves
23
1class Solution {
2    public int minIncrementForUnique(int[] nums) {
3        // Sort the array to process numbers in ascending order
4        Arrays.sort(nums);
5      
6        // Initialize total increments needed and minimum available value
7        int totalIncrements = 0;
8        int minAvailable = -1;
9      
10        // Process each number in sorted order
11        for (int currentNum : nums) {
12            // The next unique value must be at least minAvailable + 1
13            // If current number is larger, we can use it directly
14            minAvailable = Math.max(minAvailable + 1, currentNum);
15          
16            // Add the difference between required unique value and current value
17            totalIncrements += minAvailable - currentNum;
18        }
19      
20        return totalIncrements;
21    }
22}
23
1class Solution {
2public:
3    int minIncrementForUnique(vector<int>& nums) {
4        // Sort the array to process elements in ascending order
5        sort(nums.begin(), nums.end());
6      
7        // Initialize the total number of increments needed
8        int totalIncrements = 0;
9      
10        // Track the minimum value the current element should become
11        // Initialize to -1 so the first element can be 0 if needed
12        int nextAvailableValue = -1;
13      
14        // Process each number in the sorted array
15        for (int currentNum : nums) {
16            // The current number must be at least nextAvailableValue + 1
17            // or keep its original value if it's already larger
18            nextAvailableValue = max(nextAvailableValue + 1, currentNum);
19          
20            // Add the difference between the target value and original value
21            // This represents how many increments are needed for this element
22            totalIncrements += nextAvailableValue - currentNum;
23        }
24      
25        return totalIncrements;
26    }
27};
28
1/**
2 * Given an array of integers, returns the minimum number of increments
3 * needed to make all array elements unique.
4 * 
5 * @param nums - Array of integers to make unique
6 * @returns Minimum number of increments required
7 */
8function minIncrementForUnique(nums: number[]): number {
9    // Sort the array in ascending order to process elements systematically
10    nums.sort((a: number, b: number) => a - b);
11  
12    // totalIncrements: tracks the total number of increments needed
13    // nextAvailable: tracks the next available unique value we can assign
14    let totalIncrements: number = 0;
15    let nextAvailable: number = -1;
16  
17    // Process each number in the sorted array
18    for (const currentNum of nums) {
19        // The next available value is either:
20        // 1. The previous assigned value + 1 (to maintain uniqueness)
21        // 2. The current number itself (if it's already larger)
22        nextAvailable = Math.max(nextAvailable + 1, currentNum);
23      
24        // Add the difference between assigned value and original value
25        // If nextAvailable > currentNum, we need (nextAvailable - currentNum) increments
26        // If nextAvailable == currentNum, no increments needed (difference is 0)
27        totalIncrements += nextAvailable - currentNum;
28    }
29  
30    return totalIncrements;
31}
32

Time and Space Complexity

The time complexity of this solution is O(n log n), where n is the length of the array nums. This is dominated by the sorting operation nums.sort(), which takes O(n log n) time. The subsequent for loop iterates through the sorted array once, performing constant-time operations (max, addition, subtraction) in each iteration, contributing O(n) time. Since O(n log n) + O(n) = O(n log n), the overall time complexity is O(n log n).

The space complexity is O(log n). While the algorithm only uses a constant amount of extra variables (ans and y), the sorting operation typically requires O(log n) space for the recursion stack when using algorithms like quicksort or the temporary space used in merge sort's divide-and-conquer approach. Python's built-in sort() method uses Timsort, which has a worst-case space complexity of O(n), but typically uses O(log n) space in practice. Following the reference answer's assessment, the space complexity is O(log n).

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

Common Pitfalls

1. Attempting to Track Used Values with a Set/HashSet

A common mistake is trying to solve this problem by maintaining a set of already-used values and incrementing duplicates until they find an unused slot:

Incorrect Approach:

def minIncrementForUnique(self, nums: List[int]) -> int:
    used = set()
    total_moves = 0
  
    for num in nums:
        current = num
        while current in used:
            current += 1
            total_moves += 1
        used.add(current)
  
    return total_moves

Why it fails: This approach has terrible time complexity. In the worst case (e.g., nums = [1, 1, 1, ..., 1] with n elements), each subsequent element needs to search through increasingly large ranges, leading to O(nยฒ) time complexity or worse.

Solution: Always sort first! The sorted + greedy approach ensures O(n log n) time complexity and avoids redundant searching.

2. Not Handling the Case Where Original Values Are Already Larger

Some implementations forget that if a number is already larger than the minimum required value, we should use it as-is:

Incorrect:

def minIncrementForUnique(self, nums: List[int]) -> int:
    nums.sort()
    total_moves = 0
  
    for i in range(1, len(nums)):
        if nums[i] <= nums[i-1]:
            target = nums[i-1] + 1
            total_moves += target - nums[i]
            nums[i] = target  # Modifying the array
  
    return total_moves

Problem: This only handles consecutive duplicates but misses the optimization when numbers have gaps. Also, it modifies the input array unnecessarily.

Solution: Use max(next_available + 1, current_num) to handle both cases elegantly without modifying the input.

3. Integer Overflow Concerns (Language-Specific)

In languages like Java or C++, calculating the sum of moves might overflow:

Potential Issue:

int totalMoves = 0;  // Using int in Java
for (int num : nums) {
    // Large differences could cause overflow
    totalMoves += nextAvailable - num;
}

Solution: Use appropriate data types (long in Java, long long in C++) even though the problem states the answer fits in 32-bit integer. The intermediate calculations might still overflow before the final result.

4. Off-by-One Error in Initial Value

Starting with the wrong initial value for tracking the next available position:

Incorrect:

next_available = 0  # Wrong initialization

Problem: This forces the first element to be at least 0, which might require unnecessary moves if the first element is negative.

Solution: Initialize to a value that ensures the first element can keep its original value if possible (like -1 or float('-inf')).

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

How does quick sort divide the problem into subproblems?


Recommended Readings

Want a Structured Path to Master System Design Too? Donโ€™t Miss This!

Load More