LeetCode_直方图最大面积(Largest Rectangle in Histogram)

来源:互联网 发布:上海知柚网络公司市值 编辑:程序博客网 时间:2024/06/03 13:05

一、题目

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 height = [2,1,5,6,2,3],return 10.

1.1.1

LeetCode 84题:Largest Rectangle in Histogram,给定一个直方图(下图a),求直方图中能够组成的所有矩形中,面积最大为多少。对于图a来说,我们很容易看出来面积最大的矩形为高度为5和6的直方图组成的矩形(图b隐形部分),其面积为5 * 2 = 10。


二、解题

1.感想

如果就对于图片的示例而言,许多人一看就知道答案了,压根就不需要太多的思考,可是正是因为这种定性的思考,让我们慢慢没法“知其所以然”,也让我们慢慢失去了举一反三的思考方式,就图片的示例而言,之所以我们一看就能看出答案,是因为示例的例子数据太少了,如果数据很多的时候我们能一看就看出答案吗?所以无论面对什么样的题目,我们都不能放弃“知其所以然”,从根本解决问题。

2.暴力搜索

这道题目一个显而易见的解决方法就是暴力搜索:找出所有可能的矩形,然后求出面积最大的那个。要找出所有可能的矩形,只需要从左到右扫描每个立柱,然后以这个立柱为矩形的左边界(假设为第i个),再向右扫面,分别以(i+1, i+2, n)为右边界确定矩形的形状。

这符合我们本能的思考过程:要找出最大的一个,就先列出所有的可能,比较大小后求出最大的那个。然而不幸的是,本能的思考过程通常是简单粗暴而又低效的,就这个题目来说,时间复杂度为N^2 。

3.使用动态规划

用left[i]表示第i个柱子可以最多向左延伸至第left[i]个柱子,形成一个矩形,right[i]则表示向右延伸。遍历两次,分别计算出这两个数组。

再遍历一次,即可求出所有的柱子可以形成的最大的矩形面积。为了减少边界的判断,可以使用哨兵,在两端添加两个柱子高度都为-1.

