Facebook Pixel

3397. Maximum Number of Distinct Elements After Operations

Problem Description

You have an integer array nums and an integer k. You can modify each element in the array by adding any integer value between -k and k (inclusive) to it, but you can only perform this operation at most once per element.

For example, if an element has value 5 and k = 3, you could:

  • Leave it as 5 (add 0)
  • Change it to any value from 2 to 8 (by adding values from -3 to 3)

Your goal is to maximize the number of distinct elements in the array after performing these operations.

The problem asks you to strategically modify the array elements to create as many different values as possible. Since you can add values in the range [-k, k] to each element, you need to figure out the optimal way to spread out the elements to minimize overlaps and maximize the count of unique values.

For instance, if nums = [1, 2, 3] and k = 1:

  • Element 1 can become any value in [0, 2]
  • Element 2 can become any value in [1, 3]
  • Element 3 can become any value in [2, 4]

By choosing 0 for the first element, 1 for the second, and 2 for the third, you would get 3 distinct elements. But if you chose 1 for the first element, 2 for the second, and 3 for the third, you would still have 3 distinct elements. The challenge is finding the optimal assignment when the array is larger and elements might be closer together or farther apart.

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

Intuition

The key insight is that we want to spread out the elements as much as possible to avoid overlaps. Think of each element as having a "range" of possible values it can become: for element x, it can become any value in [x - k, x + k].

If we process elements in a random order, it becomes difficult to avoid collisions. However, if we sort the array first, we can process elements from smallest to largest, which gives us a clear strategy.

For the smallest element, we should make it as small as possible by subtracting k from it. This leaves maximum room for the larger elements to find distinct values.

For each subsequent element, we face a choice: what value should we assign to it? We want to:

  1. Make it as small as possible (to leave room for future elements)
  2. But ensure it's larger than any previously assigned value (to maintain distinctness)

