问题来源: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. 基于gin框架和jwt-go中间件实现小程序用户登陆和token验证

    本文核心内容是利用jwt-go中间件来开发golang webapi用户登陆模块的token下发和验证,小程序登陆功能只是一个切入点,这套逻辑同样适用于其他客户端的登陆处理. 小程序登陆逻辑 小程序的 ...

  2. spring加载xml的六种方式

    因为目前正在从事一个项目,项目中一个需求就是所有的功能都是插件的形式装入系统,这就需要利用Spring去动态加载某一位置下的配置文件,所以就总结了下Spring中加载xml配置文件的方式,我总结的有6 ...

  3. pdf如何转换为word文档

    我们经常会遇到需要将PDF转换为WORD文档,对于我来讲,有些PDF没有目录,看起来非常不方便,于是就特别想转成WORD,然后增加目录,想看某一节内容时,快速查找. 这里我总结了一些方法,后续也会不断 ...

  4. TensorFlow学习笔记(UTF-8 问题解决 UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte)

    我使用VS2013  Python3.5  TensorFlow 1.3  的开发环境 UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff ...

  5. [HNOI2008]神奇的国度

    题目描述 K国是一个热衷三角形的国度,连人的交往也只喜欢三角原则.他们认为三角关系:即AB相互认识,BC相互认识,CA相互认识,是简洁高效的.为了巩固三角关系,K国禁止四边关系,五边关系等等的存在. ...

  6. UVALive - 3942:Remember the Word

    发现字典里面的单词数目多且长度短,可以用字典树保存 f[i]表示s[i~L]的分割方式,则有f[i]=∑f[i+len(word[j])]   其中word[j]为s[i~L]的前缀 注意字典树又叫前 ...

  7. 2015 多校联赛 ——HDU5353(构造)

    Each soda has some candies in their hand. And they want to make the number of candies the same by do ...

  8. 【SDOI2009】学校食堂

    Description 小F的学校在城市的一个偏僻角落,所有学生都只好在学校吃饭.学校有一个食堂,虽然简陋,但食堂大厨总能做出让同学们满意的菜肴.当然,不同的人口味也不一定相同,但每个人的口味都可以用 ...

  9. Codeforces Round #430 B. Gleb And Pizza

    Gleb ordered pizza home. When the courier delivered the pizza, he was very upset, because several pi ...

  10. SQL_SERVER_2008升级SQL_SERVER_2008_R2的方法

    SQL 2008升级到SQL 2008 R2. 说到为什么要升级是因为,从另一台机器上备份了一个数据库,到我的机器上还原的时候提示"948错误,意思就是不能把高版本的数据库附加到低版本上,所 ...