最大化直方图下的矩形面积

77

我有一个高度为整数且宽度恒定为1的直方图。我想最大化直方图下面的矩形区域。 例如:

 _
| |
| |_ 
|   |
|   |_
|     |

答案是6,即使用col1和col2相乘得到。

我知道O(n^2)的暴力方法,但我想要一个O(n log n)的算法。我尝试考虑最大递增子序列的动态规划算法(O(n log n)),但是卡住了。我应该使用分治算法吗?

PS:有足够声望的人请删除divide-and-conquer标签,如果没有这样的解决方案。

mho的评论之后:我的意思是适合完全适配的最大矩形区域。(感谢j_random_hacker澄清 :))。


2
如果列的高度分别为3、2、1,则如何使面积为3*2!因此,面积=3+2+1=6。 另外,最大化什么?你可以改变什么?问题还不清楚。 - Om Deshmane
11
我认为“最大化矩形面积”指的是“找到完全适合直方图下方的最大矩形的面积”。 - j_random_hacker
你可以在这里找到很多解决方案(http://blog.csdn.net/arbuckle/archive/2006/05/06/710988.aspx),包括O(n log n)和O(n)的算法。 - IVlad
1
官方网址为http://www.informatik.uni-ulm.de/acm/Locals/2003/html/judge.html。 - freestyler
我在这里找到了一个图形化解释O(n)解决方案Largest_Rectangle_In_Histogram - ktime
13个回答

109
上面的答案已经给出了最佳的O(n)代码解决方案,但是他们的解释很难理解。使用栈的O(n)算法一开始对我来说很神奇,但现在我完全明白了。好的,让我解释一下。
第一个观察点:
为了找到最大的矩形,如果对于每个柱子x,我们知道它两侧的第一个较小的柱子,假设为l和r,那么我们可以确定height[x] * (r - l - 1)是使用柱子x的高度得到的最佳结果。在下面的图中,1和2是5的第一个较小的柱子。
好的,让我们假设我们可以在O(1)时间内针对每个柱子完成这个过程,那么我们可以通过扫描每个柱子来在O(n)时间内解决这个问题!

enter image description here

然后,问题来了:对于每个柱子,我们是否真的能在O(1)时间内找到它左边和右边的第一个较小的柱子呢?这似乎是不可能的,对吧?但是,通过使用递增栈,这是可能的。
为什么使用递增栈可以跟踪其左侧和右侧的第一个较小值呢?
也许仅仅告诉你递增栈可以完成任务并不足以令人信服,因此我将为你详细介绍这一点。
首先,为了保持栈的递增,我们需要进行一项操作:
while x < stack.top():
    stack.pop()
stack.push(x)

然后你可以检查递增栈(如下所示),对于stack[x]stack[x-1]是其左侧第一个更小的元素,然后一个可以弹出stack[x]的新元素是其右侧第一个更小的元素。

enter image description here

你还不相信 stack[x-1] 是在 stack[x] 左侧第一个更小的元素吗?

我将通过反证法来证明。

首先,stack[x-1] < stack[x] 当然是成立的。但是假设 stack[x-1] 不是 stack[x] 左侧的第一个更小的元素。

那么第一个更小的元素 fs 在哪里呢?

If fs < stack[x-1]:
    stack[x-1] will be popped out by fs,
else fs >= stack[x-1]:
    fs shall be pushed into stack,
Either case will result fs lie between stack[x-1] and stack[x], which is contradicting to the fact that there is no item between stack[x-1] and stack[x].

因此,stack[x-1]必须是第一个较小的。
总结:
增长栈可以跟踪每个元素左右的第一个较小值。通过利用这个属性,可以使用一个栈在O(n)中解决直方图中的最大矩形问题。
恭喜你!这确实是一个困难的问题,我很高兴我的通俗解释没有阻止你完成。附上我的证明解决方案作为你的奖励 :)
def largestRectangleArea(A):
    ans = 0
    A = [-1] + A
    A.append(-1)
    n = len(A)
    stack = [0]  # store index

    for i in range(n):
        while A[i] < A[stack[-1]]:
            h = A[stack.pop()]
            area = h*(i-stack[-1]-1)
            ans = max(ans, area)
        stack.append(i)
    return ans

