Facebook Pixel

2289. Steps to Make Array Non-decreasing

Problem Description

You have an integer array nums indexed from 0. The goal is to make this array non-decreasing (each element is less than or equal to the next one) through a specific removal process.

In each step, you simultaneously remove all elements nums[i] where the element immediately before it (nums[i-1]) is greater than nums[i]. This removal happens for all positions i from 1 to the end of the array in a single step.

After removing these elements, the remaining elements shift together to form a new array, and you repeat the process until no more removals are needed (the array becomes non-decreasing).

Your task is to count how many steps this process takes.

Example walkthrough:

If nums = [5, 3, 4, 4, 7, 3, 6, 11, 8, 5, 11]:

  • Step 1: Remove elements where previous element is greater - remove 3 (after 5), 3 (after 7), 8 (after 11), 5 (after 8)
  • Continue this process until the array is non-decreasing
  • Return the total number of steps needed

The solution uses a stack-based approach with dynamic programming. Working from right to left, it tracks for each position how many steps it would take for that element to be removed. The stack maintains indices of elements in increasing order of their values, and dp[i] stores the maximum steps needed if element i starts removing elements to its right.

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 each element will be removed rather than simulating the actual removal process step by step.

Consider any element nums[i]. It will only be removed if there's a larger element to its left that "survives" long enough to remove it. The question becomes: how many steps does it take for nums[i] to be removed?

Working backwards from right to left gives us an advantage: we can determine the "lifetime" of each element based on what happens to its right. For each position i, we need to know:

  1. Which elements to the right will nums[i] directly remove (those smaller than nums[i])
  2. How long those elements would have survived on their own

This leads to a critical observation: if nums[i] removes nums[j] directly (where j > i and nums[i] > nums[j]), then nums[i] must survive at least as long as it takes to remove nums[j], plus one more step for the actual removal.

Using a monotonic stack helps us efficiently track this relationship. As we traverse from right to left:

  • When we find an element nums[i] that's larger than elements in our stack, it means nums[i] will remove those elements
  • The removal time for nums[i] is the maximum of: one step to remove each direct neighbor, or the time needed for that neighbor to finish its own removals
  • Elements that are never removed (those in increasing order) will have dp[i] = 0

The formula dp[i] = max(dp[i] + 1, dp[stk.pop()]) captures this: we either wait one more step than the previous removal, or we wait for the popped element to finish its cascade of removals, whichever takes longer.

The answer is the maximum value in the dp array, representing the longest chain of removals that will occur.

Learn more about Stack, Linked List and Monotonic Stack patterns.

Solution Approach

The solution uses a monotonic stack combined with dynamic programming to efficiently calculate the number of steps needed.

Data Structures:

  • stk: A stack that maintains indices of elements in increasing order of their values
  • dp: An array where dp[i] represents the number of steps until element at index i gets removed (0 if never removed)

Algorithm Steps:

  1. Initialize structures:

    • Empty stack stk to store indices
    • Array dp of size n initialized with zeros
    • Variable ans to track the answer
  2. Traverse from right to left (from index n-1 to 0):

    For each position i:

    a. Process elements that will be removed by nums[i]:

    while stk and nums[i] > nums[stk[-1]]:
        dp[i] = max(dp[i] + 1, dp[stk.pop()])
    • While the stack is not empty and current element is greater than the element at the top of stack
    • This means nums[i] will eventually remove nums[stk[-1]]
    • Update dp[i] to be the maximum of:
      • dp[i] + 1: One more step than previously calculated removals
      • dp[stk.pop()]: The steps needed for the popped element to complete its removals
    • This ensures we account for cascading removals

    b. Add current index to stack:

    stk.append(i)
    • After processing, add current index to maintain the monotonic property
  3. Return the maximum value in dp:

    return max(dp)
    • The maximum value represents the longest chain of removals needed

Why this works:

  • The stack maintains indices in increasing order of values when traversing right to left
  • When we find an element that can remove others (stack elements with smaller values), we calculate how many steps it needs
  • The max operation in the update ensures we wait for all cascading removals to complete
  • Elements that are never removed remain with dp[i] = 0
  • The final answer is the maximum steps any element needs, which represents when the array becomes non-decreasing

Time Complexity: O(n) - each element is pushed and popped from the stack at most once Space Complexity: O(n) - for the stack and dp array

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 trace through a small example: nums = [10, 6, 8, 7]

Initial Setup:

  • stk = [] (empty stack)
  • dp = [0, 0, 0, 0]
  • We'll traverse from right to left (index 3 to 0)

Step 1: Process index 3 (value = 7)

  • Stack is empty, no elements to remove
  • Push index 3 to stack: stk = [3]
  • dp = [0, 0, 0, 0]

Step 2: Process index 2 (value = 8)

  • Check stack top: nums[2] = 8 > nums[3] = 7
  • Element at index 2 will remove element at index 3
  • Pop index 3: dp[2] = max(dp[2] + 1, dp[3]) = max(0 + 1, 0) = 1
  • Push index 2 to stack: stk = [2]
  • dp = [0, 0, 1, 0]

