[Monotone stack] Find the first number on the left and right of the array that is larger or smaller than itself

1. About the data structure of "monotone stack"

A monotonous stack refers to a stack structure whose interior satisfies monotonicity from the bottom of the stack to the top of the stack.

In fact, monotonic stack is "stack + maintain monotonicity".

1.1 Push operation

It is assumed here that the monotonic stack is a monotonically decreasing stack from the bottom of the stack to the top of the stack . In order to avoid disagreement, the monotonically increasing and monotonically decreasing hereinafter both refer to the order from the bottom of the stack to the top of the stack, and will not be explained later.

When inserting elements into a monotonic stack, it may be necessary to pop elements to ensure monotonicity in the stack.

If the element x to be inserted is smaller than the element at the top of the stack, it means that x is smaller than any element in the stack, so no other operations are required , just insert it directly.
If the element x to be inserted is larger than the element at the top of the stack, it means that some elements need to be popped to ensure the monotonicity of the stack after inserting the element x.
The basic template code for an insert operation is as follows:

// insert element x in mono_stack
void insert(stack<int>& mono_stack, int x) {
    
    
  while (!mono_stack.empty() && mono_stack() > x) {
    
    
    // operations #1
    mono_stack.pop();
  }
  // operations #2
  mono_stack.push(x);
}

1.2 Element relationships generated when maintaining monotonic stacks

