支线任务-3
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.
大概形如上图所示。5,6构成的就是所求的最大矩形区域。题目简单暴力。
开始先写个朴素的O(N^2)算法,发现果然过不了。。。
其实隐约的感觉到应该是要快速的找出第一个比第 i 位矮的位置在哪里,然后通过(右边的位置减去左边的位置)*height[i] 得到第 i 位的答案,这样答案就在O(N)内可以计算出来。
只要得到了这个window数组,答案就显而易见了。只需求出Max(window[i] * height[i])就可以了。
然后有了一点类似kmp那样重复利用已有信息的想法,可是还是不太清楚具体要怎么实现。
直到手贱点开了tag,看到了其中的stack才恍然大悟。(虽然后面发现不用显示的写出这个栈貌似复杂度也是一样的)
我最初的方法是:
每次进栈的时候直到栈顶不高于当前高度为止弹栈,这时候得到的栈顶就是前一个比当前高度低的高度,然后压入当前元素。再从后向前做一次得到右侧同样的结果加起来就是整个的window数组。
用之前的样例举个例子的话就是这样:
- 2入栈,且2的前继记为0,left[0] = 0
- 1入栈时候2弹出,由于此时栈为空,所以1的前继也为0,长度为1,所以 left[1] = 1
- 5入栈时候压在栈顶,所以5的前继是1,意味着5左侧直接就比它低了,所以 left[2] = 0
- 6入栈时候压在栈顶,所以6的前继是5,同理 left[3] = 0
- 2入栈的时候5,6弹出,所以2的前继是1,left[4] = 2
- 3入栈的时候压栈顶,所以left[5] = 0
- 按照对称的步骤得到right = {0, 4, 1, 0, 1, 0}
- window[i] = left[i] + 1 + right[i]
- 得到window = {1, 6, 2, 1, 4, 1},发现这个数组就是我们想要的
- answer = max(window[i] * height[i]),得到最终答案
复杂度分析:每个位置只会进栈一次出栈一次,再加上刚刚得到的对最终结果的分析。所以总的复杂度是严格O(N)的,提交发现可以AC了。
代码由于比较丑陋,就不放出来了。。。
但是结果发现这样做很慢,24ms几乎是!在!垫!底!
究其原因,我发现其实不需要真正的去遍历两次数组,在出栈的时候其实就已经可以确定它的window的结束的位置了,因为当一个高度出栈的时候就说明当前高度是这个高度碰到的右侧第一个比它低的高度,否则它就已经出栈了。
上述优化再加上手动实现的栈,加速程序,达到了16ms(已经达到C++的极限),又压缩了一下行数,这样看起来就简洁多了。(貌似这样写可读性很差。。。所以我又写了一份注释版本的)
代码如下:
height.push_back(-1); int *window = new int[height.size()], *stack = new int[height.size() + 1], top = 0, ans = 0; for (int i = 0; i < height.size(); i++){ while (top && height[stack[top - 1]] >= height[i]){ int temp = window[stack[top - 1]] += i - stack[top - 1] - 1; ans = max(ans, temp * height[stack[top-- - 1]]); } window[i] = (top ? i - stack[top - 1] - 1 : i) + 1; stack[top++] = i; } return ans;
核心算法已经压缩到了8行。这样才把题目做!爽!了!
注释版本:
height.push_back(-1); //插入一个比所有数都小的数表示结束,方便那些右侧能直接取到头的高度出栈用 int *window = new int[height.size()], *stack = new int[height.size() + 1], top = 0, ans = 0; for (int i = 0; i < height.size(); i++){ while (top && height[stack[top - 1]] >= height[i]){ //直到栈顶元素不大于当前元素为止 int temp = window[stack[top - 1]] += i - stack[top - 1] - 1; //要弹出的元素右侧伸展的位置已经可以确定,就是当前位置 ans = max(ans, temp * height[stack[top-- - 1]]); //要弹出元素已经计算完毕,更新答案,同时弹栈 } window[i] = (top ? i - stack[top - 1] - 1 : i) + 1; //当前元素的左边伸展位置可以确定,再算上自己 stack[top++] = i; //当前元素入栈 } cout << ans << endl; //输出答案
举一反三系列
注意到这个题目的similar problems当中唯一的一个题目
Maximal Rectangle
大意是要在一个01矩阵当中找出最大的一个矩形。
比如:
(请叫我灵魂画师)
仔细想想发现最终选出来的矩形反正都是以其中某一行为底边来形成的,那么不如就遍历每一条行看看如果它成为了底边能得到多大的结果最后再取全部中的最大值就可以了。这样就自然的和之前的题目联系起来了,只要得到了每一行向上形成的矩形数组(例如上例中第五行为[0,4,5,1,4,1]),就可以在O(N^2)内完成了。
为了得到这个数组方法有很多,大概都是O(N^2)的,我采用的是类似动态规划的方法:
- map[0][j] = matrix[i][j] == 1 ? 1 : 0
- map[i][j] = matrix[i][j] == 1 ? map[i - 1][j] + 1 : 0
所以整合一下,最终代码如下:
if (!matrix.size()) return 0; int row = matrix.size(), col = matrix[0].size(), ans = 0, map[1000][1000]; for (int j = 0; j < col; j++){ for (int i = 0; i <= row; i++){ map[i][j] = i == row ? -1 : matrix[i][j] == '1' ? (i ? map[i - 1][j] + 1 : 1) : 0; } } for (int j = 0, *window = new int[col + 1], *stack = new int[col + 1], top = 0; j < row; j++, top = 0){ for (int i = 0; i <= col; i++){ while (top && map[j][stack[top - 1]] >= map[j][i]){ int temp = window[stack[top - 1]] += i - stack[top - 1] - 1; ans = max(ans, temp * map[j][stack[top-- - 1]]); } window[i] = (top ? i - stack[top - 1] - 1 : i) + 1; stack[top++] = i; } } return ans;
核心部分和之前算法一模一样。在16ms AC了这个题目,可以说是比较快的了。