问题来源:Largest Rectangle in Histogram

问题描述:给定一个长度为n的直方图,我们可以在直方图高低不同的长方形之间画一个更大的长方形,求该长方形的最大面积。例如,给定下述直方图,



我们可以以高度5宽度2画一个更大的长方形,如下图,该长方形即是面积最大的长方形。该问题是难度比较大的问题,但是很出名,经常作为面试题出现。最近陈利人老师给出该问题的一个O(n)解法,非常巧妙,并从二维三维角度对问题进行了扩展。我们在陈老师的基础上,对该问题进行深入分析,给出多种方法,拓展大家的视野。

1. 解法一

如果在面试的过程中被问到这个题目,除非之前见过,否则一时很难想到解法。我们不妨从最笨的解法入手。在我看来,能把最笨的解法写出来也是很不错的,毕竟很多人见到这种题一下就蒙了。

最笨的解法是什么呢,就是遍历所有的起始和结束位置,然后计算该区域内长方形的面积,找到最大的。具体实现过程中,从第0个位置开始遍历起点,选定起点之后,从起点到末尾都可以当做终点,所以总共有O(n2)种可能。在固定了起点之后,后续对终点的遍历有一个特点:构成长方形的高度都不高于之前所构造长方形的高度,所以长方形的高度即是到当前终点为止的最小值,宽度即是终点位置减去起点位置加1。按照这个思路实现的C++代码如下:

class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int len=heights.size();
int max_size=0;
for(int i=0;i<len;i++)
{
int min_height=heights[i];
int current_size=min_height;
for(int j=i;j<len;j++)
{
if(heights[j]<min_height) min_height=heights[j];
current_size=min_height*(j-i+1);
if(current_size>max_size) max_size=current_size;
}
} return max_size;
}
};

上述代码可以通过几乎所有的测试用例,但是当测试用例为0到19999的连续整数时会超时。说明O(n2)的复杂度还是过高,需要进一步优化。下面我们看一下陈利人老师的解法,原文在《很神奇的解法!怎么求柱状图中的最大矩形?》。

新解法的核心在于考虑了直方图两个相邻长方形AB之间的关系。如果前一个长方形A低后一个长方形B高,则A肯定不会是某个大长方形的终点,因为我们可以安全地在A后面添加更高的B,使大长方形的宽度加1。如果A高B低,则A是可能的终点,假设我们就用A当做终点,并且以该长方形的高度当做大长方形的高度,看看可以往前延伸多长。根据上面这两条性质,我们可以维护一个递增序列(实际为非递减,当前后两个长方形的高度一样时,前一个长方形同样也不可能是终点,在此为了解释方便假定前后高度都不一样),当B高时就将B的位置添加到序列中,否则就弹出A的位置,并用A的位置作为终点,A的高度作为大长方形的高度计算面积。起点怎么确定呢,由于我们维护的是一个递增序列,在弹出A之后,序列中A的前一个位置所对应的长方形高度肯定低于A的高度,所以A的前一个长方形的位置加1即是大长方形的起点。因为我们每次都是对序列的末尾进行操作,所以可以用一个栈来维护此递增序列。大家可以通过下图仔细体会上面的分析。如果还是不理解,可以阅读上面提到的原文。



陈老师的博客中给出的是python代码,我们将其改写成C++代码:

class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
heights.push_back(-1);
int max_size=0;
int index=0;
stack<int> s; while(index<heights.size())
{
if(s.size()==0||heights[s.top()]<=heights[index])
{
s.push(index);
index++;
}
else
{
int top=s.top();
s.pop();
int size=0; if(s.size()==0)
{
size=heights[top]*index;
}
else
{
size=heights[top]*(index-s.top()-1);
} if(size>max_size) max_size=size;
}
} return max_size;
}
};

上述代码的核心就是判断前后两个长方形的高度,后一个高就添加到堆栈中,否则就弹出计算面积。该代码提交到leetcode上运行时间是24ms。上述代码用了STL中的stack,我们也可以用数组代替,此时代码可以修改如下:

class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
heights.push_back(-1);
int max_size=0;
int index=0;
int *s=(int*)malloc(sizeof(int)*heights.size());
int stack_index=0; while(index<heights.size())
{
if(stack_index==0||heights[s[stack_index-1]]<=heights[index])
{
s[stack_index++]=index;
index++;
}
else
{
int top=s[--stack_index];
int size=0; if(stack_index==0)
{
size=heights[top]*index;
}
else
{
size=heights[top]*(index-s[stack_index-1]-1);
} if(size>max_size) max_size=size;
}
} free(s); return max_size;
}
};

由于少了系统调用,上述代码运行时间降为16ms。需要注意的一点是,stack_index指向的是堆栈头的上一个位置。

上面的两个代码虽然都能正确运行,但是有一个坏处,破坏了原始的输入数组。为什么要向原数组中添加一个-1呢?这个也比较容易理解,假如原始数组是递增的,我们不可能只添加不弹出,添加一个-1就可以弹出所有的元素。此处也可以对代码进行修改,避免破坏原始数组。方法就是遍历完所有的元素之后,判断堆栈是否为空,不为空就弹出并计算面积,比较简单,请大家自己实现。

