Facebook Pixel

915. Partition Array into Disjoint Intervals

Problem Description

You are given an integer array nums. Your task is to split this array into two contiguous parts called left and right subarrays such that:

  1. Every element in the left subarray must be less than or equal to every element in the right subarray. This means the maximum value in left should not exceed the minimum value in right.

  2. Both subarrays must be non-empty. You cannot have an empty left or right subarray.

  3. The left subarray should have the smallest possible size while still satisfying the above conditions.

Your goal is to return the length of the left subarray after performing this partition.

For example, if nums = [5, 0, 3, 8, 6], one valid partition would be left = [5, 0, 3] and right = [8, 6]. Here, the maximum of left is 5, and the minimum of right is 6. Since 5 ≤ 6, this is a valid partition. The function should return 3 (the length of the left subarray).

The problem guarantees that at least one valid partition exists for all test cases.

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

Intuition

To find the partition point, we need to ensure that the maximum value in the left subarray is less than or equal to the minimum value in the right subarray. This gives us the key insight: at any potential partition point i, we need to check if max(nums[0...i-1]) <= min(nums[i...n-1]).

If we try to check this condition naively for each possible partition point, we would need to compute the maximum of the left part and minimum of the right part repeatedly, which would be inefficient.

The clever observation is that we can precompute these values:

  • As we move the partition point from left to right, the maximum of the left part can only increase or stay the same (since we're adding more elements to consider)
  • For the minimum of the right part at each position, we can precompute all these values in advance by traversing the array from right to left

This leads us to a two-pass approach:

  1. First pass (right to left): Build an array mi where mi[i] stores the minimum value from index i to the end of the array. This tells us "if we start the right subarray at position i, what would be its minimum value?"

  2. Second pass (left to right): Keep track of the running maximum mx as we expand the left subarray. At each position i, we check if the current maximum of left (which ends at position i-1) is less than or equal to the minimum of right (which starts at position i). The first position where this condition holds gives us the smallest valid left subarray.

The beauty of this approach is that we only need O(n) time and O(n) space to solve the problem, making it very efficient.

Solution Approach

The solution implements the prefix maximum and suffix minimum approach to find the optimal partition point.

Step 1: Build the Suffix Minimum Array

mi = [inf] * (n + 1)
for i in range(n - 1, -1, -1):
    mi[i] = min(nums[i], mi[i + 1])

We create an array mi of size n + 1 where mi[i] stores the minimum value from index i to the end of the array. We initialize it with inf (infinity) values and traverse the array from right to left:

  • mi[n] = inf (serves as a base case)
  • mi[i] = min(nums[i], mi[i + 1]) for each position i

For example, if nums = [5, 0, 3, 8, 6], the mi array would be [0, 0, 3, 6, 6, inf].

Step 2: Find the Partition Point

mx = 0
for i, v in enumerate(nums, 1):
    mx = max(mx, v)
    if mx <= mi[i]:
        return i

We traverse the array from left to right, maintaining the running maximum mx of the elements seen so far (which represents the maximum of the left subarray):

  • We use enumerate(nums, 1) to iterate with indices starting from 1, since i represents the length of the left subarray
  • For each element v at position i-1, we update mx = max(mx, v)
  • We check if mx <= mi[i], which means the maximum of left[0...i-1] is less than or equal to the minimum of right[i...n-1]
  • The first position where this condition is satisfied gives us the smallest valid left subarray

Why This Works:

The algorithm guarantees to find the smallest valid partition because:

  1. We check positions from left to right, so the first valid partition we find has the minimum size for the left subarray
  2. The condition mx <= mi[i] ensures that every element in left is less than or equal to every element in right
  3. The problem guarantees that a valid partition exists, so we will always find and return a result

Time and Space Complexity:

  • Time: O(n) - two passes through the array
  • Space: O(n) - for the suffix minimum array

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 solution with nums = [5, 0, 3, 8, 6].

Step 1: Build the Suffix Minimum Array

We create array mi to store the minimum value from each index to the end:

  • Start with mi = [inf, inf, inf, inf, inf, inf] (size n+1 = 6)
  • Traverse right to left:
    • i=4: mi[4] = min(nums[4], mi[5]) = min(6, inf) = 6
    • i=3: mi[3] = min(nums[3], mi[4]) = min(8, 6) = 6
    • i=2: mi[2] = min(nums[2], mi[3]) = min(3, 6) = 3
    • i=1: mi[1] = min(nums[1], mi[2]) = min(0, 3) = 0
    • i=0: mi[0] = min(nums[0], mi[1]) = min(5, 0) = 0
  • Result: mi = [0, 0, 3, 6, 6, inf]

Step 2: Find the Partition Point

Traverse left to right, tracking the maximum of the left subarray:

  • i=1 (considering nums[0]=5):

    • Update mx = max(0, 5) = 5
    • Check: Is mx <= mi[1]? Is 5 <= 0? No, continue
  • i=2 (considering nums[1]=0):

    • Update mx = max(5, 0) = 5
    • Check: Is mx <= mi[2]? Is 5 <= 3? No, continue
  • i=3 (considering nums[2]=3):

    • Update mx = max(5, 3) = 5
    • Check: Is mx <= mi[3]? Is 5 <= 6? Yes!
    • Return 3

Result Verification:

  • left = [5, 0, 3] has maximum value 5
  • right = [8, 6] has minimum value 6
  • Since 5 ≤ 6, this is a valid partition
  • The length of left is 3, which is our answer

Solution Implementation

1class Solution:
2    def partitionDisjoint(self, nums: List[int]) -> int:
3        """
4        Partition array into two disjoint subarrays where all elements in the left
5        subarray are less than or equal to all elements in the right subarray.
6        Returns the length of the left subarray.
7        """
8        n = len(nums)
9      
10        # Build suffix minimum array
11        # min_suffix[i] stores the minimum value from index i to the end
12        min_suffix = [float('inf')] * (n + 1)
13        for i in range(n - 1, -1, -1):
14            min_suffix[i] = min(nums[i], min_suffix[i + 1])
15      
16        # Track maximum value in the left partition
17        max_left = 0
18      
19        # Iterate through possible partition points
20        # i represents the size of the left partition (1-indexed)
21        for i, value in enumerate(nums, 1):
22            # Update maximum in left partition
23            max_left = max(max_left, value)
24          
25            # Check if all elements in left partition <= all elements in right partition
26            # This is true when max of left partition <= min of right partition
27            if max_left <= min_suffix[i]:
28                return i
29
1class Solution {
2    public int partitionDisjoint(int[] nums) {
3        int n = nums.length;
4      
5        // Build array to store minimum values from right
6        // minFromRight[i] represents the minimum value in nums from index i to the end
7        int[] minFromRight = new int[n + 1];
8        minFromRight[n] = nums[n - 1];
9      
10        // Populate minFromRight array by iterating from right to left
11        for (int i = n - 1; i >= 0; i--) {
12            minFromRight[i] = Math.min(nums[i], minFromRight[i + 1]);
13        }
14      
15        // Track the maximum value in the left partition
16        int maxInLeft = 0;
17      
18        // Find the partition point where all elements in left partition
19        // are less than or equal to all elements in right partition
20        for (int i = 1; ; i++) {
21            int currentValue = nums[i - 1];
22            maxInLeft = Math.max(maxInLeft, currentValue);
23          
24            // If max in left partition is <= min in right partition, we found the answer
25            if (maxInLeft <= minFromRight[i]) {
26                return i;  // Return the size of left partition
27            }
28        }
29    }
30}
31
1class Solution {
2public:
3    int partitionDisjoint(vector<int>& nums) {
4        int n = nums.size();
5      
6        // Build an array to store the minimum value from each position to the end
7        // minFromRight[i] represents the minimum value in nums[i..n-1]
8        vector<int> minFromRight(n + 1, INT_MAX);
9      
10        // Populate minFromRight array from right to left
11        for (int i = n - 1; i >= 0; --i) {
12            minFromRight[i] = min(nums[i], minFromRight[i + 1]);
13        }
14      
15        // Track the maximum value in the left partition
16        int maxInLeft = 0;
17      
18        // Iterate through possible partition points
19        // We need at least one element in the left partition, so start from index 1
20        for (int i = 1; ; ++i) {
21            // Update the maximum value in the left partition
22            int currentValue = nums[i - 1];
23            maxInLeft = max(maxInLeft, currentValue);
24          
25            // Check if all elements in left partition <= all elements in right partition
26            // This happens when max(left) <= min(right)
27            if (maxInLeft <= minFromRight[i]) {
28                return i;  // Return the size of the left partition
29            }
30        }
31    }
32};
33
1function partitionDisjoint(nums: number[]): number {
2    const n = nums.length;
3  
4    // Build an array to store the minimum value from each position to the end
5    // minFromRight[i] represents the minimum value in nums[i..n-1]
6    const minFromRight: number[] = new Array(n + 1).fill(Number.MAX_SAFE_INTEGER);
7  
8    // Populate minFromRight array from right to left
9    for (let i = n - 1; i >= 0; i--) {
10        minFromRight[i] = Math.min(nums[i], minFromRight[i + 1]);
11    }
12  
13    // Track the maximum value in the left partition
14    let maxInLeft = 0;
15  
16    // Iterate through possible partition points
17    // We need at least one element in the left partition, so start from index 1
18    for (let i = 1; ; i++) {
19        // Update the maximum value in the left partition
20        const currentValue = nums[i - 1];
21        maxInLeft = Math.max(maxInLeft, currentValue);
22      
23        // Check if all elements in left partition <= all elements in right partition
24        // This happens when max(left) <= min(right)
25        if (maxInLeft <= minFromRight[i]) {
26            return i;  // Return the size of the left partition
27        }
28    }
29}
30

Time and Space Complexity

Time Complexity: O(n)

The algorithm consists of two main phases:

  1. Building the suffix minimum array: The first loop iterates from index n-1 down to 0, performing a constant-time operation (min comparison) at each step. This takes O(n) time.
  2. Finding the partition point: The second loop iterates through the array once using enumerate, performing constant-time operations (max comparison and condition check) at each step. This also takes O(n) time.

Since both loops run sequentially and each performs O(n) operations, the overall time complexity is O(n) + O(n) = O(n).

Space Complexity: O(n)

The algorithm uses additional space for:

  • The mi array of size n + 1 to store suffix minimums, which requires O(n) space
  • A few scalar variables (n, mx, i, v) which require O(1) space

Therefore, the total space complexity is O(n) + O(1) = O(n), where n is the length of the array nums.

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

Common Pitfalls

1. Off-by-One Error in Index Management

A common mistake is confusion between the partition index and the length of the left subarray. Since we're using enumerate(nums, 1), the variable i represents the length of the left subarray, not the index of the current element.

Incorrect Implementation:

for i, value in enumerate(nums):  # i starts from 0
    max_left = max(max_left, value)
    if max_left <= min_suffix[i]:  # Wrong! This checks wrong boundary
        return i  # Returns 0-based index, not length

Why it's wrong: When i = 0, we're checking if the first element alone can be the left subarray, but min_suffix[0] includes that same element in the minimum calculation, leading to incorrect comparisons.

Correct Implementation:

for i, value in enumerate(nums, 1):  # i starts from 1
    max_left = max(max_left, value)
    if max_left <= min_suffix[i]:  # Correctly checks min of remaining elements
        return i  # Returns the length of left subarray

2. Incorrect Initialization of Maximum Value

Incorrect Implementation:

max_left = float('-inf')  # Wrong initialization
for i, value in enumerate(nums, 1):
    max_left = max(max_left, value)
    if max_left <= min_suffix[i]:
        return i

Why it's wrong: If the first element is negative, initializing with negative infinity could cause issues. However, initializing with 0 when all array elements are negative would also be incorrect.

Correct Implementation:

max_left = float('-inf')  # Actually correct for general case
# OR more explicitly:
max_left = nums[0]
for i in range(1, n):
    if max_left <= min_suffix[i]:
        return i
    max_left = max(max_left, nums[i])

3. Forgetting Edge Case Validation

While the problem guarantees a valid partition exists, in practice you might want to handle edge cases:

Defensive Implementation:

def partitionDisjoint(self, nums: List[int]) -> int:
    if not nums or len(nums) < 2:
        return -1  # or raise an exception
  
    n = len(nums)
    min_suffix = [float('inf')] * (n + 1)
    # ... rest of the solution

4. Using Wrong Comparison in Suffix Array Building

Incorrect Implementation:

# Building suffix minimum incorrectly
for i in range(n):
    min_suffix[i] = min(nums[i], min_suffix[i - 1])  # Wrong direction!

Correct Implementation:

# Build from right to left
for i in range(n - 1, -1, -1):
    min_suffix[i] = min(nums[i], min_suffix[i + 1])

The key insight is that suffix minimum must be built from right to left to correctly capture the minimum of all elements from position i to the end.

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

A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".

What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?


Recommended Readings

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

Load More