Learning and example of monotonic stack (the distance problem of the nearest larger number on the left and right sides and the largest rectangle problem of the histogram)

Monotonic queue and monotonic stack are very similar, what is the difference between them? First of all, quoting
http://blog.sina.com.cn/s/blog_6ffc3bde01015l2m.html
: The
monotonic stack solves the maximum interval with a certain value of the minimum (maximum) value . The implementation method is: find the minimum value (maximum value) ), maintain an incrementing (decrementing) stack, and start popping the stack when it encounters a value smaller than the top of the stack, and the range from the position where the popup stops to this value is the largest range to the left of this value; at the same time , when a value is bounced, it means that a smaller (larger) value than it is coming, and the largest interval to the right of the bounced value can also be calculated.
The monotonic queue solves the minimum (maximum) value of the interval . The implementation method is: to find the minimum (maximum) value of the interval, an increasing double-ended queue is maintained, and the label of the original sequence is stored in the queue. When the value of the element at the end of the queue is small (large), the tail of the queue will be continuously bounced until a value smaller than it appears. When the interval is greater than the specified interval, it will continue to bounce off the leader until the span is less than or equal to the specified interval. This ensures that the element at the head of the queue is the minimum (maximum) value, (but does not guarantee that the tail of the queue is the maximum (minimum) value in the original sequence), and maintains the length of the interval.

Add some personal summaries:
1) Monotonic queues can pop values ​​from the head and tail of the queue, while monotonic stacks can only pop values ​​from the top of the stack. In this sense, monotonic queues are not strictly queues (queues cannot be used but deques must be used), while monotonic stacks are strictly stacks (stacks can be used, and deques can of course be used).
2) The monotonic queue is the push value from the end of the queue, and the monotonic stack is the push value from the top of the stack. From this point of view, the tail of a monotonic queue is the same as the top of a monotonic stack.
3) Monotonic queues usually have interval length restrictions, while monotonic stacks do not necessarily have interval length restrictions (the topics I have seen seem to have none). So the monotonic stack is actually simpler, because there is no need to consider interval overflows in real time.
4) Monotonic queues use a decreasing queue to find the maximum value of the interval, and use an increasing queue to find the minimum value of the interval.
Monotonic stacks use a decrementing queue to find the boundary on the left (or right) side that is larger than the current value, and use an increasing queue to find the boundary on the left (or right) that is smaller than the current value. Why is it a decrementing queue to find a boundary larger than the current value? Because this ensures that when the top of the stack is smaller than the new element, the next element on the top of the stack (and the next element..., both larger than the top of the stack) can be compared with the new elements one by one.

Monotonic stack example:
Example 1: The distance problem of the nearest larger number on the left and right sides. Given an array, return an array of the same size. The value of the i-th position of the returned array should be, for the i-th element in the original array, at least how many steps to the right can you encounter an element larger than yourself (if there is no element larger than yourself, or is already the last element, put -1 in the corresponding position of the returned array).

Simple example:
input: 5,3,1,2,4
return: -1 3 1 1 -1

The complexity of this problem using the brute force method is O(n^2). With a monotonic stack, the complexity is O(n). Here, the monotonic stack is arranged in decreasing order from the bottom of the stack to the top of the stack. The specific execution sequence:
1) 5 (corresponding sequence number) is pushed into the stack.
2) Because 3 is smaller than 5, 3 (corresponding sequence number) is pushed onto the stack.
3) Because 1 is smaller than 3, 1 (corresponding sequence number) is pushed onto the stack.
4) Because 2 is larger than 1, the distance corresponding to 1 is the serial number of 2 - the serial number of 1 = 1, which is recorded in the output array corresponding to 1. Then 1 is popped off the stack and 3 becomes the top of the stack. Because 2 is smaller than 3, 3 is not on the stack and 2 is on the stack.
5) Because 4 is larger than 2, the distance corresponding to 2 is the serial number of 4 - the serial number of 2 = 1, recorded in the output array corresponding to 2, then 2 is popped off the stack, and 3 becomes the top of the stack. Then 4 is still larger than 3, the distance corresponding to 3 is the serial number of 4 - the serial number of 3 = 3, recorded in the output array corresponding to 3, and then 3 is popped out of the stack. Because 4 is not as big as 5, so 5 does not stand. 4 into the stack.
6) After the program runs, 5 and 4 are on the stack, and the elements of their corresponding output arrays are still -1.

