Facebook Pixel

3523. Make Array Non-decreasing

Problem Description

You are given an integer array nums. You can perform operations on this array where each operation allows you to select any contiguous subarray and replace all elements in that subarray with a single element equal to the maximum value in that subarray.

For example, if you have array [1, 3, 2, 4] and select subarray [3, 2], you can replace it with [3] (the maximum of 3 and 2), resulting in [1, 3, 4].

Your goal is to find the maximum possible size (number of elements) of the array after performing zero or more such operations, with the constraint that the resulting array must be non-decreasing (each element is greater than or equal to the previous element).

The solution works by iterating through the array and keeping track of the maximum value seen so far (mx). For each element x:

  • If x is greater than or equal to the current maximum, we can keep this element in our final array (increment ans) and update our maximum
  • If x is less than the current maximum, we cannot include it as a separate element without violating the non-decreasing property, so it would need to be merged with previous elements

This greedy approach ensures we keep the maximum number of elements possible while maintaining a non-decreasing sequence.

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

Intuition

The key insight is to think about when we can keep an element as a separate value in the final non-decreasing array versus when we must merge it with previous elements.

Consider walking through the array from left to right. At each position, we face a decision: can we keep this element as its own value in the final array, or must we merge it with what came before?

For the array to be non-decreasing, each element must be greater than or equal to all elements before it. When we perform the "replace subarray with its maximum" operation, we're essentially taking a group of elements and replacing them with their maximum value.

Here's the crucial observation: if we encounter an element x that is greater than or equal to all previous elements (or specifically, the maximum of all previous elements), we can safely keep it as a separate element in our final array. Why? Because it already satisfies the non-decreasing property.

However, if we encounter an element x that is smaller than some previous element, we cannot keep it as a separate element. It would have to be merged with previous elements through an operation, and when merged, it would contribute nothing to the final array size (since the maximum of the merged subarray would be determined by the larger previous element, not by x).

This leads to a greedy strategy: traverse the array while maintaining the maximum value seen so far. Whenever we find an element that is at least as large as this maximum, we can keep it (increasing our answer count by 1) and update our maximum. Elements smaller than the current maximum are "absorbed" into previous operations and don't contribute to the final count.

This approach maximizes the array size because we're keeping every element that can possibly be kept without violating the non-decreasing constraint.

Learn more about Stack, Greedy and Monotonic Stack patterns.

Solution Approach

The implementation uses a simple greedy algorithm with a single pass through the array. Here's how the solution works:

Algorithm Steps:

  1. Initialize two variables:

    • ans = 0: Counts the maximum possible size of the final array
    • mx = 0: Tracks the maximum value seen so far
  2. Iterate through each element x in the array:

    • Check if mx <= x (current element is greater than or equal to the maximum seen so far)
    • If true:
      • Increment ans by 1 (we can keep this element in the final array)
      • Update mx = x (this becomes our new maximum)
    • If false:
      • Do nothing (this element will be merged with previous elements)
  3. Return ans as the maximum possible size

Why this works:

When we encounter an element x where mx <= x, it means this element can exist independently in the final non-decreasing array because it's already larger than everything before it.

When we encounter an element where mx > x, this element cannot exist independently as it would violate the non-decreasing property. It must be merged with some previous subarray, and since the maximum of that merged subarray would be at least mx (not x), this element doesn't contribute to the final count.

Time and Space Complexity:

  • Time Complexity: O(n) where n is the length of the input array, as we make a single pass
  • Space Complexity: O(1) as we only use two variables regardless of input size

Example walkthrough:

For nums = [2, 1, 3, 2, 4]:

  • Start: ans = 0, mx = 0
  • x = 2: 0 <= 2, so ans = 1, mx = 2
  • x = 1: 2 > 1, skip (will be merged)
  • x = 3: 2 <= 3, so ans = 2, mx = 3
  • x = 2: 3 > 2, skip (will be merged)
  • x = 4: 3 <= 4, so ans = 3, mx = 4
  • Return 3