我还是不太明白,特别是这句话:“如果对于每个条形图x,我们知道其两侧第一个较小的条形图,假设为l和r,则我们可以确定使用条形图x的高度时height[x] *(r-l-1)是我们能得到的最佳结果。” 如果x是5,但是l和r分别是3,那么最大面积应该是3x3=9,而不是5x1=5,对吗? - timpham
这并不与引用的声明相矛盾。@Arrakëën所说的是,使用(x, height[x])仍然是您可以获得的最大区域为5。 - yangmillstheory
20
无论你是谁,我都喜欢你兄弟!这回答太棒了!我等了3个月才获得评论特权,只为了在这里发表评论! - Pavithran Ravichandiran
1
"一个新的元素可以弹出stack[x],它是右侧第一个较小的元素。这让我开心了一整天。" - Saurav Sahu
2
我不知道为什么面试会问这个问题。这很难而且毫无意义,因为如果没有看过这个问题,几乎没有人能在那么短的时间内想出解决方案。 - Trần Kim Dự
显示剩余2条评论

50
除了暴力方法,解决这个问题还有三种方法。我将把它们都写下来。Java代码已经通过名为leetcode的在线评测网站的测试:http://www.leetcode.com/onlinejudge#question_84,所以我相信这些代码是正确的。
解决方案1:动态规划+ n*n矩阵作为缓存 时间复杂度:O(n^2),空间复杂度:O(n^2)
基本思路:使用n*n矩阵dp[i][j]来缓存bar[i]和bar[j]之间的最小高度。从宽度为1的矩形开始填充矩阵。
public int solution1(int[] height) {

    int n = height.length;
    if(n == 0) return 0;
    int[][] dp = new int[n][n];        
    int max = Integer.MIN_VALUE;

    for(int width = 1; width <= n; width++){

        for(int l = 0; l+width-1 < n; l++){

            int r = l + width - 1;

            if(width == 1){
                dp[l][l] = height[l];
                max = Math.max(max, dp[l][l]);
            } else {                    
                dp[l][r] = Math.min(dp[l][r-1], height[r]);
                max = Math.max(max, dp[l][r] * width);
            }                
        }
    }

    return max;
}

解决方案2:动态规划+2个缓存数组
时间复杂度:O(n^2),空间复杂度:O(n)。
基本思想:这个解决方案与解决方案1类似,但是节省了一些空间。我们的想法是,在解决方案1中,我们从第1行到第n行构建矩阵。但是在每次迭代中,只有前一行对当前行的构建有贡献。因此,我们轮流使用两个数组作为前一行和当前行。
public int Solution2(int[] height) {

    int n = height.length;
    if(n == 0) return 0;

    int max = Integer.MIN_VALUE;

    // dp[0] and dp[1] take turns to be the "previous" line.
    int[][] dp = new int[2][n];      

    for(int width = 1; width <= n; width++){

        for(int l = 0; l+width-1 < n; l++){

            if(width == 1){
                dp[width%2][l] = height[l];
            } else {
                dp[width%2][l] = Math.min(dp[1-width%2][l], height[l+width-1]);                     
            }
            max = Math.max(max, dp[width%2][l] * width);   
        }
    }        
    return max;
}

