单调队列和单调栈很相似,他们是什么区别呢?
单调栈用来求左右两侧最近更大/小数的距离,实现方法是:求最小值(最大值)的最大区间,维护一个递增(递减)的栈,当遇到一个比栈顶小的值的时候开始弹栈,弹栈停止的位置到这个值的区间即为此值左边的最大区间;同时,当一个值被弹掉的时候也就意味着比它更小(更大)的值来了,也可以计算被弹掉的值得右边的最大区间。
注意这里单调递增是指从栈底到栈顶为递增排列,单调递减是指从栈底到栈顶为递减排列。
单调栈题目包括:
LintCode 1852. Final Discounted Price
LintCode 510. Maximal Rectangle
LintCode 126. Max Tree
LintCode 122. Largest Rectangle in Histogram
单调队列用来求滑动窗口的最大/小值,实现方法是:求区间最小(最大)值,就维护一个递增的双端队列,队中保存原始序列的标号,当即将入队的元素的值比队尾的元素的值小(大)的时候就不断弹掉队尾,直到出现比它更小的值,当即将入队的元素队首元素的跨度(即将入队元素的序号到队首元素序列的区间)大于规定区间时就不断弹掉队首,直到跨度小于或等于所规定的区间。如此可保证队首元素为最小(最大)值,(但不能保证队尾就是原始序列中的最大(最小)值),并维护区间长度。
注意这里单调递增是指从队首到队尾递增,单调递减是指从队首到队尾递减。
最典型的例题就是滑动窗口求最大/小值。见
https://blog.csdn.net/roufoo/article/details/78443281
注意这个滑动窗口求最大/小值和在全局数据里面求第k大/小的数是完全不一样的,后者要用quick select(限固定数据) 或堆(real stream或固定数据)来实现。
单调队列也可以用来求区间内递增序列包含的元素个数。典型例题有:LintCode 76: (Longest increasing subsequence)
见链接:
https://blog.csdn.net/roufoo/article/details/102882472
补充一些个人总结:
单调栈例题:
例题1:左右侧最近更大数的距离问题。 给一个数组,返回一个大小相同的数组。返回的数组的第i个位置的值应当是,对于原数组中的第i个元素,至少往右走多少步,才能遇到一个比自己大的元素(如果之后没有比自己大的元素,或者已经是最后一个元素,则在返回数组的对应位置放上-1)。
简单的例子:
input: 5,3,1,2,4
return: -1 3 1 1 -1
此题用暴力法复杂度是O(n^2)。用单调栈的话复杂度是O(n)。这里单调栈里面从栈底到栈顶为递减排列。具体执行顺序:
vector NextLarger(vector &data) {
vector output(data.size(), -1); //首先都初始化为-1
stack monoStack;
for (int i=0; i
在上面的代码中,data[monoStack.top()] < data[i] 保证一旦新元素比栈顶大,说明栈顶元素刚刚找到右侧比它大的数,此时对应的output位置马上就要更新。同时该栈顶元素也完成了任务,不能恋栈了,要马上pop出来让下面的元素跟这个新元素比试比试。如此反复,直到while循环里面条件不成立,说明栈已空,或新元素已经小于栈顶元素了 。
因为所有元素最多出栈入栈一次,相当于n个操作平摊在for循环中,所以复杂度还是O(n)。详见算法中的amortized analysis。
另外稍微回顾一下C++的内容。NextLarger()返回的是vector,这里返回的时候会调用拷贝构造函数,所以虽然output是局部变量,但不会出错,因为返回的是局部变量的拷贝。这里返回值不可以加引用,vector &会导致直接返回局部变量,但是函数结束时局部变量已经被析构了。
这题稍微修改一下,就可以变成求左侧更大数的距离问题(for循环倒过来)。
例题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
#include
#include
#include
这里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 toRight(data.size(), -1);
dataMap[true] = toRight;
这里dataMap[true] = toRight是将toRight数组拷贝到dataMap[true]。所以dataMap[true]后来变了,toRight还是没动。
解法2:
解法1向左向右各扫一遍,其实只需要扫一遍就可以了。
int LargestRec2(vector &data) {
stack monoStack; //单调递增栈
int maxV = 0;
//add two dummy boundaries
data.insert(data.begin(), -1);
data.push_back(-1);
for (int i=0; idata[i]) {
int oldTop = monoStack.top();
monoStack.pop();
maxV = max(maxV, data[oldTop]*(i-monoStack.top()-1));
}
monoStack.push(i);
}
return maxV;
}
int main()
{
vector data = {2,7,5,6,2,3};
cout<
注意:解法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。
再总结一下:为啥要用单调递增栈呢?因为这样可以保证栈内每个元素的下面一个元素(往栈bottom方向)就是该元素左侧最近的更小数,当栈顶比新元素大时,新元素就是栈顶元素右侧最近的更小数。这样,栈顶元素的左右两侧最近的更小数都同时确定了。
所以,解法2和解法1是等价的,但更巧妙。