经评论指出,原文中的枚举和动态规划都有一些问题,博主已经于2019年12月31日晚上对原文进行了更正。
问题
给定直方图,求直方图中最大的矩形面积。
例如下图,可以用数组表示为[2,1,5,6,2,3]。
对应的最大矩形面积为10.
枚举
对于直方图中每根柱子,假定其是当前矩形中最短的一根,分别向左右两个方向搜索,直到遇到比它短的为止。
int h[] = {2,1,5,6,2,3};
int length = 6, max = 0, s = 0;
for(int i = 0; i < length; i ++) {
int k, j;
for(k = i - 1; k >= 0; k --) {
if (h[i] > h[k]) break; //找到左边界
}
for(j = i + 1; j < length; j ++) {
if (h[i] > h[j]) break; //找到右边界
}
s = h[i] * (j - k - 1);
if (max < s) max = s;
}
枚举的时间为复杂度为O(n2),空间复杂度为O(1)。
动态规划
用动态规划来考虑这个问题。动态规划必须找到一个可以在相邻柱状条之间转换的状态描述。相邻柱状条除了比大小,再没有别的关系可用了。
所以,定义left[i]表示往左边扩展,高度不低于h[i]的连续柱状条中最远的下标。
right[i]表示往右边扩展,高度不低于h[i]的连续柱状条中最远的下标。
这样,以h[i]为高度的最大矩形面积为h[i]*(right[i] - left[i]+1)。
枚举所有位置,就可以找到最大矩形了,而且一定可以找到。因为直方图中的最大矩形,一定有一条最低的柱状条。
参考代码如下:
int h[] = {2,1,5,6,2,3};
int length = 6, max = 0, s = 0;
int left[6] = {0}, right[6] = {0};
left[0] = 0;
for(int i = 1; i < length;i ++) {
int k = i;
while(k > 0 && h[i] <= h[k-1]) {
k = left[k-1];
}
left[i] = k;
}
right[length-1] = length - 1;
for(int i = length - 2; i >= 0; i --) {
int k = i;
while(k < length-1 && h[i] <= h[k+1]) {
k = right[k+1];
}
right[i] = k;
}
for(int i = 0; i < length; i ++) {
s = h[i] * (right[i] - left[i] + 1);
if (max < s) max = s;
}
此处,与枚举比起来,动态规划采用的计算思路是一样的,但仅仅加速了left和right数组的计算,并没有从实质上改变其复杂度。所以,动态规划时间复杂度为O(n2),空间复杂度为O(n)。
网络上另外一种动态规划的思路是,定义dp[i][j]表示从下标i到下标j的最大矩形面积,对图形组合进行分析,有如下:
dp[i][j] = max(dp[i-1][j], dp[i][j+1], 最短柱条乘以个数(即j-i+1))
该公式中最后一项的计算仍然需要遍历,并没有从实质上改进复杂度。
单调栈
网上广为流传的是一种利用单调栈思路的代码。基本原理描述如下:
扫描数组,并维护一个单调栈,栈里面的元素是递增的。如果当前元素比栈顶元素大,入栈。否则,出栈,直到当前元素大于栈顶元素。每次出栈时,计算以当前栈顶元素为高度的最大矩形。
代码如下:
vector<int> h = {2,1,5,6,2,3};
int length = 6, max = 0;
stack<int> s;
h.push_back(0); //哨兵,用于清空单调栈
for(int i = 0; i < length; i ++) {
while(!s.empty() && h[i] <= h[s.top()]) {
int cur = s.top();
s.pop();
int r = h[cur] * (s.empty() ? i : i - s.top() - 1);
max = (max < r : r : max);
}
s.push(i);
}
该方法的时间复杂度是O(n),空间复杂度也为O(n)。
单调栈的作用,在于保存了每个柱状条往前比自己高的元素信息。每个出栈元素,必定是大于当前元素的。因而,其构成的最大矩形,也只能到当前元素为止。