解决方案3:使用栈
时间复杂度:O(n),空间复杂度:O(n)。
这个解决方案有些棘手,我是从没有图表的解释有图表的解释中学会的。建议在阅读我的解释之前先看一下这两个链接。由于没有图表,所以我的解释可能比较难懂。
以下是我的解释:
  1. For each bar, we must be able to find the biggest rectangle containing this bar. So the biggest one of these n rectangles is what we want.

  2. To get the biggest rectangle for a certain bar (let's say bar[i], the (i+1)th bar), we just need to find out the biggest interval that contains this bar. What we know is that all the bars in this interval must be at least the same height with bar[i]. So if we figure out how many consecutive same-height-or-higher bars are there on the immediate left of bar[i], and how many consecutive same-height-or-higher bars are there on the immediate right of the bar[i], we will know the length of the interval, which is the width of the biggest rectangle for bar[i].

  3. To count the number of consecutive same-height-or-higher bars on the immediate left of bar[i], we only need to find the closest bar on the left that is shorter than the bar[i], because all the bars between this bar and bar[i] will be consecutive same-height-or-higher bars.

  4. We use a stack to dynamicly keep track of all the left bars that are shorter than a certain bar. In other words, if we iterate from the first bar to bar[i], when we just arrive at the bar[i] and haven't updated the stack, the stack should store all the bars that are no higher than bar[i-1], including bar[i-1] itself. We compare bar[i]'s height with every bar in the stack until we find one that is shorter than bar[i], which is the cloest shorter bar. If the bar[i] is higher than all the bars in the stack, it means all bars on the left of bar[i] are higher than bar[i].

  5. We can do the same thing on the right side of the i-th bar. Then we know for bar[i] how many bars are there in the interval.

    public int solution3(int[] height) {
    
        int n = height.length;
        if(n == 0) return 0;
    
        Stack<Integer> left = new Stack<Integer>();
        Stack<Integer> right = new Stack<Integer>();
    
        int[] width = new int[n];// widths of intervals.
        Arrays.fill(width, 1);// all intervals should at least be 1 unit wide.
    
        for(int i = 0; i < n; i++){
            // count # of consecutive higher bars on the left of the (i+1)th bar
            while(!left.isEmpty() && height[i] <= height[left.peek()]){
                // while there are bars stored in the stack, we check the bar on the top of the stack.
                left.pop();                
            }
    
            if(left.isEmpty()){
                // all elements on the left are larger than height[i].
                width[i] += i;
            } else {
                // bar[left.peek()] is the closest shorter bar.
                width[i] += i - left.peek() - 1;
            }
            left.push(i);
        }
    
        for (int i = n-1; i >=0; i--) {
    
            while(!right.isEmpty() && height[i] <= height[right.peek()]){                
                right.pop();                
            }
    
            if(right.isEmpty()){
                // all elements to the right are larger than height[i]
                width[i] += n - 1 - i;
            } else {
                width[i] += right.peek() - i - 1;
            }
            right.push(i);
        }
    
        int max = Integer.MIN_VALUE;
        for(int i = 0; i < n; i++){
            // find the maximum value of all rectangle areas.
            max = Math.max(max, width[i] * height[i]);
        }
    
        return max;
    }
    

我建议您在说明栈方法时,包含这张图片(来自一篇博客文章(中文))。它非常易于理解。 - lcn
在查找所有矩形的最大值时,应该是max = Math.max(max,(width[i]+1)*height[i]),因为我们还必须包括我们正在包含的条形的宽度。 - shivam mitra
@shivammitra 条形图的宽度已经包含在内了,因为语句 Arrays.fill(width,1) 初始化了宽度数组的所有元素为1。 - Nikhil Goyal

15

Python实现@IVlad的答案 O(n)解决方案:

from collections import namedtuple

Info = namedtuple('Info', 'start height')

def max_rectangle_area(histogram):
    """Find the area of the largest rectangle that fits entirely under
    the histogram.

    """
    stack = []
    top = lambda: stack[-1]
    max_area = 0
    pos = 0 # current position in the histogram
    for pos, height in enumerate(histogram):
        start = pos # position where rectangle starts
        while True:
            if not stack or height > top().height:
                stack.append(Info(start, height)) # push
            elif stack and height < top().height:
                max_area = max(max_area, top().height*(pos-top().start))
                start, _ = stack.pop()
                continue
            break # height == top().height goes here

    pos += 1
    for start, height in stack:
        max_area = max(max_area, height*(pos-start))

    return max_area

例子:

>>> f = max_rectangle_area
>>> f([5,3,1])
6
>>> f([1,3,5])
6
>>> f([3,1,5])
5
>>> f([4,8,3,2,0])
9
>>> f([4,8,3,1,1,0])
9

使用不完整子问题的堆栈进行线性搜索

复制粘贴算法描述(以防页面崩溃):

我们按照从左到右的顺序处理元素,并维护有关已开始但尚未完成的子直方图的信息堆栈。每当出现新元素时,它都会遵循以下规则。如果堆栈为空,我们通过将元素推入堆栈来打开新的子问题。否则,我们将其与堆栈顶部的元素进行比较。如果新元素更大,我们再次将其推入堆栈。如果新元素相等,则跳过它。在所有这些情况下,我们继续处理下一个新元素。如果新元素更小,则通过更新堆栈顶部元素的最大区域来完成最高的子问题。然后,我们丢弃顶部的元素,并重复该过程以保留当前新元素。这样,所有子问题都会完成,直到堆栈变为空,或其顶部元素小于或等于新元素,从而导致上述操作。如果已处理所有元素并且堆栈尚未为空,则通过更新与顶部元素相关的最大区域来完成剩余的子问题。
对于相对于元素的更新,我们找到包含该元素的最大矩形。请注意,除了跳过的元素外,所有元素的最大区域都进行了最大面积的更新。但是,如果跳过元素,则它具有与那时将稍后更新的堆栈顶部元素相同的最大矩形。最大矩形的高度当然是元素的值。在更新时,我们知道最大矩形向右延伸到元素的距离,因为此时第一次出现较小的高度的新元素。如果我们也将其存储在堆栈上,则可以获得最大矩形向元素左侧延伸的信息。
因此,我们修改了上述过程。如果立即将新元素推入堆栈,无论是因为空堆栈还是大于堆栈顶部元素,包含它的最大矩形都不会向左延伸超过当前元素。如果在弹出堆栈中的多个元素后将其推入,因为它小于这些元素,则包含它的最大矩形向左延伸至最近弹出元素的最大矩形。
每个元素最多被推送和弹出一次,并且在过程的每个步骤中至少推送或弹出一个元素。由于决策和更新的工作量是恒定的,因此通过摊销分析,算法的复杂度为O(n)。

我发现这个和其他的解释很难理解,因为我不知道你们所说的“子直方图”是从哪里开始和结束的。还有其他问题,但那是最大的一个。 - Aaron Watters
@Aaron Watters:子直方图的起始位置在堆栈上显式存储(start属性)。当堆栈弹出时,结束位置是直方图中的当前位置(pos变量),请参见height < top().height分支。 - jfs

15

其他答案已经很好地介绍了使用两个堆栈的O(n)时间复杂度、O(n)空间复杂度的解决方案。对于这个问题,还有另一个角度提供了独立的O(n)时间复杂度、O(n)空间复杂度的解决方案,并且可能会更深入地解释为什么基于堆栈的解决方案有效。

关键思想是使用一种数据结构称为笛卡尔树。笛卡尔树是一种二叉树结构(虽然不是二叉搜索树),围绕输入数组构建而成。具体来说,笛卡尔树的根建立在数组的最小元素上,左右子树从最小值的左侧和右侧的子数组递归构建。

例如,这里有一个示例数组及其笛卡尔树:

                 +----------------------- 23 ------+
                 |                                 |
  +------------- 26 --+                        +-- 79
  |                   |                        |
  31 --+              53 --+                   84
       |                   |
       41 --+              58 -------+
            |                        |
            59                  +-- 93
                                |
                                97

+----+----+----+----+----+----+----+----+----+----+----+
| 31 | 41 | 59 | 26 | 53 | 58 | 97 | 93 | 23 | 84 | 79 |
+----+----+----+----+----+----+----+----+----+----+----+

笛卡尔树在这个问题中很有用的原因是,所提出的问题具有非常好的递归结构。首先看一下直方图中最低的矩形。对于最大矩形可能出现的位置,有三种情况:

  • 它可能经过直方图中的最小值。在这种情况下,为了使其尽可能大,我们希望将其宽度设为整个数组的宽度。

  • 它可能完全位于最小值左侧。在这种情况下,我们需要递归地处理最小值左侧的子数组以得到答案。

  • 它可能完全位于最小值右侧。在这种情况下,我们需要递归地处理最小值右侧的子数组以得到答案。

注意,这种递归结构-找到最小值,在最小值左右的子数组中进行某些操作-与笛卡尔树的递归结构完全匹配。事实上,如果我们在开始时可以为整个数组创建一个笛卡尔树,则可以通过从根向下递归遍历笛卡尔树来解决此问题。在每个节点处,我们递归计算左右子数组中的最佳矩形,以及通过放置在最小值下方所得到的矩形,然后返回找到的最佳选项。

伪代码如下:

function largestRectangleUnder(int low, int high, Node root) {
  /* Base case: If the range is empty, the biggest rectangle we
   * can fit is the empty rectangle.
   */
  if (low == high) return 0;

  /* Assume the Cartesian tree nodes are annotated with their
   * positions in the original array.
   */
  return max {
    (high - low) * root.value, // Widest rectangle under the minimum
    largestRectangleUnder(low,            root.index, root.left),
    largestRectnagleUnder(root.index + 1, high,       root.right)
  }
}

一旦我们获得Cartesian树,此算法的时间复杂度为O(n),因为我们恰好访问每个节点并且每个节点只需进行O(1)次操作。

事实证明,有一种简单的线性时间算法可用于构建Cartesian树。您可能会想到“自然”的构建方法是扫描数组,找到最小值,然后递归地从左右子数组构建Cartesian树。问题在于查找最小值的过程非常昂贵,这可能需要Θ(n²)的时间。

构建Cartesian树的“快速”方法是从左到右扫描数组,逐个添加一个元素。该算法基于以下关于Cartesian树的观察结果:

  • 首先,Cartesian树遵循堆属性:每个元素都小于或等于其子元素。原因在于,Cartesian树根是整个数组中最小的值,而其子元素是它们自己子数组中的最小元素,以此类推。

  • 其次,如果对Cartesian树进行中序遍历,则可以按它们出现的顺序返回数组的元素。要了解为什么,请注意,如果对Cartesian树进行中序遍历,则首先访问最小值左侧的所有内容,然后是最小值,最后是最小值右侧的所有内容。这些访问以相同的方式递归进行,因此每个元素最终都按顺序被访问。

这两条规则为我们提供了有关从数组的前k个元素开始构建Cartesian树并且想要形成前k + 1个元素的Cartesian树时会发生什么的许多信息。该新元素将必须出现在Cartesian树的右脊柱上 - 即从根节点开始向右移动的部分 - 因为否则中序遍历中会有其它元素紧随其后。而且,在该右脊柱内,它必须以使其大于其上面的所有元素的方式放置,因为我们需要遵守堆属性。

实际上添加新节点到Cartesian树的方法是从树中最右侧的节点开始向上遍历,直到到达树的根节点或者找到具有更小值的节点。然后,将新值的左子节点设置为在其上方最后遍历的节点。

下面是在一个小数组上应用该算法的示例:

+---+---+---+---+
| 2 | 4 | 3 | 1 |
+---+---+---+---+

2成为根节点。

  2 --+
      |  
      4

4比2大,我们不能向上移动。追加到右侧。

+---+---+---+---+
| 2 | 4 | 3 | 1 |
+---+---+---+---+

  2 ------+
          |
      --- 3 
      |
      4

3小于4,爬过它。 无法继续超过2,因为它比3小。 爬过以4为根的子树将移动到新值3的左侧,现在3成为最右边的节点。

+---+---+---+---+
| 2 | 4 | 3 | 1 |
+---+---+---+---+

  +---------- 1
  |
  2 ------+
          |
      --- 3
      |
      4

1爬过根节点2后,以2作为根的整个树被移动到1的左侧,1现在成为新的根,同时也是最右侧的值。

+---+---+---+---+
| 2 | 4 | 3 | 1 |
+---+---+---+---+
虽然这似乎不是线性时间运行的——你可能会一遍又一遍地爬到树的根部吗?——但是你可以使用一个巧妙的论证来证明这是线性时间运行的。如果在插入过程中从右侧脊柱上跨越节点,那么该节点最终会被移出右侧脊柱,因此不能在将来的插入中重新扫描。因此,每个节点只会被扫描一次,因此总工作量是线性的。

现在关键的一步——实际实现这种方法的标准方式是通过维护一个对应于右侧脊柱上节点的值的堆栈来完成的。沿着节点“向上”和跨越节点相当于弹出堆栈中的一个元素。因此,构建笛卡尔树的代码看起来像这样:

Stack s;
for (each array element x) {
   pop s until it's empty or s.top > x
   push x onto the stack.
   do some sort of pointer rewiring based on what you just did.
}

这里的堆栈操作可能会很熟悉,因为这些操作与其他答案中显示的完全相同。实际上,你可以将那些方法看作是在隐式构建笛卡尔树以及在此过程中运行上面显示的递归算法。

我认为了解笛卡尔树的好处在于它提供了一个非常好的概念框架,可以看到为什么该算法能正确地工作。如果你知道你正在运行笛卡尔树的递归遍历,那么更容易看出你保证找到最大的矩形。此外,知道笛卡尔树的存在还可以为解决其他问题提供有用的工具。笛卡尔树出现在针对区间最小查询问题设计快速数据结构时,并用于将后缀数组转换为后缀树


这是一些Java代码,它实现了这个思想,由@Azeem提供!

import java.util.Stack;

public class CartesianTreeMakerUtil {

    private static class Node {
        int val;
        Node left;
        Node right;
    }

    public static Node cartesianTreeFor(int[] nums) {
        Node root = null;
        Stack<Node> s = new Stack<>();
        for(int curr : nums) {
            Node lastJumpedOver = null;
            while(!s.empty() && s.peek().val > curr) {
                lastJumpedOver = s.pop();
            }
            Node currNode = this.new Node();
            currNode.val = curr;
            if(s.isEmpty()) {
                root = currNode;
            }
            else {
                s.peek().right = currNode;
            }
            currNode.left = lastJumpedOver;
            s.push(currNode);
        }
        return root;
    }
    
    public static void printInOrder(Node root) {
        if(root == null) return;
        if(root.left != null ) {
            printInOrder(root.left);
        }
        System.out.println(root.val);
        if(root.right != null) {
            printInOrder(root.right);
        }
    }
    
    public static void main(String[] args) {
        int[] nums = new int[args.length];
        for (int i = 0; i < args.length; i++) {
            nums[i] = Integer.parseInt(args[i]);
        }
        Node root = cartesianTreeFor(nums);
        tester.printInOrder(root);
    }
}

5

O(N) 中最简单的解决方案

long long getMaxArea(long long hist[], long long n)
{

    stack<long long> s;

    long long max_area = 0; 
    long long tp;  
    long long area_with_top; 

    long long i = 0;
    while (i < n)
    {
        if (s.empty() || hist[s.top()] <= hist[i])
            s.push(i++);
       else
        {
            tp = s.top();  // store the top index
            s.pop();  // pop the top
            area_with_top = hist[tp] * (s.empty() ? i : i - s.top() - 1);
            if (max_area < area_with_top)
            {
                max_area = area_with_top;
            }
        }
    }

   while (!s.empty())
    {
        tp = s.top();
        s.pop();
        area_with_top = hist[tp] * (s.empty() ? i : i - s.top() - 1);

        if (max_area < area_with_top)
            max_area = area_with_top;
    }

    return max_area;
}

2
“堆栈解决方案”是我至今看到的最聪明的解决方案之一。但理解它为什么有效可能有点困难。我在这里尝试了解释这个问题的详细情况。
文章中的摘要如下:
- 我们大脑思考问题的一般方式是:
- 创建每种情况并尝试找到解决问题所需的约束条件的值。 - 然后我们愉快地将其转换为代码:对于每种情况(pair(i,j)),找到约束条件(min)的值。
- 这个巧妙的解决方案试图反转问题:“对于该区域的每个约束/最小值,左右极限的最佳可能性是什么?”
如果我们遍历数组中每个可能的min,那么每个值的左右极限是什么?
经过一点思考,第一个小于当前min的最左边的值,以及同样小于当前min的第一个最右边的值。
现在我们需要看看是否有聪明的方法来找到第一个比当前值小的左右值。
思考:如果我们部分遍历了数组,比如说到了min_i,那么如何构建min_i+1的解决方案?
我们需要它左边第一个小于min_i的值。
反转语句:我们需要忽略所有左边大于min_i的值。当我们找到第一个小于min_i的值(i)时停止。因此,曲线中的低谷一旦被越过就变得无用了。在直方图中,(2 4 3) => 如果3是min_i,则4比它大就没有意义。
推论:在区间(i,j)中,j是我们考虑的最小值,所有j和其左侧值i之间的值都是无用的。即使对于进一步的计算也是如此。
右侧的任何直方图,如果最小值大于j,将会绑定在j上。左侧的感兴趣的值形成一个单调递增的序列,其中j是最大的值。(这里的感兴趣的值是可能对后来的数组有用的可能值)
由于我们从左到右遍历,所以对于每个最小值/当前值,我们不知道数组的右侧是否会有一个比它小的元素。
因此,我们必须将其保留在内存中,直到我们知道这个值是无用的。(因为找到了更小的值)
所有这些都导致我们使用自己的stack结构。
我们一直保持堆栈,直到我们知道它是无用的。
我们一旦知道这个东西是垃圾,就从堆栈中删除它。
因此,对于每个最小值要找到其左边的较小值,我们执行以下操作:
1.弹出大于它的元素(无用的值)
2.第一个小于该值的元素是左极限。i到我们的min。
我们可以从数组的右侧执行相同的操作,我们会得到j到我们的min。
这有点难以解释,但如果您理解了我的话,我建议阅读完整的文章这里,因为它有更多深入的见解和细节。

2

还有一种使用分治算法的解决方案。其算法如下:

1)以最小高度为断点将数组分成两部分

