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 (incrementans
) 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.
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:
-
Initialize two variables:
ans = 0
: Counts the maximum possible size of the final arraymx = 0
: Tracks the maximum value seen so far
-
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)
- Increment
- If false:
- Do nothing (this element will be merged with previous elements)
- Check if
-
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
, soans = 1, mx = 2
x = 1
:2 > 1
, skip (will be merged)x = 3
:2 <= 3
, soans = 2, mx = 3
x = 2
:3 > 2
, skip (will be merged)x = 4
:3 <= 4
, soans = 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 EvaluatorExample 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:
-
Process element 1:
- Check: Is
mx <= 1
? Yes,0 <= 1
- Action: Keep this element
- Update:
ans = 1
,mx = 1
- Array so far:
[1]
- Check: Is
-
Process element 4:
- Check: Is
mx <= 4
? Yes,1 <= 4
- Action: Keep this element
- Update:
ans = 2
,mx = 4
- Array so far:
[1, 4]
- Check: Is
-
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
- Check: Is
-
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
- Check: Is
-
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]
- Check: Is
-
Process element 1:
- Check: Is
mx <= 1
? No,5 > 1
- Action: Skip (this element must be merged)
- No update:
ans = 3
,mx = 5
- Check: Is
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 with4
(the maximum) - Merging
[5, 1]
and replacing with5
(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.
What are the most two important steps in writing a depth first search function? (Select 2)
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Monotonic Stack Deque Intro If you'd prefer a video div class responsive iframe iframe src https www youtube com embed Dq_ObZwTY_Q title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div The word monotonic means a list or
Want a Structured Path to Master System Design Too? Don’t Miss This!