单调栈的学习及例题(左右侧最近更大数的距离问题和直方图最大矩形问题)

单调队列和单调栈很相似,他们是什么区别呢? 首先引用
http://blog.sina.com.cn/s/blog_6ffc3bde01015l2m.html
的话:
单调栈解决的是以某个值为最小(最大)值的最大区间,实现方法是:求最小值(最大值)的最大区间,维护一个递增(递减)的栈,当遇到一个比栈顶小的值的时候开始弹栈,弹栈停止的位置到这个值的区间即为此值左边的最大区间;同时,当一个值被弹掉的时候也就意味着比它更小(更大)的值来了,也可以计算被弹掉的值得右边的最大区间。
单调队列解决的是区间最小(最大)值,实现方法是:求区间最小(最大)值,就维护一个递增的双端队列,队中保存原始序列的标号,当即将入队的元素的值比队尾的元素的值小(大)的时候就不断弹掉队尾,知道出现比它更小的值,当即将入队的元素队首元素的跨度(即将入队元素的序号到队首元素序列的区间)大于规定区间时就不断弹掉队首,直到跨度小于或等于所规定的区间。如此可保证队首元素为最小(最大)值,(但不能保证队尾就是原始序列中的最大(最小)值),并维护区间长度。

补充一些个人总结:
1) 单调队列可以从队首和队尾pop值,而单调栈只能从栈顶pop值。从这个意义来看,单调队列并不是严格意义的队列(不能用queue而必须用deque),而单调栈却是严格意义的栈(可以用stack,当然也可以用deque)。
2) 单调队列是从队尾push值,单调栈是从栈顶push值。从这点来看,单调队列的队尾跟单调栈的栈顶是一样的。
3) 单调队列通常还有区间长度限制 ,而单调栈不一定有区间长度限制(我看到的题目好像都没有)。所以单调栈其实更简单,因为不需要实时考虑区间溢出。
4) 单调队列求区间最大值用递减队列,求区间最小值用递增队列。
单调栈求左(或右)侧比当前值大的边界用递减队列,求左(或右)比当前值小的边界用递增队列。为啥求比当前值大的边界是递减队列呢?因为这样才能保证栈顶比新元素小的时候,栈顶的下一个元素(和下下一个元素…,都比栈顶大)能够挨个和新元素比较。

单调栈例题:
例题1:左右侧最近更大数的距离问题。 给一个数组,返回一个大小相同的数组。返回的数组的第i个位置的值应当是,对于原数组中的第i个元素,至少往右走多少步,才能遇到一个比自己大的元素(如果之后没有比自己大的元素,或者已经是最后一个元素,则在返回数组的对应位置放上-1)。

简单的例子:
input: 5,3,1,2,4
return: -1 3 1 1 -1

此题用暴力法复杂度是O(n^2)。用单调栈的话复杂度是O(n)。这里单调栈里面从栈底到栈顶为递减排列。具体执行顺序:
1) 5(对应的序号)入栈。
2) 因为3比5小,3(对应的序号)入栈。
3) 因为1比3小,1(对应的序号)入栈。
4) 因为2比1大,1对应的距离就是2的序号-1的序号=1,记录在1对应的output数组中。然后1出栈,3成为栈顶。因为2比3小,所以3不出栈,2入栈。
5) 因为4比2大,2对应的距离就是4的序号-2的序号=1,记录在2对应的output数组中,然后2出栈,3成为栈顶。然后4还是比3大,3对应的距离就是4的序号-3的序号=3,记录在3对应的output数组中,然后3出栈。因为4没有5大,所以5不出站。4入栈。
6) 程序跑完了,5和4在栈中,它们对应的output数组的元素还是-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;
}

在上面的代码中,data[monoStack.top()] < data[i] 保证一旦新元素比栈顶大,说明栈顶元素刚刚找到右侧比它大的数,此时对应的output位置马上就要更新。同时该栈顶元素也完成了任务,不能恋栈了,要马上pop出来让下面的元素跟这个新元素比试比试。如此反复,直到while循环里面条件不成立,说明栈已空,或新元素已经小于栈顶元素了 。

因为所有元素最多出栈入栈一次,相当于n个操作平摊在for循环中,所以复杂度还是O(n)。详见算法中的amortized analysis。

