Steps to Make Array Non-decreasing
MedPrompt
You are given a 0-indexed integer array nums. In one step, remove all elements nums[i] where nums[i - 1] > nums[i] for all 0 < i < nums.length.
Return the number of steps performed until nums becomes a non-decreasing array.
Example 1:
Input: nums = [5,3,4,4,7,3,6,11,8,5,11]
Output: 3
Explanation: The following are the steps performed:
- Step 1: [5,3,4,4,7,3,6,11,8,5,11] becomes [5,4,4,7,6,11,11]
- Step 2: [5,4,4,7,6,11,11] becomes [5,4,7,11,11]
- Step 3: [5,4,7,11,11] becomes [5,7,11,11]
[5,7,11,11] is a non-decreasing array. Therefore, we return 3.Example 2:
Input: nums = [4,5,7,7,13]
Output: 0
Explanation: nums is already a non-decreasing array. Therefore, we return 0.
Constraints:
1 <= nums.length <= 1051 <= nums[i] <= 109
Approaches
2 approaches with complexity analysis and trade-offs.
This approach directly simulates the process described in the problem. We repeatedly scan the array, identify all elements that are smaller than their preceding element, and remove them. We count how many such removal steps are needed until the array becomes non-decreasing.
Algorithm
- Initialize a variable
stepsto 0. - Start an infinite loop to simulate the steps.
- Inside the loop, create a list to store the indices of elements that need to be removed in the current step. Iterate through the current array from the second element (
i=1) to the end. Ifnums[i-1] > nums[i], add indexito the removal list. - If the removal list is empty, it means the array is non-decreasing. Break the loop.
- If the list is not empty, increment
steps. - Create a new array (or list) by filtering out the elements at the indices marked for removal.
- Replace the old array with this new one.
- After the loop terminates, return
steps.
Walkthrough
The core idea is to mimic the step-by-step removal process. We use a loop that continues as long as the array is not sorted. In each iteration of the loop, which represents one step, we first identify all elements nums[i] such that nums[i-1] > nums[i]. We store their indices. If no such elements are found, the array is non-decreasing, and we can stop. Otherwise, we increment our step counter and construct the array for the next step by removing all the identified elements. Using a dynamic data structure like an ArrayList in Java simplifies the removal and resizing of the array in each step.
import java.util.ArrayList;import java.util.List; class Solution { public int totalSteps(int[] nums) { int n = nums.length; if (n <= 1) { return 0; } List<Integer> currentNums = new ArrayList<>(); for (int num : nums) { currentNums.add(num); } int steps = 0; while (true) { List<Integer> toRemoveIndices = new ArrayList<>(); for (int i = 1; i < currentNums.size(); i++) { if (currentNums.get(i - 1) > currentNums.get(i)) { toRemoveIndices.add(i); } } if (toRemoveIndices.isEmpty()) { break; } steps++; List<Integer> nextNums = new ArrayList<>(); int removeIndexPtr = 0; for (int i = 0; i < currentNums.size(); i++) { if (removeIndexPtr < toRemoveIndices.size() && toRemoveIndices.get(removeIndexPtr) == i) { removeIndexPtr++; } else { nextNums.add(currentNums.get(i)); } } currentNums = nextNums; } return steps; }}Complexity
Time
O(N^2), where N is the initial number of elements. In the worst-case scenario (e.g., a strictly decreasing array), we might perform O(N) steps. Each step involves iterating through the current array to find elements to remove and then building a new array, which takes O(K) time where K is the current array size. This leads to a total time complexity of roughly N + (N-1) + (N-2) + ... + 1, which is O(N^2).
Space
O(N), where N is the initial number of elements in the array. In each step, we need extra space to store the indices to be removed and to build the array for the next step. The maximum size of these lists is proportional to N.
Trade-offs
Pros
It's straightforward to understand as it directly translates the problem statement into code.
The logic is simple to implement without requiring complex data structures or algorithms.
Cons
The time complexity of O(N^2) is too slow for the given constraints (N up to 10^5) and will result in a 'Time Limit Exceeded' error on most platforms.
Creating a new list in every step can be memory-intensive, although the total space remains O(N).
A more efficient approach uses dynamic programming combined with a monotonic stack. The key idea is to determine, for each element nums[i], how many steps it takes for it to be removed. The final answer is the maximum of these values. A monotonic stack helps us efficiently find the preceding element that will cause the removal and calculate the time it takes by considering the elements that need to be removed in between.
Algorithm
- Initialize a
dparray of sizento store the removal steps for each element, and an integeransto 0 to track the maximum steps. - Use a stack to store indices of elements that form a monotonically decreasing sequence.
- Iterate through the
numsarray from left to right with indexi:- Initialize
current_steps = 0. - While the stack is not empty and the element at the top of the stack is less than or equal to the current element
nums[i], it means the element atstack.peek()is 'shielded' bynums[i]. We must wait for it to be removed. The time it takes isdp[stack.peek()]. We updatecurrent_steps = max(current_steps, dp[stack.peek()])and pop from the stack. - After the loop, if the stack is empty, it means no element to the left is larger than
nums[i], sonums[i]will never be removed.dp[i]remains 0. - If the stack is not empty, the element at
stack.peek()is larger thannums[i]and will be its eventual remover. The removal will happen one step after all intermediate elements (which we just popped) are removed. So,dp[i] = current_steps + 1. - Update the overall answer:
ans = max(ans, dp[i]). - Push the current index
ionto the stack.
- Initialize
- Return
ans.
Walkthrough
Let dp[i] be the number of steps required to remove nums[i]. If nums[i] is never removed, dp[i] = 0. The answer to the problem is max(dp). We can compute dp for all i in a single pass from left to right.
An element nums[i] is removed when its left neighbor becomes larger than it. This can happen in step 1 if nums[i-1] > nums[i], or in a later step if nums[i-1] (and potentially other elements) are removed, exposing nums[i] to a new, larger neighbor from further left.
The number of steps to remove nums[i] is 1 + the number of steps required to remove all the 'blocking' elements between nums[i] and the first element to its left that is larger than it.
A monotonic stack (specifically, one that maintains indices of elements in decreasing order of value) is perfect for this. As we iterate through nums with index i, the stack helps us keep track of the chain of potential 'removers' to the left of i.
When considering nums[i], we pop elements nums[j] from the stack if nums[j] <= nums[i]. For each popped element j, we know it takes dp[j] steps to be removed. We need to wait for the slowest of these removals. We keep track of the maximum dp value among all popped elements. After popping, if the stack is not empty, its top element nums[k] is the first element to the left of i that is greater than nums[i]. nums[i] will be removed by nums[k] one step after all the intermediate elements are gone. This gives us dp[i]. If the stack becomes empty, nums[i] is larger than or equal to all elements to its left and will never be removed.
import java.util.Stack; class Solution { public int totalSteps(int[] nums) { int n = nums.length; int[] dp = new int[n]; Stack<Integer> stack = new Stack<>(); int ans = 0; for (int i = 0; i < n; i++) { int currentSteps = 0; // Pop elements from the stack that are smaller or equal to the current element. // These elements are 'eaten' by the current element or a previous larger one. while (!stack.isEmpty() && nums[stack.peek()] <= nums[i]) { // The current element nums[i] has to wait for the element at stack.peek() // to be removed. We take the maximum of the steps needed for all such elements. currentSteps = Math.max(currentSteps, dp[stack.pop()]); } if (!stack.isEmpty()) { // The element at stack.peek() is greater than nums[i]. // It will remove nums[i]. This happens one step after all elements // between them are removed. dp[i] = currentSteps + 1; } else { // If stack is empty, no element to the left is greater than nums[i]. // So, nums[i] will never be removed. dp[i] = 0; } // Update the maximum steps seen so far. ans = Math.max(ans, dp[i]); // Push the current index onto the stack. stack.push(i); } return ans; }}Complexity
Time
O(N), where N is the number of elements in the array. Each element's index is pushed onto the stack exactly once and popped at most once. All operations within the loop are amortized constant time.
Space
O(N), where N is the number of elements. This is for the `dp` array and the stack. In the worst case (a strictly decreasing array), the stack can hold up to N elements.
Trade-offs
Pros
Optimal time complexity of O(N), making it very efficient for large inputs.
Solves the problem in a single pass through the array.
Cons
The logic is more complex and less intuitive compared to the direct simulation.
Requires careful handling of the stack and the
dparray logic to ensure correctness.
Solutions
Solution
class Solution {public int totalSteps(int[] nums) { Deque<Integer> stk = new ArrayDeque<>(); int ans = 0; int n = nums.length; int[] dp = new int[n]; for (int i = n - 1; i >= 0; --i) { while (!stk.isEmpty() && nums[i] > nums[stk.peek()]) { dp[i] = Math.max(dp[i] + 1, dp[stk.pop()]); ans = Math.max(ans, dp[i]); } stk.push(i); } return ans; }}class Solution {public: int totalSteps(vector<int> &nums) { stack<int> stk; int ans = 0, n = nums.size(); vector<int> dp(n); for (int i = n - 1; i >= 0; --i) { while (!stk.empty() && nums[i] > nums[stk.top()]) { dp[i] = max(dp[i] + 1, dp[stk.top()]); ans = max(ans, dp[i]); stk.pop(); } stk.push(i); } return ans; }};class Solution: def totalSteps(self, nums: List[int]) -> int: stk = [] ans, n = 0, len(nums) dp = [0] * n for i in range(n - 1, - 1, - 1): while stk and nums[i] > nums[stk[- 1]]: dp[i] = max(dp[i] + 1, dp[stk . pop()]) stk . append(i) return max(dp)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.