Maximum Balanced Shipments visualization

3638: Maximum Balanced Shipments

Difficulty: Medium
Topics: Arrays, Greedy, Dynamic Programming, Monotonic Stack
Companies: Amazon, Google, Microsoft
Pro Tip: For contiguous subarray problems, consider greedy approaches first - they often provide efficient solutions with O(n) complexity.

Problem Statement

Given an integer array weight of length n, representing parcel weights:

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.

Problem Link: View on LeetCode ↗

Key Insight

The optimal solution leverages these observations:

  1. Short shipments are better - a shipment of size 2 is easiest to balance
  2. We can always form a balanced shipment with two parcels if the second is smaller than the first
  3. For larger shipments, the last element must be smaller than the current maximum
  4. Greedy approach works: take the smallest possible shipments starting from left
Important: The constraint (last element < max element) means single-element shipments are never valid since max = last.

Brute Force Approach (TLE for Large Inputs)

Try all possible shipment combinations using recursion:

Algorithm Steps

  1. Use recursion to explore all possible shipment partitions
  2. At each position, try forming shipments of length 2 to remaining length
  3. Check if shipment is balanced (last element < max in shipment)
  4. Track the maximum count of balanced shipments

Complexity Analysis

Time Complexity Space Complexity
O(n×2n) O(n)
Brute Force Solution (TLE)
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)
Warning: This solution will time out for large inputs (n > 1000) due to exponential time complexity.

Greedy Approach (Optimal O(n) Solution)

This approach leverages the observation that smaller shipments give more balanced shipments:

Algorithm Steps

  1. Traverse parcels from left to right
  2. Track current shipment's maximum weight
  3. For each parcel:
    • If adding to current shipment maintains balance (current weight < current max), continue
    • Otherwise, finalize current shipment and start a new one
  4. Specifically: Start new shipment when current weight >= current shipment max
  5. 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)
Greedy Solution (Optimal)
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
Efficiency: This solution processes each parcel exactly once, achieving optimal O(n) time complexity.

Dynamic Programming with Monotonic Stack

This advanced approach uses DP and monotonic stack for optimal partition:

Algorithm Steps

  1. Precompute previous greater elements using monotonic stack
  2. Define dp[i] = max shipments for weights[0..i]
  3. For each parcel:
    • Option 1: Skip current parcel → dp[i] = dp[i-1]
    • Option 2: Form shipment ending at i if valid
  4. Use stack to find valid starting points for shipments

Complexity Analysis

Time Complexity Space Complexity
O(n) O(n)
DP with Monotonic Stack
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]
Advanced Technique: This approach is useful for related problems like "Largest Rectangle in Histogram" and "Maximal Rectangle".

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
Testing Tip: Always test with sequences that have multiple valid shipment possibilities to ensure your solution finds the maximum count.

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

  1. Local Optimality: Forming a shipment as soon as possible allows remaining parcels to form more shipments
  2. Invariant Maintenance: By resetting currentMax at each shipment start, we ensure condition validity
  3. Termination Condition: We stop a shipment when next parcel would violate the balance condition
Why greedy works: The problem has optimal substructure - the best solution for n parcels contains optimal solutions for subarrays.

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.