Facebook Pixel

2294. Partition Array Such That Maximum Difference Is K

Problem Description

You have an integer array nums and an integer k. Your task is to partition nums into one or more subsequences where:

  1. Each element in nums appears in exactly one subsequence
  2. Within each subsequence, the difference between the maximum value and minimum value must be at most k

A subsequence is a sequence derived from the original array by deleting some or no elements while maintaining the relative order of remaining elements. However, since we're partitioning the array, the elements don't need to be contiguous in the original array.

The goal is to find the minimum number of subsequences needed to satisfy these conditions.

For example, if nums = [3, 6, 1, 2, 5] and k = 2:

  • After sorting: [1, 2, 3, 5, 6]
  • We could partition into subsequences like [1, 2, 3] (max-min = 3-1 = 2 โ‰ค k) and [5, 6] (max-min = 6-5 = 1 โ‰ค k)
  • This gives us 2 subsequences, which would be the minimum needed

The solution uses a greedy approach after sorting. Since subsequences don't require contiguous elements, sorting helps group elements that are close in value. The algorithm tracks the starting element a of each subsequence and adds elements to it as long as the difference stays within k. When an element b is found where b - a > k, a new subsequence must start.

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

Intuition

The key insight is that since we're dealing with subsequences (not subarrays), we have the flexibility to pick any elements from the array in any combination. This means we can rearrange our thinking by sorting the array first without losing any valid partitioning options.

Why does sorting help? Consider any valid subsequence - its maximum and minimum values determine whether it's valid (their difference must be โ‰ค k). If we sort the array, elements with similar values are grouped together, making it easier to form valid subsequences.

Once sorted, we can think greedily: start with the smallest element and keep adding elements to the current subsequence as long as the difference between the current element and the starting element doesn't exceed k. Why is this greedy approach optimal?

  1. If we have a sorted sequence like [1, 2, 3, 5, 6, 9] and k = 2, starting from 1, we can include 2 and 3 (since 3 - 1 = 2 โ‰ค k).

  2. When we reach 5, we find 5 - 1 = 4 > k, so we must start a new subsequence. The crucial observation is: there's no benefit to ending the first subsequence earlier. Including 3 in the first subsequence doesn't prevent us from creating valid subsequences with the remaining elements.

  3. This "maximize each subsequence" strategy minimizes the total number of subsequences needed because we're fitting as many elements as possible into each subsequence before being forced to start a new one.

The algorithm essentially creates intervals of width at most k along the sorted array, where each interval represents a subsequence. The minimum number of such intervals needed to cover all elements gives us our answer.

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 groups similar values together and allows us to process elements in increasing order.

  2. Initialize tracking variables:

    • ans = 1: Start with one subsequence (we need at least one)
    • a = nums[0]: Track the minimum value of the current subsequence (which is the first element after sorting)
  3. Iterate through the sorted array: For each element b in the sorted array:

    • Check if b - a > k: This compares the current element with the minimum value of the current subsequence
    • If the difference exceeds k, we cannot include b in the current subsequence:
      • Start a new subsequence by setting a = b (the current element becomes the minimum of the new subsequence)
      • Increment ans by 1 to count the new subsequence
    • If the difference is within k, we continue with the current subsequence (no action needed)
  4. Return the result: After processing all elements, ans contains the minimum number of subsequences needed.

The algorithm's correctness relies on the fact that in a sorted array, if an element b cannot be included in a subsequence starting with a (because b - a > k), then no element after b can be included either (since they are all greater than or equal to b). This makes starting a new subsequence at b the optimal choice.

Time Complexity: O(n log n) due to sorting, where n is the length of the array. Space Complexity: O(1) if we don't count the space used by the sorting algorithm.

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 the algorithm with nums = [10, 1, 7, 3, 2, 6] and k = 3.

Step 1: Sort the array

  • Original: [10, 1, 7, 3, 2, 6]
  • Sorted: [1, 2, 3, 6, 7, 10]

Step 2: Initialize variables

  • ans = 1 (start with one subsequence)
  • a = 1 (first element becomes the minimum of the current subsequence)