public class LargestRectangle {    public static void main(String[] args) {        int height[] = { 2, 1, 5, 6, 2, 3 };        int ans = getMaxRectangle(height);        // lift[]=[0,1,1,3,4,3,6] right[]=[0,1,6,4,4,6,6]        System.out.println("最大的面积位:" + ans);    }    /**     * 遍历每个柱子可以组成的最大面积     *      * @param heights     * @return 最大的面积     */    public static int getMaxRectangle(int heights[]) {        int ans = 0;        int n = heights.length;        int left[] = new int[n + 1];        int right[] = new int[n + 1];        processLR(heights, left, right);        for (int i = 1; i <= n; i++) {            int tmp = (right[i] - left[i] + 1) * heights[i - 1];            if (ans < tmp)                ans = tmp;        }        return ans;    }    /**     * 初始化左右两个数组     *      * @param heights     * @param left     * @param right     */    public static void processLR(int heights[], int left[], int right[]) {        int n = heights.length;        // 用临时数组,设置两个哨兵        int tempArr[] = new int[n + 2];        tempArr[0] = -1;        for (int i = 1; i <= n; i++)            tempArr[i] = heights[i - 1];        tempArr[tempArr.length - 1] = -1;        // 用left[i]表示第i个柱子可以最多向左延伸至第left[i]个柱子        for (int i = 1; i <= n; i++) {            int k = i;            while (tempArr[i] <= tempArr[k - 1])                k = left[k - 1];            left[i] = k;        }        // 用right[i]表示第i个柱子可以最多向右延伸至第right[i]个柱子        for (int i = n; i > 0; i--) {            int k = i;            while (tempArr[i] <= tempArr[k + 1])                k = right[k + 1];            right[i] = k;        }    }}

运行的结果:

2.3.1

4.巧妙使用栈

在网上发现另外一个使用一个栈的O(n)解法,代码非常简洁,栈内存储的是高度递增的下标。对于每一个直方图高度,分两种情况。1:当栈空或者当前高度大于栈顶下标所指示的高度时,当前下标入栈。否则,2:当前栈顶出栈,并且用这个下标所指示的高度计算面积。而这个方法为什么只需要一个栈呢?因为当第二种情况时,for循环的循环下标回退,也就让下一次for循环比较当前高度与新的栈顶下标所指示的高度,注意此时的栈顶已经改变由于之前的出栈。

public class largestRectangleArea {    public static void main(String[] args) {        int height[] = { 2, 1, 5, 6, 2, 3 };        int ans = largestRectangleArea(height);        System.out.println("最大的面积位:" + ans);    }    // O(n) using one stack    public static int largestRectangleArea(int[] height) {        // Start typing your Java solution below        // DO NOT write main() function        int area = 0;        java.util.Stack<Integer> stack = new java.util.Stack<Integer>();        for (int i = 0; i < height.length; i++) {            if (stack.empty() || height[stack.peek()] < height[i]) {                stack.push(i);            } else {                int start = stack.pop();                int width = stack.empty() ? i : i - stack.peek() - 1;                area = Math.max(area, height[start] * width);                i--;            }        }        while (!stack.empty()) {            int start = stack.pop();            int width = stack.empty() ? height.length : height.length                    - stack.peek() - 1;            area = Math.max(area, height[start] * width);        }        return area;    }}

这是看到别人的推演过程,之前看到,就添加到笔记了,没有原著的地址,所以没法标明出处:

一个思维历程

那么这个算法真的就是我等凡夫俗子不能想出来的?难道我们只能仰望高山,恨自己智商不高?我还真不服气呢,于是又静下心去思考这个问题。

这次我们不从已知条件推结果,而直接从结论入手,就是说假设现在已经找到了面积最大的那个矩形。接着我们来分析该矩形有什么特征,然后可以用下面两种方法之一来缩减问题的规模(因为这两种方法都不用找出所有的矩形一一比较)。

找出满足这些特征的矩形,面积最大的矩形肯定是其中之一;
排除那些不满足这些特征的矩形,面积最大的矩形在剩下的那些矩形里面。
为了使考虑情况尽可能全面,画了许多直方图,防止使用原题目图片可能存在的一些特定假设,其中一个直方图如下图:

2.4.1

题目情况分析

通过不断地对多个直方图的观察,发现面积最大的那个矩形好像都包含至少一个完整的bar,那么这条规律适用于所有的直方图吗?我们用反证法来证明,假设某个最大矩形中每个竖直块都是所在的bar的一小段,那么这个矩形高度增加1后仍然是一个合法的矩形,但新的矩形面积更大,与假设矛盾,所以面积最大的矩形必须至少有一个竖直块是整个bar。

至此我们找到了面积最大矩形的一个特性:各组成竖直块中至少有一个是完整的Bar。有了这条特性,我们再找面积最大的矩形时,就有了一个比较小的范围。具体来说就是针对每个bar,我们找出包含这个bar的面积最大的矩形,然后只需要比较这N个矩形即可(N为bar的个数)。

那么问题又来了,如何找出“包含某个bar的面积最大的矩形呢”?对于上面的直方图,包含下标为4的bar的最大矩形如下图橘黄色部分:

2.4.2

局部最大矩形

简单观察一下,就会发现要找到包含某个bar的最大矩形其实很简答,只需要找到高度小于该bar的左、右边界即可,上图中分别是下标为1的bar和下标为10的bar。

至此问题已经变为“对于给定的bar,如何确定高度比它小的左、右边界”。其实求左边界和右边界是同样的求法,下面我们考虑求每个bar的左边界。最直接的思路是对于每个bar,扫面其前面所有的bar,找出最后一个高度小于它的bar,这样的话时间复杂度明显又是N^2 ,Holy Shit。

到这里似乎没有路可走了,但如果我们继续绞尽脑汁地去想,可能(或许你对栈理解的很深入,或许是你在一个类似的问题中用到了栈,当然你也可能想到动态规划的思想,那也是可行的)会联想到栈这一数据结构。用栈维护一个高度递增的bar的集合,也就是说栈底到栈顶部对应的bar的高度越来越大。那么对应一个刚读入的bar,我们只需要比较它的高度和栈顶对应bar的高度,如果当前bar比较高,则弹出栈顶元素继续比较,直到栈顶bar比它低或者栈为空。之后,将当前bar入栈,更新栈内的递增序列。

我们从左到右扫一遍得到每个bar对应的左边界,然后从右到左扫一遍得到bar的右边界。两次扫描过程中,每个bar都只有出栈、入栈操作,所以时间复杂度为O(N)。通过这样的预处理,即可以O(N)的时间复杂度得到每个bar的左右边界。之后对于每个bar求出包含它的最大面积,也即是由左右边界和bar的高度围起来的矩形的面积。再做N次比较,即可得出最终的结果。

这里先预处理用两个栈扫描两次得到左、右边界,再计算面积,是按照推导过程一步一步来的。当我们写完程序后,再综合看这个问题,可能会发现其实没必要这样分开来做,我们可以在扫描的同时,维护一个递增的栈,同时在“合适的”时候计算面积,然后更新最大面积。具体实现方法就是前面给出的那个神奇的算法,不过现在看来一点也不神奇了,我们已经探索到了它背后的思维历程。

当然,条条道路通罗马,上面思维过程只是其中一条通往解决方案的路径,你可能以另一种思维过程找到了答案。不过,我们上面的整个推导过程没有涉及一些类似“神谕”的启发,只是一些简单的方法:比如从结论推导、反证法、归纳总结、联想(可能联想到栈有点难)等,因此每个人都可以学会,并且很容易被大脑记住。值得注意的是,我们的整个思考过程并不简简单单地跟上面写的那样是线性的,它更可能是树形的,只是我们剪去了那些后来证明行不通的枝。

Demo:http://download.csdn.net/detail/two_water/9670562


三、思考

最后奉上看到别人写的东西,略有感想,故分享。

3.1.1

0 0
原创粉丝点击