vector<int> NextLarger(vector<int> &data) {
    vector<int> output(data.size(), -1); //首先都初始化为-1
    stack<int> monoStack;

    for (int i=0; i<data.size(); ++i) {
        while(!monoStack.empty() && data[monoStack.top()]<data[i]) {
            output[monoStack.top()] = i-monoStack.top();
            monoStack.pop();
        }
        monoStack.push(i);
    }

    return output;
}

In the above code, data[monoStack.top()] < data[i] guarantees that once the new element is larger than the top of the stack, it means that the element at the top of the stack has just found a number larger than it on the right, and the corresponding output position will be immediately to update. At the same time, the element at the top of the stack has also completed the task and can no longer fall in love with the stack. It must be popped out immediately and let the elements below compete with this new element. This is repeated until the condition in the while loop does not hold, indicating that the stack is empty, or the new element is already smaller than the top element of the stack.

Because all elements are popped onto the stack at most once, which is equivalent to n operations amortized in the for loop, the complexity is still O(n). See the amortized analysis in the algorithm for details.

Also a little review of the content of C++. NextLarger() returns a vector, and the copy constructor is called when returning here, so although the output is a local variable, it will not go wrong, because it returns a copy of the local variable. The return value cannot be referenced here, vector & will cause the local variable to be returned directly, but the local variable has been destructed when the function ends.

With a little modification, this problem can be turned into a distance problem of finding a larger number on the left (the for loop is reversed, and data[monoStack.top()]>data[i]).

Example 2: Largest Rectangle in Histogram, given a histogram, assuming that each rectangle has a width of 1, find the largest area of ​​all rectangles that can be formed in the histogram.
Simple example:
input: 2,1,5,6,2,3
return: 10

It is easy to see that the rectangle with the largest area is the rectangle formed by the histograms with heights 5 and 6, and its area is 5 * 2 = 10.

Solution 1: This problem is actually equivalent to: find the boundary of the nearest rectangle on the left and right lower than him for each rectangle, and then add the distances on the left and right sides (also -1, because it has been calculated twice) × this Rectangle height. Then find the maximum value of that operation across all rectangles. In this way, our previous example 1 can be used immediately. Note that each element is required here, and the left and right sides are smaller than it, so use an incrementing queue (data[monoStack.top()] > data[i]).

#include <iostream>
#include <stack>
#include <vector>
#include <map>

using namespace std;

//rightwards is TRUE, leftwards is FALSE
map<bool, vector<int> > dataMap;

void NextSmaller(vector<int> &data) {
    vector<int> toRight(data.size(), -1);
    vector<int> toLeft(data.size(), -1);
    dataMap[true] = toRight;
    dataMap[false] = toLeft;
    stack<int> monoToRightStack;
    stack<int> monoToLeftStack;

    for (int i=0; i<data.size(); ++i) {
        while(!monoToRightStack.empty() && data[monoToRightStack.top()]>data[i]) {
            dataMap[true][monoToRightStack.top()] = i-monoToRightStack.top();
            monoToRightStack.pop();
        }
        monoToRightStack.push(i);
    }

    for (int i=data.size()-1; i>=0; --i) {
        while(!monoToLeftStack.empty() && data[monoToLeftStack.top()]>data[i]) {
            dataMap[false][monoToLeftStack.top()] = monoToLeftStack.top() - i;
            monoToLeftStack.pop();
        }
        monoToLeftStack.push(i);
    }

    return;
}

int LargestRec1(vector<int> &data) {
    //add two dummy boundaries
    data.insert(data.begin(), -1);
    data.push_back(-1);
    NextSmaller(data);

    //cout<<"Rightwards"<<endl;
    //for (int i=0; i<data.size(); i++) {
    //    cout<<dataMap[true][i]<<" ";
    //}
    //cout<<endl;

    //cout<<"Leftwards"<<endl;
    //for (int i=0; i<data.size(); i++) {
    //    cout<<dataMap[false][i]<<" ";
    //}
    //cout<<endl;

    int maxV=0;
    int index=0;
    for (int i=0; i<data.size(); i++) {
        int tempV= heights[i]>0 ? data[i]*(dataMap[true][i]+dataMap[false][i]-1) : 0;
        if (maxV < tempV) {
           index = i;
        maxV = tempV;
        }
    }
    return maxV;
}

Here dataMap[true][i] and dataMap[false][i] correspond to the distance of element i to the right and left to the nearest element smaller than it, respectively.