The final array could be [2, 3, 4] after merging appropriate subarrays.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the algorithm with the array nums = [1, 4, 2, 3, 5, 1]:

Initial State:

  • ans = 0 (count of elements we can keep)
  • mx = 0 (maximum value seen so far)

Step-by-step iteration:

  1. Process element 1:

    • Check: Is mx <= 1? Yes, 0 <= 1
    • Action: Keep this element
    • Update: ans = 1, mx = 1
    • Array so far: [1]
  2. Process element 4:

    • Check: Is mx <= 4? Yes, 1 <= 4
    • Action: Keep this element
    • Update: ans = 2, mx = 4
    • Array so far: [1, 4]
  3. Process element 2:

    • Check: Is mx <= 2? No, 4 > 2
    • Action: Skip (this element must be merged with previous elements)
    • No update: ans = 2, mx = 4
    • Note: Element 2 would be absorbed into a merge operation
  4. Process element 3:

    • Check: Is mx <= 3? No, 4 > 3
    • Action: Skip (this element must be merged with previous elements)
    • No update: ans = 2, mx = 4
    • Note: Element 3 would also be absorbed
  5. Process element 5:

    • Check: Is mx <= 5? Yes, 4 <= 5
    • Action: Keep this element
    • Update: ans = 3, mx = 5
    • Array so far: [1, 4, 5]
  6. Process element 1:

    • Check: Is mx <= 1? No, 5 > 1
    • Action: Skip (this element must be merged)
    • No update: ans = 3, mx = 5

Final Result: ans = 3

The maximum possible size of the array is 3. One possible final non-decreasing array would be [1, 4, 5], achieved by:

  • Keeping the first 1 as-is
  • Merging [4, 2, 3] and replacing with 4 (the maximum)
  • Merging [5, 1] and replacing with 5 (the maximum)

This demonstrates how the greedy algorithm identifies exactly which elements can be preserved as separate values in the final non-decreasing array.

Solution Implementation

1class Solution:
2    def maximumPossibleSize(self, nums: List[int]) -> int:
3        # Initialize counter for valid elements and tracking maximum
4        count = 0
5        current_max = 0
6      
7        # Iterate through each number in the input list
8        for num in nums:
9            # Check if current number is greater than or equal to the tracked maximum
10            if current_max <= num:
11                # Increment count as this element contributes to the result
12                count += 1
13                # Update the maximum value seen so far
14                current_max = num
15      
16        # Return the total count of valid elements
17        return count
18
1class Solution {
2    public int maximumPossibleSize(int[] nums) {
3        // Count of elements that are >= all previous elements
4        int count = 0;
5      
6        // Track the maximum value seen so far
7        int maxSoFar = 0;
8      
9        // Iterate through each element in the array
10        for (int currentNum : nums) {
11            // If current element is >= the maximum seen so far,
12            // it contributes to our count
13            if (currentNum >= maxSoFar) {
14                count++;
15                // Update the maximum value seen so far
16                maxSoFar = currentNum;
17            }
18        }
19      
20        // Return the total count of qualifying elements
21        return count;
22    }
23}
24
1class Solution {
2public:
3    int maximumPossibleSize(vector<int>& nums) {
4        // Initialize counter for the maximum possible size
5        int count = 0;
6      
7        // Track the maximum value seen so far
8        int maxValue = 0;
9      
10        // Iterate through each number in the array
11        for (int num : nums) {
12            // If current number is greater than or equal to the max seen so far
13            if (maxValue <= num) {
14                // Increment the count (include this element)
15                ++count;
16              
17                // Update the maximum value to current number
18                maxValue = num;
19            }
20        }
21      
22        // Return the maximum possible size
23        return count;
24    }
25};
26
1/**
2 * Calculates the maximum possible size of a subsequence where each element
3 * is greater than or equal to all previous elements in the subsequence
4 * @param nums - Array of numbers to process
5 * @returns The maximum possible size of the subsequence
6 */
7function maximumPossibleSize(nums: number[]): number {
8    // Initialize counter for subsequence size and current maximum value
9    let subsequenceSize: number = 0;
10    let currentMaximum: number = 0;
11  
12    // Iterate through each number in the array
13    for (const currentNumber of nums) {
14        // If current number is greater than or equal to the current maximum,
15        // it can be added to the subsequence
16        if (currentMaximum <= currentNumber) {
17            // Increment the subsequence size
18            subsequenceSize++;
19            // Update the current maximum to the current number
20            currentMaximum = currentNumber;
21        }
22    }
23  
24    // Return the final subsequence size
25    return subsequenceSize;
26}
27