2)最大面积是以下三者中的最大值: a)最小高度 * 数组大小 b)左半数组中的最大矩形 c)右半数组中的最大矩形

时间复杂度为O(nlogn)


这是一个不错的解决方案。尽管最坏情况下时间复杂度为O(n2)。 - techolic

1

我不理解其他条目,但我认为我知道如何以O(n)的方式执行以下操作。

A)对于每个索引,在该索引处结束的直方图内找到与索引列接触矩形顶部的最大矩形,并记住矩形开始位置。可以使用基于堆栈的算法在O(n)中完成此操作。

B)同样,对于每个索引,在该索引处开始的直方图内找到与索引列接触矩形顶部的最大矩形,并记住矩形结束位置。也可以使用与(A)相同的方法,在直方图向后扫描时以O(n)的速度完成。

C)对于每个索引,结合(A)和(B)的结果,确定该索引处的列接触矩形顶部的最大矩形。像(A)一样的O(n)。

D)由于最大矩形必须被直方图的某些列所接触,因此最大矩形是在步骤(C)中找到的最大矩形。

难点在于实现(A)和(B),我认为这就是JF Sebastian可能已经解决而不是所述一般问题。


1
顺便提一下,这是我从以下网站获取的基本思路:http://tech-queries.blogspot.com/2011/03/maximum-area-rectangle-in-histogram.html - Aaron Watters