2. 解法二

不知道大家对最长回文子串的几种解法是否了解。如果不考虑最优解法,最长回文子串问题可以有两种不同的思路:1. 确定头和尾,判断该子串是否为回文串;2. 指定回文串的中点,看能往两侧延伸多长。在最大矩形问题中,上面的解法一和回文子串的思路一相似,同理,我们也可以仿照思路二来解决最大矩形问题。我们可以将直方图的任意一个长方形当做中点,然后以该长方形的高度当做大长方形的高度,看可以往两侧延伸多长。这种思路其实更符合大家的思维方式。很明显,这种方法的复杂度也是O(n2),提交代码还是超时,我们对其进行优化。

2.1 基于堆栈的解法

上面原始解法慢的原因也是没有考虑直方图相邻长方形之间的关系,我们分下面两种情况考虑,看是否有优化余地。当出现A这种情形时,其实我们可以获得一些有用的信息,这表明第i个长方形不能再往左扩展(以第i个长方形的高度往两侧扩展),进而我们可以求得left[i](在此left有两种不同的含义,既可以为向左扩展到的位置,也可以为向左扩展的长度,后续代码实现按第一种理解方式)。当出现B情形时,表明第i-1个长方形高,第i个长方形可以继续往左扩展,直到遇到A情形,然后计算left[i]。在实际情况中,由于A情形和B情形随机出现,所以前后两个长方形的位置并不一定相邻。采用数学归纳法我们可以求得所有的left值。可以看出,A情形只往末尾添加元素,B情形只在末尾弹出元素,从而我们可以用一个栈来维护单调递增(实际为非递减)队列。



同理,我们可以通过倒序遍历数组来计算right。按照上述思路实现的代码如下:

class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int n=heights.size();
int* stack=new int[n];
int* right=new int[n];
int* left=new int[n]; int s=0;
for(int i=0;i<n;i++)
{
while(s>0&&heights[stack[s-1]]>=heights[i]) s--;
left[i]=(s==0?0:stack[s-1]+1);
stack[s++]=i;
} s=0;
for(int i=n-1;i>=0;i--)
{
while(s>0&&heights[stack[s-1]]>=heights[i]) s--;
right[i]=(s==0?n:stack[s-1]);
stack[s++]=i;
} int size=0,max_size=0; for (int i=0;i<n;i++)
{
size=(right[i]-left[i])*heights[i];
if(size>max_size)max_size=size;
} delete[] stack;
delete[] right;
delete[] left; return max_size;
}
};

上面代码看似是两重循环,但其实每个元素只进入堆栈一次,通过均摊分析可以得到复杂度为O(n)。提交到leetcode上运行时间为16ms。

2.2 基于单调队列的解法

除了上面巧妙的堆栈解法外,还可以用单调队列来解决。单调队列维护数列的下标,队列内的元素满足:

设单调队列从头部开始的元素值为xi,则xi<xi+1且axi<axi+1。

简单来说单调队列就是下标对应的元素是严格递增的顺序。当然在实际应用过程中,可能不严格单调。

在该问题中,该怎样应用单调队列呢?我们还是分两种情形考虑。考虑方法正好和基于堆栈的方法相反。假设我们维护了递增的单调队列(实际为非递减),当出现B情形时,我们其实可以知道第i-1个长方形不能再往右扩展,从而可以求得right[i-1];当出现A情形时,第i-1个长方形还可以继续向右扩展。只要高度递增,就往单调队列末尾添加元素,否则就计算单调队列末尾元素的right值。当我们遍历完所有的元素之后,单调队列中存在着一个递增的序列,表示剩余位置可以从队列头扩展到队列尾。依次计算扩展长度。



同理,我们可以通过倒序遍历数组来计算left。按照上述思路实现的代码如下:

class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int* deq=new int[heights.size()];
int* right=new int[heights.size()];
int* left=new int[heights.size()];
int n=heights.size(); int s=0,t=0;
for(int i=0;i<n;i++)
{
while(s<t&&heights[deq[t-1]]>heights[i])
{
right[deq[t-1]]=i;
t--;
}
deq[t++]=i;
} while (s<t)
{
right[deq[s]]=deq[t-1]+1;
s++;
} s=0;
t=0;
for (int i=n-1;i>=0;i--)
{
while (s<t&&heights[deq[t-1]]>heights[i])
{
left[deq[t-1]]=i+1;
t--;
}
deq[t++]=i;
} while (s<t)
{
left[deq[s]]=deq[t-1];
s++;
} int size=0,max_size=0;
for (int i=0;i<n;i++)
{
size=(right[i]-left[i])*heights[i];
if(size>max_size)max_size=size;
} delete[] deq;
delete[] right;
delete[] left; return max_size;
}
};

代码的整体逻辑和基于堆栈的实现类似,只是由首先计算left变为首先求right。复杂度也是O(n),提交到leetcode上运行时间也是16ms。

3. 扩展