Step 3: Process index 1 (value = 6)

  • Check stack top: nums[1] = 6 < nums[2] = 8
  • No elements to remove (6 cannot remove 8)
  • Push index 1 to stack: stk = [2, 1]
  • dp = [0, 0, 1, 0]

Step 4: Process index 0 (value = 10)

  • Check stack top: nums[0] = 10 > nums[1] = 6
  • Pop index 1: dp[0] = max(dp[0] + 1, dp[1]) = max(0 + 1, 0) = 1
  • Check stack top again: nums[0] = 10 > nums[2] = 8
  • Pop index 2: dp[0] = max(dp[0] + 1, dp[2]) = max(1 + 1, 1) = 2
  • Push index 0 to stack: stk = [0]
  • dp = [2, 0, 1, 0]

Final Result: max(dp) = 2

What happens during actual removal:

  • Step 1: Remove 6 (10 > 6), remove 7 (8 > 7) → Array becomes [10, 8]
  • Step 2: Remove 8 (10 > 8) → Array becomes [10]
  • Array is now non-decreasing after 2 steps ✓

The algorithm correctly identifies that element at index 0 (value 10) needs 2 steps to complete all its removals, which determines the total number of steps needed.

Solution Implementation

1class Solution:
2    def totalSteps(self, nums: List[int]) -> int:
3        # Stack to maintain indices of elements in decreasing order
4        stack = []
5      
6        # dp[i] represents the number of steps needed for nums[i] to remove all elements it can remove
7        n = len(nums)
8        dp = [0] * n
9      
10        # Traverse the array from right to left
11        for i in range(n - 1, -1, -1):
12            # While stack is not empty and current element is greater than element at top of stack
13            # This means nums[i] will eventually remove nums[stack[-1]]
14            while stack and nums[i] > nums[stack[-1]]:
15                # The number of steps for nums[i] is either:
16                # 1. One more than its current count (removing the immediate next smaller element)
17                # 2. The steps needed by the element being removed (if it removes other elements first)
18                dp[i] = max(dp[i] + 1, dp[stack.pop()])
19          
20            # Add current index to stack
21            stack.append(i)
22      
23        # Return the maximum number of steps among all elements
24        return max(dp)
25
1class Solution {
2    public int totalSteps(int[] nums) {
3        // Stack to maintain indices of elements in decreasing order
4        Deque<Integer> indexStack = new ArrayDeque<>();
5      
6        // Maximum steps needed across all elements
7        int maxSteps = 0;
8      
9        int arrayLength = nums.length;
10      
11        // dp[i] represents the number of steps needed for element at index i
12        // to remove all smaller elements to its right
13        int[] stepsToRemove = new int[arrayLength];
14      
15        // Process array from right to left
16        for (int currentIndex = arrayLength - 1; currentIndex >= 0; currentIndex--) {
17          
18            // While stack has elements and current element is greater than stack top
19            // This means current element can remove the element at stack top
20            while (!indexStack.isEmpty() && nums[currentIndex] > nums[indexStack.peek()]) {
21                // Pop the smaller element's index
22                int smallerElementIndex = indexStack.pop();
23              
24                // Update steps for current element:
25                // Either increment current steps by 1, or take the steps from popped element
26                // We take maximum because some elements might be removed in parallel
27                stepsToRemove[currentIndex] = Math.max(
28                    stepsToRemove[currentIndex] + 1, 
29                    stepsToRemove[smallerElementIndex]
30                );
31              
32                // Update the global maximum steps
33                maxSteps = Math.max(maxSteps, stepsToRemove[currentIndex]);
34            }
35          
36            // Push current index onto stack
37            indexStack.push(currentIndex);
38        }
39      
40        return maxSteps;
41    }
42}
43
1class Solution {
2public:
3    int totalSteps(vector<int>& nums) {
4        // Stack to maintain indices of elements in decreasing order
5        stack<int> indexStack;
6      
7        // Maximum steps needed across all elements
8        int maxSteps = 0;
9        int arraySize = nums.size();
10      
11        // dp[i] stores the number of steps needed for element at index i
12        // to remove all smaller elements to its right that it can remove
13        vector<int> stepsToRemove(arraySize, 0);
14      
15        // Process elements from right to left
16        for (int currentIndex = arraySize - 1; currentIndex >= 0; --currentIndex) {
17            // While stack is not empty and current element is greater than stack top
18            // Current element can remove the element at stack top
19            while (!indexStack.empty() && nums[currentIndex] > nums[indexStack.top()]) {
20                int topIndex = indexStack.top();
21              
22                // Update steps for current element:
23                // Either increment its own count or inherit from the removed element
24                // This accounts for cascading removals
25                stepsToRemove[currentIndex] = max(stepsToRemove[currentIndex] + 1, 
26                                                   stepsToRemove[topIndex]);
27              
28                // Update the global maximum steps
29                maxSteps = max(maxSteps, stepsToRemove[currentIndex]);
30              
31                // Remove the processed element from stack
32                indexStack.pop();
33            }
34          
35            // Add current element index to stack
36            indexStack.push(currentIndex);
37        }
38      
39        return maxSteps;
40    }
41};
42
1/**
2 * Calculates the total number of steps required to remove all elements
3 * that are smaller than their right neighbor in the array
4 * @param nums - The input array of numbers
5 * @returns The maximum number of steps needed
6 */
7function totalSteps(nums: number[]): number {
8    let maxSteps: number = 0;
9    // Stack to store pairs of [value, steps needed to remove this element]
10    let stack: Array<[number, number]> = [];
11  
12    // Process each number from left to right
13    for (let currentNum of nums) {
14        let stepsForCurrent: number = 0;
15      
16        // Remove elements from stack that are smaller than or equal to current number
17        // These elements would be removed by the current number
18        while (stack.length > 0 && stack[0][0] <= currentNum) {
19            // Track the maximum steps among removed elements
20            stepsForCurrent = Math.max(stack[0][1], stepsForCurrent);
21            stack.shift();
22        }
23      
24        // If there are still elements in stack, current element needs one more step
25        // (it will be removed by the larger element to its left)
26        if (stack.length > 0) {
27            stepsForCurrent++;
28        }
29      
30        // Update the global maximum steps
31        maxSteps = Math.max(stepsForCurrent, maxSteps);
32      
33        // Add current element and its steps to the stack
34        stack.unshift([currentNum, stepsForCurrent]);
35    }
36  
37    return maxSteps;
38}
39