另外稍微回顾一下C++的内容。NextLarger()返回的是vector,这里返回的时候会调用拷贝构造函数,所以虽然output是局部变量,但不会出错,因为返回的是局部变量的拷贝。这里返回值不可以加引用,vector &会导致直接返回局部变量,但是函数结束时局部变量已经被析构了。

这题稍微修改一下,就可以变成求左侧更大数的距离问题(for循环倒过来,并且data[monoStack.top()]>data[i])。

例题2: Largest Rectangle in Histogram,给定一个直方图,假定每个矩形宽度为1,求直方图中能够组成的所有矩形中,面积最大为多少。
简单的例子:
input: 2,1,5,6,2,3
return: 10

容易看出面积最大的矩形为高度为5和6的直方图组成的矩形,其面积为5 * 2 = 10。

解法1:这题实际上等价于:对每个矩形求左右最近的一个比他低的矩形的边界,然后左右两侧距离相加(还要-1,因为自身算了2遍)×该矩形高度。然后找出所有矩形中该操作的最大值。 这样我们前面例题1就可以马上拿来用了。注意这里是要求每个元素,左右两侧比它小的元素,所以要用递增队列 (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;
}

这里dataMap[true][i]和dataMap[false][i]分别对应元素i往右和往左遇到最近的小于它的元素的距离。

注意上面是求左右两侧最近更小数的距离问题,所以是data[monoStack.top()]>data[i]。该不等式表面一旦新元素比栈顶元素小,说明栈顶元素已经找到一侧最近更小数了,此时要马上记录下栈顶元素在output数组中对应的距离,并pop栈顶数组,如此反复,直到栈空或新元素比栈顶元素大。

注意这题要特别注意的是边界条件,即左右边界特别大的情况。比如说input是 200,1,5,6,2,3 或 2,1,5,6,2,300,则output应该分别是200, 300。所以在LargestRec1()中特地在data[]的左右两侧加入两个dummy -1,确保左右两侧会被考虑到。

另外回顾一下C++的内容。上面的例子中为了练习stl,用了map

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

这里dataMap[true] = toRight是将toRight数组拷贝到dataMap[true]。所以dataMap[true]后来变了,toRight还是没动。

解法2:
解法1向左向右各扫一遍,其实只需要扫一遍就可以了。

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;
}

注意:解法2的

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

是不是和解法1的
data[i]*(dataMap[true][i]+dataMap[false][i]-1)
很相似? 这里实际上i-oldTop就是oldTop到右边比它小的最近一个元素的距离,oldTop-monoStack.top()就是oldTop到左边比它小的最近一个元素的距离。两者相加要减一,因为oldTop本身算了2次。

以input为[2,7,5,6,2,3]为例,解法2步骤为:
0) maxV = 0。
1) 2(对应序号)入栈。
2) 2比7小,7(对应序号)入栈。
3) 5比7小,记下7的数值,7出栈。7*(2-0-1)=7。 这里2和0分别是5和第1个2对应的序号,也就是7右侧和左侧最近的更小数的序号。maxV=7。注意:为简便起见,这里的序号没有考虑dummy边界。
4) 6比5大,6入栈;
5) 2比6小。记下6的数值,6出栈。6*(4-2-1)=6。这里4和2分别是第2个2和5对应的序号,也就是6右侧和左侧最近的更小数的序号。maxV=7。
while循环继续,2比5小,记下5的数值,5出栈,5*(4-0-1)=15。这里4和0分别是第2个2和第一个2对应的序号,也就是5右侧和左侧最近的更小数的序号。maxV=15。
while循环继续,(第1个)2不大于(第2个)2,所以第2个2入栈。
6) 2比3小。3入栈。
7) 这里实际上还要考虑左右两边边界的问题。在此两边边界对应的maxV都小于15,所以对结果无影响。

再总结一下:为啥要用单调递增栈呢?因为这样可以保证栈内每个元素的下面一个元素(往栈bottom方向)就是该元素左侧最近的更小数,当栈顶比新元素大时,新元素就是栈顶元素右侧最近的更小数。这样,栈顶元素的左右两侧最近的更小数都同时确定了。
所以,解法2和解法1是等价的,但更巧妙。

猜你喜欢

转载自blog.csdn.net/roufoo/article/details/78569554