1

我编写了这个代码,感觉稍微好一些:

import java.util.Stack;

     class StackItem{
       public int sup;
       public int height;
       public int sub;

       public StackItem(int a, int b, int c){
           sup = a;
           height = b;
           sub =c;
       }
       public int getArea(){
           return (sup - sub)* height;
       }


       @Override
       public String toString(){
       return "     from:"+sup+
              "     to:"+sub+
              "     height:"+height+              
              "     Area ="+getArea();
       }
    }   


public class MaxRectangleInHistogram {    
    Stack<StackItem> S;
    StackItem curr;
    StackItem maxRectangle;

    public StackItem getMaxRectangleInHistogram(int A[], int n){
        int i = 0;
        S = new Stack();        
        S.push(new StackItem(0,0,-1));
        maxRectangle = new StackItem(0,0,-1);

        while(i<n){

                curr = new StackItem(i,A[i],i);

                    if(curr.height > S.peek().height){
                            S.push(curr); 
                    }else if(curr.height == S.peek().height){                            
                            S.peek().sup = i+1;                         
                    }else if(curr.height < S.peek().height){                            

                            while((S.size()>1) && (curr.height<=S.peek().height)){
                                curr.sub = S.peek().sub;
                                S.peek().sup = i;
                                decideMaxRectangle(S.peek());
                                S.pop(); 
                            }                               
                        S.push(curr);                    
                    }
            i++;
        }

        while(S.size()>1){ 
            S.peek().sup = i;
            decideMaxRectangle(S.peek());
            S.pop();            
        }  

        return maxRectangle;
    }

