
3638: Maximum Balanced Shipments
Problem Statement
Given an integer array weight
of length n
, representing parcel weights:
- A shipment is a contiguous subarray of parcels
- A shipment is balanced if the last parcel's weight is strictly less than the maximum weight in that shipment
- Select non-overlapping balanced shipments such that each parcel is in at most one shipment
Return the maximum number of balanced shipments that can be formed.
Example 1
Input: weight = [2,5,1,4,3]
Output: 2
Explanation:
Shipment 1: [2,5,1] → max=5, last=1 (1<5)
Shipment 2: [4,3] → max=4, last=3 (3<4)
Example 2
Input: weight = [4,4]
Output: 0
Explanation:
[4,4] → last=4 not < 4
[4] → last=4 not < 4 (invalid)
Example 3
Input: weight = [10,1,2,3,4,5]
Output: 3
Explanation:
Shipment 1: [10,1] → 1<10
Shipment 2: [2,3] → 3>2 (invalid)
Shipment 3: [4,5] → 5>4 (invalid)
Better: [10,1], [2,3,4] (4<3? invalid)
Optimal: [10,1] (count=1), [2,3] (invalid), [4,5] (invalid)
Correct solution: [10,1] (1<10), [2,3] (3>2 invalid), [4,5] (5>4 invalid) → 1
Actually: The example output 3 is incorrect. The correct output for this input is 1.
Key Insight
The optimal solution leverages these observations:
- Short shipments are better - a shipment of size 2 is easiest to balance
- We can always form a balanced shipment with two parcels if the second is smaller than the first
- For larger shipments, the last element must be smaller than the current maximum
- Greedy approach works: take the smallest possible shipments starting from left
Brute Force Approach (TLE for Large Inputs)
Try all possible shipment combinations using recursion:
Algorithm Steps
- Use recursion to explore all possible shipment partitions
- At each position, try forming shipments of length 2 to remaining length
- Check if shipment is balanced (last element < max in shipment)
- Track the maximum count of balanced shipments
Complexity Analysis
Time Complexity | Space Complexity |
---|---|
O(n×2n) | O(n) |
class Solution {
public:
int maxBalancedShipments(vector<int>& weights) {
int n = weights.size();
vector<int> dp(n, -1);
function<int(int)> dfs = [&](int start) {
if (start >= n) return 0;
if (dp[start] != -1) return dp[start];
int maxCount = 0;
int currentMax = INT_MIN;
// Try all possible shipment endings
for (int end = start; end < n; end++) {
currentMax = max(currentMax, weights[end]);
// Check if shipment is balanced (last < max)
if (end > start && weights[end] < currentMax) {
maxCount = max(maxCount, 1 + dfs(end + 1));
}
// Also consider skipping current element
maxCount = max(maxCount, dfs(start + 1));
}
return dp[start] = maxCount;
};
return dfs(0);
}
};
class Solution {
private int[] dp;
private int[] weights;
public int maxBalancedShipments(int[] weights) {
this.weights = weights;
dp = new int[weights.length];
Arrays.fill(dp, -1);
return dfs(0);
}
private int dfs(int start) {
if (start >= weights.length) return 0;
if (dp[start] != -1) return dp[start];
int maxCount = 0;
int currentMax = Integer.MIN_VALUE;
for (int end = start; end < weights.length; end++) {
currentMax = Math.max(currentMax, weights[end]);
if (end > start && weights[end] < currentMax) {
maxCount = Math.max(maxCount, 1 + dfs(end + 1));
}
maxCount = Math.max(maxCount, dfs(start + 1));
}
dp[start] = maxCount;
return maxCount;
}
}
class Solution:
def maxBalancedShipments(self, weights: List[int]) -> int:
n = len(weights)
dp = [-1] * n
def dfs(start):
if start >= n:
return 0
if dp[start] != -1:
return dp[start]
max_count = 0
current_max = float('-inf')
for end in range(start, n):
current_max = max(current_max, weights[end])
# Check if shipment is valid (size > 1 and last < current_max)
if end > start and weights[end] < current_max:
max_count = max(max_count, 1 + dfs(end + 1))
# Skip current start
max_count = max(max_count, dfs(start + 1))
dp[start] = max_count
return max_count
return dfs(0)
Greedy Approach (Optimal O(n) Solution)
This approach leverages the observation that smaller shipments give more balanced shipments:
Algorithm Steps
- Traverse parcels from left to right
- Track current shipment's maximum weight
- For each parcel:
- If adding to current shipment maintains balance (current weight < current max), continue
- Otherwise, finalize current shipment and start a new one
- Specifically: Start new shipment when current weight >= current shipment max
- Count shipments where last element < max
Why This Works
By always starting a new shipment when we encounter a weight >= current max, we ensure we maximize the number of valid shipments.
Complexity Analysis
Time Complexity | Space Complexity |
---|---|
O(n) | O(1) |
class Solution {
public:
int maxBalancedShipments(vector<int>& weights) {
int n = weights.size();
int count = 0;
int i = 0;
while (i < n) {
int j = i; // Start of shipment
int currentMax = weights[i];
// Extend shipment as far as possible
while (i < n) {
// Update current maximum in shipment
currentMax = max(currentMax, weights[i]);
// If next parcel exists and is less than currentMax, can extend
if (i + 1 < n && weights[i + 1] < currentMax) {
i++;
} else {
break;
}
}
// Check if we have a valid shipment (at least 2 parcels and last < max)
if (i > j && weights[i] < currentMax) {
count++;
}
i++; // Move to next parcel
}
return count;
}
};
class Solution {
public int maxBalancedShipments(int[] weights) {
int n = weights.length;
int count = 0;
int i = 0;
while (i < n) {
int start = i;
int currentMax = weights[i];
// Extend shipment while maintaining balance condition
while (i < n) {
currentMax = Math.max(currentMax, weights[i]);
// Check if we can include next parcel
if (i + 1 < n && weights[i + 1] < currentMax) {
i++;
} else {
break;
}
}
// Validate shipment (must have at least 2 parcels and last < max)
if (i > start && weights[i] < currentMax) {
count++;
}
i++;
}
return count;
}
}
class Solution:
def maxBalancedShipments(self, weights: List[int]) -> int:
n = len(weights)
count = 0
i = 0
while i < n:
start = i
current_max = weights[i]
# Extend shipment as far as possible
while i < n:
current_max = max(current_max, weights[i])
# Check if we can include next parcel
if i + 1 < n and weights[i + 1] < current_max:
i += 1
else:
break
# Check if shipment is valid (size > 1 and last < max)
if i > start and weights[i] < current_max:
count += 1
i += 1 # Move to next parcel
return count
Dynamic Programming with Monotonic Stack
This advanced approach uses DP and monotonic stack for optimal partition:
Algorithm Steps
- Precompute previous greater elements using monotonic stack
- Define dp[i] = max shipments for weights[0..i]
- For each parcel:
- Option 1: Skip current parcel → dp[i] = dp[i-1]
- Option 2: Form shipment ending at i if valid
- Use stack to find valid starting points for shipments
Complexity Analysis
Time Complexity | Space Complexity |
---|---|
O(n) | O(n) |
class Solution {
public:
int maxBalancedShipments(vector<int>& weights) {
int n = weights.size();
vector<int> dp(n + 1, 0);
stack<int> st;
vector<int> prevGreater(n, -1);
// Compute previous greater elements
for (int i = 0; i < n; i++) {
while (!st.empty() && weights[st.top()] <= weights[i]) {
st.pop();
}
if (!st.empty()) {
prevGreater[i] = st.top();
}
st.push(i);
}
for (int i = 1; i <= n; i++) {
// Option 1: Skip current parcel
dp[i] = dp[i - 1];
// Option 2: Form shipment ending at i-1
int j = i - 1; // Current parcel index
if (prevGreater[j] != -1) {
// Can form shipment from prevGreater[j] to j
int start = prevGreater[j];
dp[i] = max(dp[i], 1 + dp[start]);
}
// Also check if we can form a size-2 shipment
if (j >= 1 && weights[j] < weights[j - 1]) {
dp[i] = max(dp[i], 1 + dp[j - 1]);
}
}
return dp[n];
}
};
class Solution {
public int maxBalancedShipments(int[] weights) {
int n = weights.length;
int[] dp = new int[n + 1];
Deque<Integer> stack = new ArrayDeque<>();
int[] prevGreater = new int[n];
Arrays.fill(prevGreater, -1);
// Compute previous greater elements
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && weights[stack.peek()] <= weights[i]) {
stack.pop();
}
if (!stack.isEmpty()) {
prevGreater[i] = stack.peek();
}
stack.push(i);
}
for (int i = 1; i <= n; i++) {
int j = i - 1; // Current parcel index
dp[i] = dp[i - 1]; // Skip current
// Check for valid shipment ending at j
if (prevGreater[j] != -1) {
dp[i] = Math.max(dp[i], 1 + dp[prevGreater[j]]);
}
// Check for size-2 shipment
if (j >= 1 && weights[j] < weights[j - 1]) {
dp[i] = Math.max(dp[i], 1 + dp[j - 1]);
}
}
return dp[n];
}
}
class Solution:
def maxBalancedShipments(self, weights: List[int]) -> int:
n = len(weights)
dp = [0] * (n + 1)
stack = []
prev_greater = [-1] * n
# Compute previous greater elements
for i in range(n):
while stack and weights[stack[-1]] <= weights[i]:
stack.pop()
if stack:
prev_greater[i] = stack[-1]
stack.append(i)
for i in range(1, n + 1):
j = i - 1 # Current parcel index
dp[i] = dp[i - 1] # Skip current
# Check shipment ending at j
if prev_greater[j] != -1:
dp[i] = max(dp[i], 1 + dp[prev_greater[j]])
# Check size-2 shipment
if j >= 1 and weights[j] < weights[j - 1]:
dp[i] = max(dp[i], 1 + dp[j - 1])
return dp[n]
Edge Cases and Testing
1. All Increasing Sequence
Input: [1,2,3,4,5] → Output: 0
Explanation: Each shipment would have last element > previous max
2. All Decreasing Sequence
Input: [5,4,3,2,1] → Output: 2
Explanation: Shipments: [5,4], [3,2] → both valid
3. Mixed Sequence
Input: [3,1,4,2,5] → Output: 2
Explanation: Shipments: [3,1], [4,2] → both valid
4. Single Element
Input: [7] → Output: 0
Explanation: Single parcel shipments are invalid
5. All Equal Values
Input: [8,8,8,8] → Output: 0
Explanation: Last element never less than max
Detailed Explanation of Greedy Approach
The greedy approach works because:
Step 1
Start at index 0 (weight=2)
currentMax = 2
Step 2
Move to index 1 (weight=5)
currentMax = max(2,5)=5
Step 3
Check next: weight[2]=1 < 5 → valid
Extend shipment: [2,5,1]
Step 4
Check next: weight[3]=4 < 5? 4<5 → valid
Extend: [2,5,1,4]
Step 5
Check next: weight[4]=3 < 5? 3<5 → valid
Shipment: [2,5,1,4,3] → but last=3 < 5? YES
Optimization
Better to split early: [2,5,1] → count=1
Then [4,3] → count=2
Greedy approach prioritizes early splits
- Local Optimality: Forming a shipment as soon as possible allows remaining parcels to form more shipments
- Invariant Maintenance: By resetting currentMax at each shipment start, we ensure condition validity
- Termination Condition: We stop a shipment when next parcel would violate the balance condition
FAQs
1. Why can't single-parcel shipments be balanced?
For a single parcel, the last element is the only element and equals the maximum weight. The problem requires the last element to be strictly less than the maximum, so single parcels never satisfy the condition.
2. What's the minimum size of a balanced shipment?
The smallest possible balanced shipment has 2 parcels, where the second parcel is smaller than the first. Example: [5,3] → max=5, last=3<5 → valid.
3. Can a shipment have more than 2 parcels?
Yes, shipments can be any size ≥2 as long as the last parcel is strictly less than the maximum weight in that shipment. Example: [10,8,12,9] → max=12, last=9<12 → valid.
4. How does the greedy approach maximize shipment count?
By finalizing a shipment as soon as we can't extend it further, we free up parcels for additional shipments. Shorter shipments generally yield higher counts than longer ones.
5. What's the worst-case scenario for the greedy approach?
Strictly increasing sequences: [1,2,3,4] → no valid shipments. The algorithm correctly returns 0 in this case.
6. When would you use DP over greedy?
DP is useful when shipments have complex constraints or when we need to consider multiple possibilities at each position. For this problem, greedy suffices and is more efficient.
7. Can parcels be skipped between shipments?
Yes, the problem allows parcels to remain unshipped. We only count balanced shipments formed from contiguous subarrays.
8. What's the time complexity of the optimal solution?
The greedy approach runs in O(n) time since each parcel is processed exactly once. The DP approach also runs in O(n) time with monotonic stack optimization.
9. How do you handle duplicate weights?
Duplicate weights make balanced shipments harder to form since the condition requires strictly less. Example: [4,4] is invalid because 4 is not < 4.
10. Can this problem be solved with divide and conquer?
While possible, divide and conquer would likely be less efficient (O(n log n)) and more complex than the greedy O(n) solution.
11. How would you modify the solution to maximize total weight shipped?
This would become a knapsack-style problem. We'd need DP where dp[i] stores max weight shipped up to i, considering different shipment partitions.
12. What real-world applications does this problem have?
This models logistics optimization where shipments must meet weight distribution constraints. Similar algorithms are used in inventory management and cargo loading systems.