该问题非常有趣,可以做很多扩展。例如将长方形的宽度由1变为任意宽度,再或者将二维直方图变为三维直方图求最大长方体。针对这两个扩展的解法,大家可以参考陈利人老师“待字闺中”微信公众号中的分析。先给出两个原始链接:《举一反三,从一道面试题说起》和《快看,快看!求3D柱状图中的最大长方体有解了》。

针对第一个扩展,可以很容易采用上面的解法二实现。首先遍历一遍所有的长方形,累积长方形的宽度得到每个长方形在不同宽度下的起始位置。然后继续采用解法二中任意一种实现获得每个长方形往两侧的扩展位置。最后利用计算的起始位置和扩展位置即可计算最大面积,复杂度还是O(n)。

leetcode之Largest Rectangle in Histogram的更多相关文章

  1. LeetCode 84. Largest Rectangle in Histogram 单调栈应用

    LeetCode 84. Largest Rectangle in Histogram 单调栈应用 leetcode+ 循环数组,求右边第一个大的数字 求一个数组中右边第一个比他大的数(单调栈 Lee ...

  2. Java for LeetCode 084 Largest Rectangle in Histogram【HARD】

    For example, Given height = [2,1,5,6,2,3], return 10. 解题思路: 参考Problem H: Largest Rectangle in a Hist ...

  3. 关于LeetCode的Largest Rectangle in Histogram的低级解法

    在某篇博客见到的Largest Rectangle in Histogram的题目,感觉蛮好玩的,于是想呀想呀,怎么求解呢? 还是先把题目贴上来吧 题目写的很直观,就是找直方图的最大矩形面积,不知道是 ...

  4. [LeetCode] 84. Largest Rectangle in Histogram 直方图中最大的矩形

    Given n non-negative integers representing the histogram's bar height where the width of each bar is ...

  5. LeetCode之Largest Rectangle in Histogram浅析

    首先上题目 Given n non-negative integers representing the histogram's bar height where the width of each ...

  6. [LeetCode OJ] Largest Rectangle in Histogram

    Given n non-negative integers representing the histogram's bar height where the width of each bar is ...

  7. [LeetCode#84]Largest Rectangle in Histogram

    Problem: Given n non-negative integers representing the histogram's bar height where the width of ea ...

  8. LeetCode 84. Largest Rectangle in Histogram 直方图里的最大长方形

    原题 Given n non-negative integers representing the histogram's bar height where the width of each bar ...

  9. [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. eclipse下maven插件搭建springmvc之helloworld

    这几天学习了怎样使用maven,最终还是要回归web项目嘛,所以主要还是使用eclipse插件. 1 下载eclipse maven插件. 其实新版的eclipse已经集成了maven:lunar.m ...

  2. javascript实现有限状态机

    1.状态机描述 简单说,有限状态机是一种模型,模型都用来模拟事物,能够被有限状态机这种模型模拟的事物,一般都有以下特点: 1)可以用状态来描述事物,并且任一时刻,事物总是处于一种状态: 2)事物拥有的 ...

  3. 是否可能两个ETH私钥对应同一个地址

    原提问在这里. 笔者在使用到neon-js中的私钥生成方法时发现其使用了getRandomValues方法来生成64字符长度的私钥,进而考虑到其随机性,若是调用足够多次,依然有可能生成两个完全一样的私 ...

  4. [LeetCode] Largest Palindrome Product 最大回文串乘积

    Find the largest palindrome made from the product of two n-digit numbers. Since the result could be ...

  5. [HNOI 2011]XOR和路径

    Description 给定一个无向连通图,其节点编号为 1 到 N,其边的权值为非负整数.试求出一条从 1 号节点到 N 号节点的路径,使得该路径上经过的边的权值的“XOR 和”最大.该路径可以重复 ...

  6. [PA 2014]Iloczyn

    Description 斐波那契数列的定义为:k=0或1时,F[k]=k:k>1时,F[k]=F[k-1]+F[k-2].数列的开头几项为0,1,1,2,3,5,8,13,21,34,55,…你 ...

  7. 51nod 1981 如何愉快地与STL玩耍

    Description 驴蛋蛋在愉快地与STL玩耍 突然间小A跳了出来对驴蛋蛋说,看你与STL玩的很开心啊,那我给你一个大小为N的vector,这个vector上每个位置上是一个set, 每次我会在闭 ...

  8. bzoj 1058: [ZJOI2007]报表统计

    Description 小Q的妈妈是一个出纳,经常需要做一些统计报表的工作.今天是妈妈的生日,小Q希望可以帮妈妈分担一些工 作,作为她的生日礼物之一.经过仔细观察,小Q发现统计一张报表实际上是维护一个 ...

  9. 【网络流】【BZOJ1001】狼抓兔子

    继续网络流的学习.... 题意简析:就是给你张图,叫你求最小割. 解题思路:最小割=最大流,按题意见图跑一次就好了. 附代码: #include<cstdio> #include<i ...

  10. ubuntu Linux下C语言open函数打开或创建文件与read,write函数详细讲解

    open(打开文件) 相关函数 read,write,fcntl,close,link,stat,umask,unlink,fopen 表头文件 #include<sys/types.h> ...