Given an array of integers A
, your task is to partition it into two (contiguous) subarrays left
and right
such that:
left
is less than or equal to every element in right
.left
and right
are non-empty.left
and right
together cover all elements of A
(i.e., they are a partition).
Return the smallest possible length of left
after such a partitioning. The problem guarantees that there is at least one valid answer.
Key Constraints:
left
and right
.left
consists of A[0]
to A[i]
for some i
, and right
is A[i+1]
to A[n-1]
.At first glance, the problem seems to suggest checking every possible partition point and testing if all elements to the left are less than or equal to all elements to the right. This brute-force method would work, but it’s inefficient for large arrays.
To optimize, we recognize that for a partition at index i
:
left
(i.e., max(A[0..i])
) must be less than or equal to the minimum value in right
(i.e., min(A[i+1..n-1])
).i
that works.
The challenge is to avoid recomputing maximums and minimums for every possible split, which would be slow. Instead, we can precompute prefix maximums and suffix minimums, allowing constant-time checks at each partition point.
We solve the problem efficiently by precomputing useful information:
i
, compute the largest value in A[0..i]
. This tells us the maximum in left
if we partition after i
.
i
, compute the smallest value in A[i..n-1]
. This tells us the minimum in right
if we partition after i
.
right
must be non-empty). For each i
, check if prefix_max[i] ≤ suffix_min[i+1]
. The smallest such i+1
is our answer.
Why this works: By precomputing prefix maximums and suffix minimums, we can check the partition condition in constant time at every potential split point, ensuring efficiency.
Consider A = [5, 0, 3, 8, 6]
.
prefix_max = [5, 5, 5, 8, 8]
suffix_min = [0, 0, 3, 6, 6]
[5, 0, 3]
(length 3).
The key insight is to recognize that a valid partition requires the maximum of the left to be less than or equal to the minimum of the right. By precomputing prefix maximums and suffix minimums, we can efficiently check all possible split points in O(n) time. This approach is elegant, easy to implement, and leverages common algorithmic techniques like prefix and suffix computations.
class Solution:
def partitionDisjoint(self, A):
n = len(A)
prefix_max = [0] * n
suffix_min = [0] * n
prefix_max[0] = A[0]
for i in range(1, n):
prefix_max[i] = max(prefix_max[i-1], A[i])
suffix_min[-1] = A[-1]
for i in range(n-2, -1, -1):
suffix_min[i] = min(suffix_min[i+1], A[i])
for i in range(n-1):
if prefix_max[i] <= suffix_min[i+1]:
return i+1
class Solution {
public:
int partitionDisjoint(vector<int>& A) {
int n = A.size();
vector<int> prefix_max(n), suffix_min(n);
prefix_max[0] = A[0];
for (int i = 1; i < n; ++i)
prefix_max[i] = max(prefix_max[i-1], A[i]);
suffix_min[n-1] = A[n-1];
for (int i = n-2; i >= 0; --i)
suffix_min[i] = min(suffix_min[i+1], A[i]);
for (int i = 0; i < n-1; ++i)
if (prefix_max[i] <= suffix_min[i+1])
return i+1;
return -1; // Should never reach here
}
};
class Solution {
public int partitionDisjoint(int[] A) {
int n = A.length;
int[] prefixMax = new int[n];
int[] suffixMin = new int[n];
prefixMax[0] = A[0];
for (int i = 1; i < n; i++) {
prefixMax[i] = Math.max(prefixMax[i-1], A[i]);
}
suffixMin[n-1] = A[n-1];
for (int i = n-2; i >= 0; i--) {
suffixMin[i] = Math.min(suffixMin[i+1], A[i]);
}
for (int i = 0; i < n-1; i++) {
if (prefixMax[i] <= suffixMin[i+1]) {
return i+1;
}
}
return -1; // Should not reach here
}
}
var partitionDisjoint = function(A) {
const n = A.length;
const prefixMax = new Array(n);
const suffixMin = new Array(n);
prefixMax[0] = A[0];
for (let i = 1; i < n; i++) {
prefixMax[i] = Math.max(prefixMax[i-1], A[i]);
}
suffixMin[n-1] = A[n-1];
for (let i = n-2; i >= 0; i--) {
suffixMin[i] = Math.min(suffixMin[i+1], A[i]);
}
for (let i = 0; i < n-1; i++) {
if (prefixMax[i] <= suffixMin[i+1]) {
return i+1;
}
}
};