There are two places in the above code where additional operations can be added (ie operations #1 and operations #2). If the previous insertion and use of the stack are the basis, then the element relationship generated at the two additional operation positions is the essence of the monotonic stack .

There are two additional operations on the monotonic stack, when the traversed elements are pushed onto the stack (corresponding to operations #2), and when the elements in the stack are popped (corresponding to operations #1) . This series of operations can actually be divided into two parts, namely when elements are pushed onto the stack and when elements are popped off the stack . For example, when an element is to be pushed into the stack, it is found that the monotonicity in the stack is no longer satisfied, and a series of elements need to be popped and then pushed into the stack. This process includes both the push of the element and the pop of other elements. In order to simplify the analysis, we consider the two operations separately, and analyze the element relationships involved when the two operations occur.

The following two cases are explained by creating a monotonically decreasing stack as an example. When the traversed element is pushed onto the stack :

insert image description here

As shown in the figure above, when we want to add element 1 to the stack, since element 1 can be directly pushed into the stack without destroying the monotonicity, we can directly push it into it. Before 1 is pushed into the stack, we can observe that the smallest element in the stack is 3 at the top of the stack, so the element that is about to be pushed into the stack on the left side of the original array position can only be 3, not 4. or 6 (since 3 is the smallest on the stack, 4 and 6 have been "truncated" by 3).

Therefore, we can abstract: if an element does not destroy the monotonicity of the monotonic stack after being pushed onto the stack, then the top element of the stack is the first element to the left of the original array position that is larger than itself .

It needs to be emphasized again that this conclusion only exists in the case of a monotonically decreasing stack and the array is traversed from left to right.

When an element is popped from the stack :

insert image description here

As shown in the figure above, when element 5 is to be pushed into the stack, it is found that the monotonicity cannot be satisfied after being pushed into the stack. The 1 at the top of the stack is less than 5, so the top element 1 must be popped. Here, the 1 to be popped and the 5 to be pushed also contain a relationship: 5 is the reason for breaking the monotonicity of the position of 1, and the elements (in the original array) between 1 and 5 do not break this relationship ( There is no element between 1 and 5 here), so the 5 to be pushed is the first element larger than itself on the right side of the original array position of the 1 to be popped.

Abstract this relationship: when an element to be pushed is pushed onto the stack, the element to be pushed is the first element to the right of the element to be pushed that is larger than itself .

This conclusion only exists when the stack is monotonically decreasing and the array is traversed from left to right.

In the example above, when 1 is popped off the stack, 3 and 4 must also be popped off the stack to allow 5 to be pushed onto the stack (there are no elements in the original array between 3 and 5, and between 4 and 5 to break the monotonicity). We can find that 5 is the first value to the right of 3 and 4 that is greater than itself. Then 5 will be pushed into the stack. At this time, the element 6 in the stack is the first value of 5 on the left side of the original array that is larger than itself. These also verify the two properties abstracted above.

1.3 Summary

Next, we summarize the properties of monotonic stacks. When we traverse an array from left to right, maintaining a monotonically decreasing stack, we have:

  • The first is monotonicity: the elements in the stack satisfy monotonicity, but this does not make a big difference.
  • If an element is pushed into the stack without destroying the monotonicity of the monotonic stack, then the element at the top of the stack is the first element that is larger than itself at the position [left] of the original array to be pushed.
  • Since an element to be pushed is to be pushed into the stack, when an element is popped, the element to be pushed is the first element that is larger than itself [on the right] of the popped element.

The above are all monotonically decreasing stacks. When faced with monotonically increasing stacks, they are directly extended to:

  • If an element is pushed into the stack without destroying the monotonicity of the monotonic stack, then the element at the top of the stack is the element to be pushed on the stack that is the first element that is smaller than itself at the position [left] of the original array.
  • Since an element to be pushed is to be pushed into the stack, when an element is popped, the element to be pushed is the first element that is [smaller] than itself [on the right] of the popped element.

From the above properties, we can see that the role of the monotonic stack is more to maintain the information obtained in the process of the stack, rather than the final stack itself. Therefore, we can obtain different information in operations #1 and operations #2 according to the requirements.

Second, the basic problem of using monotonic stack

2.1 Basic questions

Question 1:

For the given integer array nums, find the subscript of the first number larger than itself on the right side of each element, if not, fill in -1

example:
input:[2, 1, 5, 6, 2, 3, 1]
output:[2, 2, 3, -1, 5, -1, -1]

Analysis:
According to the monotonic stack features we got earlier:

  • We are concerned with subscripts of elements larger than ourselves, so we maintain a monotonically decreasing stack .
  • To find the first subscript of the element on the right that is larger than itself, this is the information that can only be obtained when popping the stack. Therefore, it is only necessary to obtain the information of the element to be pushed at that time before the element is popped . This is the element to be popped . The first element on the right that is larger than itself.
  • If an element has not been popped out of the stack, it means that there is no element larger than itself, so you can assign -1 during initialization.

After the simple analysis above, the answer can be easily obtained.

Code:

#include <iostream>
#include <stack>
#include <vector>
using namespace std;

// 问题1
vector<int> solve(vector<int>& nums) {
    
    
  int n = nums.size();
  vector<int> ret(n, -1);
  stack<int> st;
  for (int i = 0; i < n; ++i) {
    
    
    while (!st.empty() && nums[i] > nums[st.top()]) {
    
    
      ret[st.top()] = i;
      st.pop();
    }
    st.push(i);
  }
  return ret;
}

void printVec(vector<int> vec) {
    
    
  for (auto& x : vec) {
    
    
    cout << x << "  ";
  }
  cout << endl;
}

int main() {
    
    
  vector<int> nums = {
    
    2, 1, 5, 6, 2, 3, 1};
  auto ret = solve(nums);
  printVec(ret);
  return 0;
}

2.2 Problem extension

The problem of "the first person on the right side is bigger than ourselves" is solved, then we can easily extend three analogy problems, namely "the first person on the right side is smaller than himself" and "the first person on the left side is bigger than himself" , "the first one on the left is smaller than itself". details as follows:

Question 2:
For a given integer array nums, find the subscript of the first number smaller than itself on the right side of each element, if not, fill in -1

example:
input:[2, 1, 5, 6, 2, 3, 1]
output:[1, -1, 4, 4, 6, 6, -1]

analyze:

  • We are concerned with subscripts of elements smaller than ourselves, so we maintain a monotonically increasing stack.
  • To find the first subscript of the element on the right that is larger than itself, this is the information that can only be obtained when popping the stack. Therefore, when the element is popped, it is only necessary to obtain the information of the element to be pushed at that time, which is the element to be popped. The first element on the right that is smaller than itself.
    Code:

Compared with question 1, you only need to change the judgment on pop from > to <:

// 问题2
vector<int> solve(vector<int>& nums) {
    
    
  int n = nums.size();
  vector<int> ret(n, -1);
  stack<int> st;
  for (int i = 0; i < n; ++i) {
    
    
    while (!st.empty() && nums[i] < nums[st.top()]) {
    
    
      ret[st.top()] = i;
      st.pop();
    }
    st.push(i);
  }
  return ret;
}

Question 3:

For the given integer array nums, find the subscript of the first number on the left side of each element that is larger than itself, if not, fill in -1

example:
input:[2, 1, 5, 6, 2, 3, 1]
output:[-1, 0, -1, -1, 3, 3, 5]

analyze:

  • We are concerned with subscripts of elements larger than ourselves, so we maintain a monotonically decreasing stack.
  • To find the subscript of the first element on the left that is larger than itself, this is the information that can be obtained when pushing into the stack. Therefore, when an element can be successfully pushed onto the stack, we can obtain the element to be pushed and the top element of the stack before pushing. The top element of the stack is the first element to the left of the pushed element that is larger than itself.
  • If the stack is empty when an element is pushed, it is because there is no subscript greater than the element.

Code:

Compared with the code in question 1, there are three changes:

  • Remove the operation before pop
  • The loop where pop is located is changed to >=
  • Add the required operations before push (pay attention to prevent the stack from being empty)
// 问题3
vector<int> solve(vector<int>& nums) {
    
    
  int n = nums.size();
  vector<int> ret(n, -1);
  stack<int> st;
  for (int i = 0; i < n; ++i) {
    
    
    while (!st.empty() && nums[i] >= nums[st.top()]) {
    
    
      st.pop();
    }
    ret[i] = st.empty() ? -1 : st.top();
    st.push(i);
  }
  return ret;
}

Question 4:

For the given integer array nums, find the subscript of the first number smaller than itself on the left side of each element, if not, fill in -1

example:
input:[2, 1, 5, 6, 2, 3, 1]
output:[-1, -1, 1, 2, 1, 4, 4]

analyze:

  • We are concerned with subscripts of elements smaller than ourselves, so we maintain a monotonically increasing stack.
  • To find the subscript of the first element on the left that is smaller than itself, this is the information that can be obtained when pushing into the stack. Therefore, when an element can be successfully pushed onto the stack, we can obtain the element to be pushed and the top element of the stack before pushing. The top element of the stack is the first element to the left of the pushed element that is smaller than itself.
    Code:

Compared with question 3, you only need to change the judgment of the loop where pop is located to <=.

// 问题4
vector<int> solve(vector<int>& nums) {
    
    
  int n = nums.size();
  vector<int> ret(n, -1);
  stack<int> st;
  for (int i = 0; i < n; ++i) {
    
    
    while (!st.empty() && nums[i] <= nums[st.top()]) {
    
    
      st.pop();
    }
    ret[i] = st.empty() ? -1 : st.top();
    st.push(i);
  }
  return ret;
}

Through the above four very simple questions, you can easily understand the core operation of the monotonic stack.

3. Summary of the use of monotonic stack

Through the previous explanation and four questions, let's summarize the use of monotonic stack.

insert image description here

Find the elements on the left and right that are smaller than yourself, use a monotonically incremented stack, update the element on the left that is smaller than yourself when you put it on the stack, and update the first element on the right that is smaller than you when popping out of the stack. In the same way, to find the elements on the left and right that are larger than yourself, use a monotonically decrement stack, update the element on the left that is the most recent larger than yourself when you put it on the stack, and update the first element on the right that is larger than you when popping out of the stack.

In order to understand the reasons for the above rules, you can think about it this way: the life cycle of an element is that it is pushed into the stack when it is traversed, and then it has the opportunity to pop out of the stack when it is traversed to other elements. The information on the right side of the element is not known when it is pushed into the stack, so currently only the information on the left side is known (that is, the first element on the left side that is larger/smaller than itself is updated). When popping the stack, it is known that it is caused by an element on the right to be pushed into the stack, which is the information on the right (update the first element on the right that is larger/smaller than itself) (if it is not updated when popping the stack, it will be no chance).

The above are all traversed from left to right. If traversal from right to left is allowed , the table will become as follows:

insert image description here

It can be found that it is only the original table traversed from left to right, and the order of the stacking and popping operations is changed. In order to understand this change, it is necessary to look at the traversal direction and the order of obtaining information. When traversing from right to left, for the element being traversed, we only know the element and its right side information. Therefore, for this traversed element, when the first operation is to push the stack, the first value on the right that is larger or smaller than itself is obtained. When the second operation is to pop the stack, the first value on the left side of the element that is larger than itself is obtained (the pop-up is caused by an element on the left side of the element, and this element is the first value that is larger than itself. own large or small value) .

  1. Reference
    [1] https://www.cnblogs.com/molinchn/p/14772025.html

Guess you like

Origin blog.csdn.net/jiaoyangwm/article/details/127455866