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:
- Each element in
nums
appears in exactly one subsequence - 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.
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?
-
If we have a sorted sequence like
[1, 2, 3, 5, 6, 9]
andk = 2
, starting from1
, we can include2
and3
(since3 - 1 = 2 โค k
). -
When we reach
5
, we find5 - 1 = 4 > k
, so we must start a new subsequence. The crucial observation is: there's no benefit to ending the first subsequence earlier. Including3
in the first subsequence doesn't prevent us from creating valid subsequences with the remaining elements. -
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.
Solution Approach
The implementation follows a greedy approach with sorting:
-
Sort the array: First, we sort
nums
in ascending order. This groups similar values together and allows us to process elements in increasing order. -
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)
-
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 includeb
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
- Start a new subsequence by setting
- If the difference is within
k
, we continue with the current subsequence (no action needed)
- Check if
-
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 EvaluatorExample 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 equalsk
, but the problem allows differences up to and includingk
.
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
Which data structure is used to implement priority queue?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Donโt Miss This!