Time and Space Complexity

Time Complexity: O(n), where n is the length of the input array nums. The algorithm iterates through the array exactly once with a single for loop, performing constant-time operations (comparison, addition, and assignment) for each element.

Space Complexity: O(1). The algorithm uses only a constant amount of extra space regardless of the input size. It maintains three variables (ans, mx, and the loop variable x) which do not scale with the input size.

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

Common Pitfalls

1. Misunderstanding the Problem: Thinking We Need to Actually Perform Operations

Pitfall: A common mistake is attempting to actually modify the array and perform the merge operations, tracking which subarrays to combine and maintaining the resulting array structure. This leads to unnecessarily complex code with nested loops and array manipulations.

Why it happens: The problem statement describes operations on subarrays, which might lead developers to think they need to simulate these operations.

Correct approach: The key insight is that we only need to count how many elements can remain in the final non-decreasing array. We don't need to track which specific operations to perform or maintain the actual resulting array.

2. Incorrect Initial Value for Maximum

Pitfall: Initializing mx with the first element of the array or using float('-inf') instead of 0.

# Incorrect approach
def maximumPossibleSize(self, nums: List[int]) -> int:
    count = 1  # Start with 1 assuming first element is always valid
    current_max = nums[0]  # Wrong initialization
  
    for i in range(1, len(nums)):
        if current_max <= nums[i]:
            count += 1
            current_max = nums[i]
  
    return count

Why it's wrong: This approach fails for edge cases like empty arrays or assumes the first element is always included, which may not be optimal. Starting with mx = 0 allows the algorithm to work correctly even if all elements are positive.

Solution: Always initialize with mx = 0 and count = 0, letting the algorithm naturally handle all elements including the first one.

3. Using Strict Inequality Instead of Less-Than-or-Equal

Pitfall: Using mx < x instead of mx <= x in the comparison.

# Incorrect comparison
if current_max < num:  # Wrong: should be <=
    count += 1
    current_max = num

Why it's wrong: The problem asks for a non-decreasing array, which means equal consecutive elements are allowed. Using strict inequality would incorrectly skip elements that are equal to the current maximum.

Example where it fails: For nums = [1, 2, 2, 3], using < would give result 3 instead of the correct 4.

4. Overthinking with Dynamic Programming or Complex Data Structures

Pitfall: Attempting to use dynamic programming, stacks, or other complex approaches when a simple greedy solution suffices.

# Overcomplicated approach using DP
def maximumPossibleSize(self, nums: List[int]) -> int:
    n = len(nums)
    dp = [1] * n  # dp[i] = max size ending at index i
  
    for i in range(1, n):
        for j in range(i):
            # Complex logic trying to compute optimal merges
            ...
  
    return max(dp)

Why it's wrong: This problem has a greedy property where local optimal choices lead to the global optimum. Adding complexity doesn't improve the solution and makes it harder to understand and debug.

Solution: Stick with the simple single-pass greedy approach that runs in O(n) time and O(1) space.

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

What are the most two important steps in writing a depth first search function? (Select 2)


Recommended Readings

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

Load More