This leads to a greedy approach: for each element x, we try to assign it the smallest possible value that is:

  • At least pre + 1 (where pre is the largest value we've assigned so far)
  • At least x - k (the minimum value this element can become)
  • At most x + k (the maximum value this element can become)

So the optimal value for element x is min(x + k, max(x - k, pre + 1)). The max(x - k, pre + 1) ensures we don't go below either bound, and the outer min with x + k ensures we don't exceed the upper limit.

If this calculated value is greater than pre, we successfully found a distinct value and can increment our count. Otherwise, we can't make this element distinct from previous ones.

By processing sorted elements left to right and greedily assigning the smallest valid value to each, we maximize the total number of distinct elements.

Learn more about Greedy and Sorting patterns.

Solution Approach

The implementation follows a greedy approach with sorting:

  1. Sort the array: First, we sort nums in ascending order. This allows us to process elements from smallest to largest, making it easier to assign values without conflicts.

  2. Initialize tracking variables:

    • ans = 0: Counts the number of distinct elements we can create
    • pre = -inf: Tracks the maximum value assigned so far, initialized to negative infinity so the first element can take any value
  3. Process each element greedily: For each element x in the sorted array:

    • Calculate the optimal value to assign: cur = min(x + k, max(x - k, pre + 1))
      • x - k is the minimum value this element can become
      • pre + 1 is the minimum value needed to ensure distinctness from previous elements
      • max(x - k, pre + 1) gives us the smallest valid value considering both constraints
      • min(x + k, ...) ensures we don't exceed the maximum allowed value for this element
    • Check if we can create a distinct element:
      • If cur > pre, we successfully found a distinct value:
        • Increment ans by 1
        • Update pre = cur to track this new maximum assigned value
      • If cur <= pre, we cannot make this element distinct (its entire range [x - k, x + k] is already occupied by previous assignments)
  4. Return the result: After processing all elements, ans contains the maximum number of distinct elements possible.

The algorithm runs in O(n log n) time due to sorting, with O(1) extra space (not counting the input). The greedy strategy of assigning the smallest possible distinct value to each element ensures we maximize the total count of distinct elements.

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 an example with nums = [4, 4, 7, 9] and k = 2.

Step 1: Sort the array

  • Array is already sorted: [4, 4, 7, 9]
  • Initialize: ans = 0, pre = -infinity

Step 2: Process first element (x = 4)

  • Element 4 can become any value in range [4-2, 4+2] = [2, 6]
  • Calculate optimal value: cur = min(6, max(2, -inf + 1)) = min(6, 2) = 2
  • Since 2 > -inf, we can assign this value
  • Update: ans = 1, pre = 2
  • We've assigned value 2 to the first element

Step 3: Process second element (x = 4)

  • Element 4 can become any value in range [2, 6]
  • Calculate optimal value: cur = min(6, max(2, 2 + 1)) = min(6, 3) = 3
  • Since 3 > 2, we can assign this value
  • Update: ans = 2, pre = 3
  • We've assigned value 3 to the second element

Step 4: Process third element (x = 7)

  • Element 7 can become any value in range [5, 9]
  • Calculate optimal value: cur = min(9, max(5, 3 + 1)) = min(9, 5) = 5
  • Since 5 > 3, we can assign this value
  • Update: ans = 3, pre = 5
  • We've assigned value 5 to the third element

Step 5: Process fourth element (x = 9)

  • Element 9 can become any value in range [7, 11]
  • Calculate optimal value: cur = min(11, max(7, 5 + 1)) = min(11, 7) = 7
  • Since 7 > 5, we can assign this value
  • Update: ans = 4, pre = 7
  • We've assigned value 7 to the fourth element

Result: We can create 4 distinct elements by transforming:

  • First 42 (subtract 2)
  • Second 43 (subtract 1)
  • Third 75 (subtract 2)
  • Fourth 97 (subtract 2)

The final array would be [2, 3, 5, 7] with all distinct values.

Solution Implementation

1class Solution:
2    def maxDistinctElements(self, nums: List[int], k: int) -> int:
3        # Sort the array to process elements in ascending order
4        nums.sort()
5      
6        # Initialize the count of distinct elements
7        distinct_count = 0
8      
9        # Track the previous assigned value (start with negative infinity)
10        previous_value = float('-inf')
11      
12        # Process each number in sorted order
13        for num in nums:
14            # Calculate the optimal value for current element:
15            # 1. We want it to be at least previous_value + 1 (to ensure distinctness)
16            # 2. But it cannot be less than num - k (lower bound of modification range)
17            # 3. And it cannot be greater than num + k (upper bound of modification range)
18          
19            # The earliest valid position is max(num - k, previous_value + 1)
20            # But we're limited by the upper bound num + k
21            current_value = min(num + k, max(num - k, previous_value + 1))
22          
23            # If we can assign a value greater than the previous one, it's distinct
24            if current_value > previous_value:
25                distinct_count += 1
26                previous_value = current_value
27      
28        return distinct_count
29
1class Solution {
2    public int maxDistinctElements(int[] nums, int k) {
3        // Sort the array to process elements in ascending order
4        Arrays.sort(nums);
5      
6        int n = nums.length;
7        int distinctCount = 0;  // Count of distinct elements after modification
8        int previousValue = Integer.MIN_VALUE;  // Track the last assigned value
9      
10        // Process each element in sorted order
11        for (int currentNum : nums) {
12            // Calculate the optimal value for current element:
13            // - Can be at most currentNum + k (upper bound of modification range)
14            // - Must be at least max of:
15            //   1. currentNum - k (lower bound of modification range)
16            //   2. previousValue + 1 (to maintain distinctness)
17            int optimalValue = Math.min(currentNum + k, Math.max(currentNum - k, previousValue + 1));
18          
19            // Check if we can assign a valid distinct value
20            if (optimalValue > previousValue) {
21                // Successfully assigned a distinct value
22                distinctCount++;
23                previousValue = optimalValue;
24            }
25            // If optimalValue <= previousValue, we cannot make this element distinct
26        }
27      
28        return distinctCount;
29    }
30}
31
1class Solution {
2public:
3    int maxDistinctElements(vector<int>& nums, int k) {
4        // Sort the array to process elements in ascending order
5        ranges::sort(nums);
6      
7        // Initialize result counter and previous selected value
8        int distinctCount = 0;
9        int previousValue = INT_MIN;
10      
11        // Process each element in sorted order
12        for (int currentNum : nums) {
13            // Calculate the valid range for current element: [currentNum - k, currentNum + k]
14            // We want to pick the smallest valid value that is greater than previousValue
15          
16            // Lower bound of valid range
17            int lowerBound = currentNum - k;
18          
19            // The minimum valid value we can pick (must be > previousValue)
20            int minValidValue = max(lowerBound, previousValue + 1);
21          
22            // Upper bound of valid range
23            int upperBound = currentNum + k;
24          
25            // The actual value we pick (constrained by upper bound)
26            int selectedValue = min(upperBound, minValidValue);
27          
28            // Check if we can select a valid distinct value
29            if (selectedValue > previousValue) {
30                // We found a valid distinct value
31                distinctCount++;
32                previousValue = selectedValue;
33            }
34        }
35      
36        return distinctCount;
37    }
38};
39
1/**
2 * Finds the maximum number of distinct elements after modifying each element
3 * by at most k (can add or subtract any value between -k and k)
4 * @param nums - Array of integers to process
5 * @param k - Maximum modification allowed for each element
6 * @returns Maximum number of distinct elements possible
7 */
8function maxDistinctElements(nums: number[], k: number): number {
9    // Sort the array in ascending order to process elements systematically
10    nums.sort((a: number, b: number) => a - b);
11  
12    // Initialize answer counter and previous assigned value
13    let distinctCount: number = 0;
14    let previousValue: number = -Infinity;
15  
16    // Process each element in sorted order
17    for (const currentNum of nums) {
18        // Calculate the optimal value for current element:
19        // - Can't be less than (currentNum - k) due to modification constraint
20        // - Should be at least (previousValue + 1) to maintain distinctness
21        // - Can't be more than (currentNum + k) due to modification constraint
22        const lowerBound: number = Math.max(currentNum - k, previousValue + 1);
23        const assignedValue: number = Math.min(currentNum + k, lowerBound);
24      
25        // If we can assign a value greater than the previous one, it's distinct
26        if (assignedValue > previousValue) {
27            distinctCount++;
28            previousValue = assignedValue;
29        }
30    }
31  
32    return distinctCount;
33}
34

Time and Space Complexity

The time complexity of this algorithm is O(n × log n), where n is the length of the array nums. This is dominated by the sorting operation nums.sort() which requires O(n × log n) time. The subsequent for loop iterates through the sorted array once, performing constant-time operations (min, max, comparisons, and arithmetic operations) for each element, 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 uses only a constant amount of extra variables (ans, pre, cur, x), the sorting operation typically requires O(log n) space for the recursion stack in efficient sorting algorithms like Timsort (used by Python's built-in sort()). Therefore, the space complexity is O(log n).

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

Common Pitfalls

Pitfall 1: Not Sorting the Array First

A common mistake is attempting to process elements in their original order without sorting. This leads to suboptimal results because you might "waste" the modification range of earlier elements, blocking later elements from achieving distinctness.

Incorrect approach:

def maxDistinctElements(self, nums: List[int], k: int) -> int:
    seen = set()
    count = 0
    for num in nums:  # Processing in original order
        for delta in range(-k, k + 1):
            if num + delta not in seen:
                seen.add(num + delta)
                count += 1
                break
    return count

Why it fails: Consider nums = [4, 1] with k = 2. Processing 4 first might assign it value 2, then 1 could only take values [-1, 0, 3] (since 2 is taken), giving us 2 distinct elements. But if we sort first to get [1, 4], we can assign 1 → -1 and 4 → 2, still getting 2 distinct elements but with a systematic approach that scales better.

Pitfall 2: Using a Set to Track All Possible Values

Another mistake is trying to generate all possible values for each element and using set operations to find the maximum distinct count. This approach is inefficient and can lead to incorrect results.

Incorrect approach:

def maxDistinctElements(self, nums: List[int], k: int) -> int:
    used = set()
    nums.sort()
    for num in nums:
        for val in range(num - k, num + k + 1):
            if val not in used:
                used.add(val)
                break
    return len(used)

Why it's problematic: While this might work, it's inefficient (O(n*k) in the worst case) and doesn't leverage the key insight that we should greedily assign the smallest available value to maximize future options.

Pitfall 3: Incorrect Boundary Calculation

A subtle but critical error is miscalculating the boundaries when determining the optimal value for each element.

Incorrect calculation:

# Wrong: might assign values outside the valid range
current_value = max(num - k, previous_value + 1)  # Missing upper bound check!

Correct calculation:

# Correct: ensures we stay within [num - k, num + k]
current_value = min(num + k, max(num - k, previous_value + 1))

The min(num + k, ...) is crucial because even if we want to assign previous_value + 1, we cannot exceed num + k.

Solution to Avoid These Pitfalls:

  1. Always sort first: This enables the greedy approach to work optimally
  2. Use the greedy assignment formula: min(num + k, max(num - k, previous_value + 1))
  3. Track only the previous assigned value: No need for sets or complex data structures
  4. Test with edge cases: Arrays with duplicate elements, large gaps between elements, and cases where k is very small or very large
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What data structure does Breadth-first search typically uses to store intermediate states?


Recommended Readings

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

Load More