Step 3: Process each element

Processing element at index 1: b = 2

  • Check: 2 - 1 = 1 โ‰ค 3 โœ“
  • Can include in current subsequence
  • Current subsequence range: [1, 2]

Processing element at index 2: b = 3

  • Check: 3 - 1 = 2 โ‰ค 3 โœ“
  • Can include in current subsequence
  • Current subsequence range: [1, 3]

Processing element at index 3: b = 6

  • Check: 6 - 1 = 5 > 3 โœ—
  • Cannot include in current subsequence
  • Start new subsequence: a = 6, ans = 2
  • First subsequence finalized: [1, 2, 3]
  • New subsequence starts: [6]

Processing element at index 4: b = 7

  • Check: 7 - 6 = 1 โ‰ค 3 โœ“
  • Can include in current subsequence
  • Current subsequence range: [6, 7]

Processing element at index 5: b = 10

  • Check: 10 - 6 = 4 > 3 โœ—
  • Cannot include in current subsequence
  • Start new subsequence: a = 10, ans = 3
  • Second subsequence finalized: [6, 7]
  • New subsequence starts: [10]

Step 4: Return result

  • Final answer: 3 subsequences
  • The subsequences are: [1, 2, 3], [6, 7], and [10]
  • Each satisfies the constraint that max - min โ‰ค 3

Solution Implementation

1class Solution:
2    def partitionArray(self, nums: List[int], k: int) -> int:
3        # Sort the array to group similar values together
4        nums.sort()
5      
6        # Initialize partition count to 1 (at least one partition needed)
7        partition_count = 1
8      
9        # Track the minimum value of the current partition
10        current_partition_min = nums[0]
11      
12        # Iterate through each number in the sorted array
13        for current_num in nums:
14            # If the difference between current number and partition minimum exceeds k,
15            # we need to start a new partition
16            if current_num - current_partition_min > k:
17                # Update the minimum value for the new partition
18                current_partition_min = current_num
19                # Increment the partition count
20                partition_count += 1
21      
22        return partition_count
23
1class Solution {
2    public int partitionArray(int[] nums, int k) {
3        // Sort the array to group similar values together
4        Arrays.sort(nums);
5      
6        // Initialize the number of partitions to 1 (at least one partition is needed)
7        int partitionCount = 1;
8      
9        // Track the minimum value of the current partition
10        int currentPartitionMin = nums[0];
11      
12        // Iterate through each element in the sorted array
13        for (int currentValue : nums) {
14            // Check if the difference between current value and partition's minimum exceeds k
15            if (currentValue - currentPartitionMin > k) {
16                // Start a new partition with current value as the minimum
17                currentPartitionMin = currentValue;
18                // Increment the partition count
19                partitionCount++;
20            }
21        }
22      
23        // Return the total number of partitions needed
24        return partitionCount;
25    }
26}
27
1class Solution {
2public:
3    int partitionArray(vector<int>& nums, int k) {
4        // Sort the array to group similar values together
5        sort(nums.begin(), nums.end());
6      
7        // Initialize partition count to 1 (at least one partition is needed)
8        int partitionCount = 1;
9      
10        // Track the minimum value of the current partition
11        int currentPartitionMin = nums[0];
12      
13        // Iterate through each element in the sorted array
14        for (int& currentNum : nums) {
15            // Check if current element exceeds the partition range
16            // If difference between current element and partition minimum > k,
17            // we need to start a new partition
18            if (currentNum - currentPartitionMin > k) {
19                // Start a new partition with current element as minimum
20                currentPartitionMin = currentNum;
21                // Increment the partition count
22                ++partitionCount;
23            }
24        }
25      
26        // Return the total number of partitions needed
27        return partitionCount;
28    }
29};
30
1/**
2 * Partitions an array into minimum number of subsequences where 
3 * the difference between maximum and minimum in each subsequence is at most k
4 * @param nums - The input array of numbers to partition
5 * @param k - The maximum allowed difference within each partition
6 * @returns The minimum number of partitions needed
7 */
8function partitionArray(nums: number[], k: number): number {
9    // Sort the array in ascending order to group similar values together
10    nums.sort((a: number, b: number) => a - b);
11  
12    // Initialize partition count to 1 (at least one partition is needed)
13    let partitionCount: number = 1;
14  
15    // Track the minimum value of the current partition
16    let currentPartitionMin: number = nums[0];
17  
18    // Iterate through each element in the sorted array
19    for (const currentValue of nums) {
20        // Check if current value exceeds the allowed difference from partition minimum
21        if (currentValue - currentPartitionMin > k) {
22            // Start a new partition with current value as the new minimum
23            currentPartitionMin = currentValue;
24            // Increment the partition count
25            partitionCount++;
26        }
27    }
28  
29    return partitionCount;
30}
31