Time and Space Complexity

Time Complexity: O(n) where n is the length of the input array nums.

The algorithm iterates through the array once from right to left (index n-1 to 0). For each element at index i, it performs stack operations:

  • The while loop pops elements from the stack when nums[i] > nums[stk[-1]]
  • Each element is pushed onto the stack exactly once and popped at most once

Since each element is pushed once and popped at most once throughout the entire execution, the total number of operations across all iterations is bounded by 2n. Therefore, despite the nested loop structure, the amortized time complexity is O(n).

Space Complexity: O(n) where n is the length of the input array.

The algorithm uses:

  • A stack stk that can contain at most n elements (in the worst case, all indices are stored)
  • A dynamic programming array dp of size n to store the number of steps for each position
  • A few constant variables (ans, n, i)

The dominant space usage comes from the dp array and the stack, both of which can grow up to size n in the worst case, resulting in O(n) space complexity.

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

Common Pitfalls

1. Incorrect Understanding of Simultaneous Removal

Pitfall: A common mistake is thinking elements are removed one at a time sequentially, leading to incorrect step counting. For example, if nums = [10, 1, 2, 3, 4, 5], some might think:

  • Step 1: Remove 1 (after 10) → [10, 2, 3, 4, 5]
  • Step 2: Remove 2 (after 10) → [10, 3, 4, 5]
  • And so on...

Reality: All qualifying elements are removed simultaneously in each step:

  • Step 1: Remove 1, 2, 3, 4, 5 all at once → [10]
  • Total: 1 step

Solution: Remember that in each step, ALL elements where nums[i-1] > nums[i] are removed together, not one by one.

2. Misunderstanding the DP Update Logic

Pitfall: Using dp[i] = dp[i] + 1 instead of dp[i] = max(dp[i] + 1, dp[stack.pop()]).

Consider nums = [7, 3, 5, 4]:

  • When processing index 0 (value 7), it will remove index 1 (value 3)
  • But value 3 needs to first remove value 4 (which takes 1 step)
  • If we just use dp[i] + 1, we miss the cascading effect

Solution: The max operation ensures we wait for all cascading removals. If an element we're removing also removes other elements, we need to account for those steps.

3. Stack Order Confusion

Pitfall: Maintaining the wrong monotonic property in the stack. The stack should maintain indices where the corresponding values are in increasing order (when traversing right to left).

Wrong approach:

# Incorrect: trying to maintain decreasing order
while stack and nums[i] < nums[stack[-1]]:  # Wrong comparison
    dp[i] = max(dp[i] + 1, dp[stack.pop()])

Solution: Use the correct comparison nums[i] > nums[stack[-1]] to identify elements that will be removed by nums[i].

4. Edge Case: Already Non-Decreasing Array

Pitfall: Not handling arrays that are already non-decreasing, leading to unnecessary computations or incorrect results.

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

  • No removals are needed
  • All dp[i] values remain 0
  • Answer should be 0

Solution: The algorithm naturally handles this - when no element can remove others, all dp values stay 0, and max(dp) returns 0.

5. Traversal Direction Error

Pitfall: Traversing left to right instead of right to left.

Why right to left is crucial:

  • We need to know what happens to elements on the right before processing elements on the left
  • An element at position i affects elements at positions i+1, i+2, ...
  • By processing right to left, we ensure all downstream effects are already calculated

Solution: Always traverse from n-1 to 0:

for i in range(n - 1, -1, -1):  # Correct direction
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

You are given an array of intervals where intervals[i] = [start_i, end_i] represent the start and end of the ith interval. You need to merge all overlapping intervals and return an array of the non-overlapping intervals that cover all the intervals in the input.


Recommended Readings

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

Load More