Note that the above is to find the distance between the nearest decimals on the left and right sides, so it is data[monoStack.top()]>data[i]. Once the new element of the inequality surface is smaller than the element at the top of the stack, it means that the element at the top of the stack has found the nearest decimal point. At this time, the distance corresponding to the element at the top of the stack in the output array should be recorded immediately, and the top of the stack array should be popped, and so on. , until the stack is empty or the new element is larger than the top element.

Note that this question should pay special attention to the boundary conditions, that is, the case where the left and right boundaries are particularly large. For example, if the input is 200, 1, 5, 6, 2, 3 or 2, 1, 5, 6, 2, 300, the output should be 200, 300 respectively. So in LargestRec1(), two dummy -1s are specially added to the left and right sides of data[] to ensure that the left and right sides will be considered.

Also review the content of C++. In the above example, in order to practice stl, map is used

    vector<int> toRight(data.size(), -1);
    dataMap[true] = toRight;

Here dataMap[true] = toRight is to copy the toRight array to dataMap[true]. So dataMap[true] changed later, but toRight still didn't move.

Solution 2:
Solution 1 swipe left and right each time, in fact, only need to swipe once.

int LargestRec2(vector<int> &data) {
    stack<int> monoStack; //单调递增栈
    int maxV = 0;

    //add two dummy boundaries
    data.insert(data.begin(), -1);
    data.push_back(-1);

    for (int i=0; i<data.size(); ++i) {
        while(!monoStack.empty() && data[monoStack.top()]>data[i]) {
            int oldTop = monoStack.top();
            monoStack.pop();
            maxV = max(maxV, data[oldTop]*(i-monoStack.top()-1));
        }
        monoStack.push(i);
    }

    return maxV;
}

int main()
{
    vector<int> data = {2,7,5,6,2,3};
    cout<<LargestRec2(data)<<endl;
    return 0;
}

Note: Solution 2's

data[oldTop]*(i-monoStack.top()-1)

Is it similar to
data[i]*(dataMap[true][i]+dataMap[false][i]-1) of solution 1
? In fact, i-oldTop is the nearest element from oldTop to the right that is smaller than it The distance of oldTop-monoStack.top() is the distance from oldTop to the nearest element on the left that is smaller than it. The addition of the two must be subtracted by one, because oldTop itself is counted twice.

Taking the input as [2,7,5,6,2,3] as an example, the second step of the solution is:
0) maxV = 0.
1) 2 (corresponding sequence number) is pushed onto the stack.
2) 2 is smaller than 7, and 7 (corresponding to the sequence number) is pushed onto the stack.
3) 5 is less than 7, write down the value of 7, and pop 7 out of the stack. 7*(2-0-1)=7. Here 2 and 0 are the serial numbers corresponding to 5 and the first 2, respectively, that is, the serial numbers of the nearest lower decimals to the right and left of 7. maxV=7. Note: For brevity, the ordinal numbers here do not take into account dummy boundaries.
4) 6 is larger than 5, and 6 is pushed onto the stack;
5) 2 is smaller than 6. Write down the value of 6 and pop 6 from the stack. 6*(4-2-1)=6. Here 4 and 2 are the serial numbers corresponding to the second 2 and 5, respectively, that is, the serial numbers of the nearest smaller decimals to the right and left of 6. maxV=7.
The while loop continues, 2 is smaller than 5, write down the value of 5, pop 5 out of the stack, 5*(4-0-1)=15. Here 4 and 0 are the serial numbers corresponding to the second 2 and the first 2, respectively, that is, the serial numbers of the nearest lower decimals to the right and left of 5. maxV=15.
The while loop continues, (1st) 2 is not greater than (2nd) 2, so the second 2 is pushed onto the stack.
6) 2 is smaller than 3. 3 into the stack.
7) In fact, the problem of the left and right borders is also considered here. The maxV corresponding to both sides of the boundary is less than 15, so it has no effect on the result.

To sum up: Why use a monotonically increasing stack? Because this ensures that the next element (toward the bottom of the stack) of each element in the stack is the nearest smaller number to the left of the element. When the top of the stack is larger than the new element, the new element is the nearest smaller number to the right of the element at the top of the stack. . In this way, the nearest smaller decimals on the left and right sides of the top element of the stack are determined at the same time.
So, solution 2 and solution 1 are equivalent, but more subtle.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325640863&siteId=291194637