Time and Space Complexity

Time Complexity: O(n ร— log n)

The time complexity is dominated by the sorting operation nums.sort(), which takes O(n ร— log n) time where n is the length of the array nums. After sorting, the algorithm performs a single pass through the array with a for loop, which takes O(n) time. Since O(n ร— log n) + O(n) = O(n ร— log n), the overall time complexity is O(n ร— log n).

Space Complexity: O(log n)

The space complexity comes from the sorting algorithm. Python's built-in sort() method uses Timsort, which requires O(log n) space in the worst case for its recursive calls and temporary storage. The algorithm itself only uses a constant amount of extra space for variables ans and a, which is O(1). Therefore, the total space complexity is O(log n).

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

Common Pitfalls

1. Forgetting to Sort the Array

One of the most common mistakes is attempting to solve this problem without sorting the array first. The greedy approach only works correctly on a sorted array.

Incorrect approach:

def partitionArray(self, nums: List[int], k: int) -> int:
    partition_count = 1
    current_min = nums[0]
  
    # Wrong: Processing unsorted array
    for num in nums:
        if num - current_min > k:
            current_min = num
            partition_count += 1
  
    return partition_count

Why it fails: Without sorting, you might incorrectly group elements. For example, with nums = [5, 1, 3] and k = 2, processing unsorted would create unnecessary partitions.

Solution: Always sort the array first before applying the greedy algorithm.

2. Incorrect Range Checking Logic

Another pitfall is checking the wrong condition or using the wrong comparison when determining if an element fits in the current subsequence.

Common mistakes:

# Mistake 1: Checking against maximum instead of minimum
if current_num - current_partition_max > k:  # Wrong!

# Mistake 2: Using >= instead of >
if current_num - current_partition_min >= k:  # Wrong!

Why it fails:

  • We need to check against the minimum of the current partition (not maximum) because in a sorted array, the current element is already the maximum.
  • Using >= would incorrectly start a new partition when the difference equals k, but the problem allows differences up to and including k.

Solution: Always check if current_num - current_partition_min > k (strictly greater than).

3. Misunderstanding the Problem - Thinking Subsequences Must Be Contiguous

Some might mistakenly think that subsequences need to consist of contiguous elements from the original array, leading to overly complex solutions.

Incorrect understanding might lead to:

# Trying to track indices or maintain original order unnecessarily
def partitionArray(self, nums: List[int], k: int) -> int:
    # Complex logic trying to maintain original indices...

Solution: Remember that after sorting, we're free to group any elements together as long as their max-min difference is at most k. The "subsequence" term in the problem refers to the partition groups, not to maintaining the original array order.

4. Edge Case: Empty Array or Single Element

While most implementations handle this naturally, explicitly checking for edge cases can prevent runtime errors.

Potential issue:

def partitionArray(self, nums: List[int], k: int) -> int:
    nums.sort()
    # What if nums is empty?
    current_partition_min = nums[0]  # IndexError if nums is empty

Solution: Add a guard clause or ensure the problem constraints guarantee non-empty arrays:

def partitionArray(self, nums: List[int], k: int) -> int:
    if not nums:
        return 0
  
    nums.sort()
    # ... rest of the code
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which data structure is used to implement priority queue?


Recommended Readings

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

Load More