刷题笔记6(浅谈单调栈)

题目,Leetcode 84. Largest Rectangle in Histogram,85. Maximal Rectangle.

单调栈是一种理解起来很容易,但是运用起来并不那么简单的数据结构。一句话解释单调栈,就是一个栈,里面的元素的大小按照他们所在栈内的位置,满足一定的单调性。那么到底什么时候用这个单调栈,怎么用单调栈呢。下面我们来看几个例子。

先来分享一道非常简单的,我本人在google interview中遇到的题目。(大雾,当时并没有做出来。)

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

简单的例子:

input: 5,3,1,2,4

return: -1 3 1 1 -1

explaination: 对于第0个数字5,之后没有比它更大的数字,因此是-1,对于第1个数字3,需要走3步才能达到4(第一个比3大的元素),对于第2和第3个数字,都只需要走1步,就可以遇到比自己大的元素。对于最后一个数字4,因为之后没有更多的元素,所以是-1。

暴力做的结果就是O(n^2)的时间复杂度,例如对于一个单调递减的数组,每次都要走到数组的末尾。那么用单调栈怎么做呢?先来看代码:

vector<int> nextExceed(vector<int> &input) {
	vector<int> result (input.size(), -1);
	stack<int> monoStack;
	for(int i = 0; i < input.size(); ++i) {	
		while(!monoStack.empty() && input[monoStack.top()] < input[i]) {
			result[monoStack.top()] = i - monoStack.top();
			monoStack.pop();
		}
		monoStack.push(i);
	}
	return result;
}

我们维护这样一个单调递减的stack,stack内部存的是原数组的每个index。每当我们遇到一个比当前栈顶所对应的数(就是input[monoStack.top()])大的数的时候,我们就遇到了一个“大数“。这个”大数“比它之前多少个数大我们不知道,但是至少比当前栈顶所对应的数大。我们弹出栈内所有对应数比这个数小的栈内元素,并更新它们在返回数组中对应位置的值。因为这个栈本身的单调性,当我们栈顶元素所对应的数比这个元素大的时候,我们可以保证,栈内所有元素都比这个元素大。对于每一个元素,当它出栈的时候,说明它遇到了自己的next greater element,我们也就要更新return数组中的对应位置的值。如果一个元素一直不曾出栈,那么说明不存在next greater element,我们也就不用更新return数组了。

这里作者在数组末尾加入了一个height 0,来强迫程序在结束前,将所有元素按照顺序弹出栈。是一个很巧妙的想法。在这个例子中,对于每一个元素都只有一次入栈和出栈的操作,因此时间复杂度只有O(n)。


解决了这个开胃菜,我们来看一道稍微复杂一点的题目。Leetcode 84. Largest Rectangle in Histogram

Given n non-negative integers representing the histogram's bar height where the width of each bar is 1, find the area of largest rectangle in the histogram.


Above is a histogram where width of each bar is 1, given height = [2,1,5,6,2,3].


The largest rectangle is shown in the shaded area, which has area = 10 unit.

For example,
Given heights = [2,1,5,6,2,3],
return 10.

来看看discuss中sipiprotoss5的解法。

int largestRectangleArea(vector<int> &height) {
            int ret = 0;
            height.push_back(0);
            vector<int> index;
            for(int i = 0; i < height.size(); i++) {
                while(index.size() > 0 && height[index.back()] >= height[i]) {
                    int h = height[index.back()];
                    index.pop_back();
                    int sidx = index.size() > 0 ? index.back() : -1;
                    ret = max(ret, h * (i-sidx-1));
                }
                index.push_back(i);
            }
            return ret;
        }

看上去这个解法长得和刚才那道题的差不多。实际算法也很相近,作者维护了一个单调递增的栈,然后进行了一系列xjb操作。那么到底为什么可以这么做?我们需要分析一下,每一个元素都要入栈一次,出栈一次。入栈的时候是for loop的iteration走到它的时候,那出栈的时候意味着什么呢。想清楚了这一点,我们也就理解了上面的答案。在上一题,每个元素出栈,是说明它找到了它在原数组中的next greater element.那这道题呢?元素出栈,意味着,我们已经计算了以它的顶为上边框的最大矩形。首先我们可以通过反证法轻松证明,最后的结果中的最大矩形的上边框,一定和某一个bar的顶重合,否则我们一定可以通过提高上边框来增加这个矩形的面积。这一步之后,我们还需要理解,这时候我们计算的矩形的左右边框都已经到达了极限。结合栈内元素的单调性,我们知道左边的边框是栈顶的元素+1,栈顶元素所对应的bar一定比出栈元素对应的bar小,所以以出栈元素对应的bar为高的矩形无法往左边延展。结合代码,我们知道右边的边框是正在处理的i,因为我们已经判断过这个第i个元素所对应的bar也一定比出栈元素对应的bar小,所以矩形无法往右边延展。这个元素和左右边框之间如果还有空隙,那么这些空隙里所存在的bar,一定是因为维护栈的单调性而被弹出了。换言之,这些bar如果存在,那么一定比这个出栈元素所对应的bar高。既然这些bar的高度更高,那么就可以被纳入这个最大矩形的计算中(例如一个“凹”字)。因此我们证明了,当我们将第i个元素弹出栈的时候,我们计算了以hight[i]为高的最大矩形的面积。

下面我们来看另一个可以借助单调栈的题目,Leetcode 85. Maximal Rectangle.


Given a 2D binary matrix filled with 0's and 1's, find the largest rectangle containing only 1's and return its area.

For example, given the following matrix:

1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0

Return 6.


这道题可以轻松地转化成上一题。对于每一行,我们都可以构建一个histogram,然后计算。在构建新的histogram的时候,我们不需要全部遍历,只需要对已有的histogram进行略微的修改(运用DP的思想)。为了在视觉上更加清晰,我直接调用了上一题中的函数。思路还是异常简单的。

 int maximalRectangle(vector<vector<char>>& matrix) {
        if (matrix.empty())  return 0;
        vector<int> height(matrix[0].size(), 0);
        int maxRect= 0;
        for(int i = 0; i < matrix.size(); ++i) {
            for(int j = 0; j < height.size(); ++j) {
                if(matrix[i][j] == '0')    height[j] = 0;
                else    ++height[j];
            }
            maxRect = max(maxRect, largestRectangleArea(height));
            height.pop_back();
        }
        return maxRect;
    }
    int largestRectangleArea(vector<int> &height) {
            int ret = 0;
            height.push_back(0);
            vector<int> index;
            for(int i = 0; i < height.size(); i++) {
                while(index.size() > 0 && height[index.back()] >= height[i]) {
                    int h = height[index.back()];
                    index.pop_back();
                    int sidx = index.size() > 0 ? index.back() : -1;
                    ret = max(ret, h * (i-sidx-1));
                }
                index.push_back(i);
            }
            return ret;
    }

这道题还有纯粹的DP解法,但是和今天的主题——单调栈无关,这里就不展开讨论了。

最后再总结一下单调栈。单调栈这种数据结构,通常应用在一维数组上。如果遇到的问题,和前后元素之间的大小关系有关系的话,(例如第一题中我们要找比某个元素大的元素,第二个题目中,前后的bar的高低影响了最终矩形的计算),我们可以试图用单调栈来解决。在思考如何使用单调栈的时候,可以回忆一下这两题的解题套路,然后想清楚,如果使用单调栈,每个元素出栈时候的意义。最后的时间复杂度,因为每个元素都出栈入栈各一次,所以是线性时间的复杂度。

编辑于 2017-04-21

文章被以下专栏收录