    private void decideMaxRectangle(StackItem s){ 

        if(s.getArea() > maxRectangle.getArea() )
            maxRectangle = s;      
    }

}

请注意:

Time Complexity: T(n) < O(2n) ~ O(n)
Space Complexity S(n) < O(n)

1
我希望能感谢@templatetypedef提供的非常详细和直观的答案。以下Java代码基于他的建议使用笛卡尔树,以O(N)时间和O(N)空间解决了该问题。建议在阅读下面的代码之前先阅读@templatetypedef上面的答案。代码以leetcode上的问题解决方案格式给出:https://leetcode.com/problems/largest-rectangle-in-histogram/description/ 并通过了所有96个测试用例。
class Solution {

private class Node {
    int val;
    Node left;
    Node right;
    int index;
}

public  Node getCartesianTreeFromArray(int [] nums) {
    Node root = null;
    Stack<Node> s = new Stack<>();
    for(int i = 0; i < nums.length; i++) {
        int curr = nums[i];
        Node lastJumpedOver = null;
        while(!s.empty() && s.peek().val >= curr) {
            lastJumpedOver = s.pop();
        }
        Node currNode = this.new Node();
        currNode.val = curr;
        currNode.index = i;
        if(s.isEmpty()) {
            root = currNode;
        }
        else {
            s.peek().right = currNode;
        }
        currNode.left = lastJumpedOver;
        s.push(currNode);
    }
    return root;
}

public int largestRectangleUnder(int low, int high, Node root, int [] nums) {
    /* Base case: If the range is empty, the biggest rectangle we
     * can fit is the empty rectangle.
     */
    if(root == null) return 0;

    if (low == high) {
        if(0 <= low && low <= nums.length - 1) {
            return nums[low];
        }
        return 0;
    }

    /* Assume the Cartesian tree nodes are annotated with their
     * positions in the original array.
     */
    int leftArea = -1 , rightArea= -1;
    if(root.left != null) {
        leftArea = largestRectangleUnder(low, root.index - 1 , root.left, nums);
    }
    if(root.right != null) {
        rightArea = largestRectangleUnder(root.index + 1, high,root.right, nums);
    }
    return Math.max((high - low  + 1) * root.val, 
           Math.max(leftArea, rightArea));
}

public int largestRectangleArea(int[] heights) {
    if(heights == null || heights.length == 0 ) {
        return 0;
    }
    if(heights.length == 1) {
        return heights[0];
    }
    Node root = getCartesianTreeFromArray(heights);
    return largestRectangleUnder(0, heights.length - 1, root, heights);
}

}


经过一些快速的编辑,我最终将你的代码融入了我的答案中。感谢你编写这个! - templatetypedef
你对笛卡尔树的解释激励了我编写一个可行的解决方案 - 很高兴能够提供帮助。如果您愿意,您可以在您的答案中加入我的Java代码,用于largestRectangleUnder()函数伪代码。它处理了一些伪代码缺失的边缘情况。 - Azeem

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接