最大化直方图下的矩形区域


72

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

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

使用col1和col2的答案将是6、3 * 2。

O(n ^ 2)蛮力对我很清楚,我想要一个O(n log n)算法。我正在尝试按照最大递增子序列O(n log n)算法的方式进行动态编程,但我没有前进。我应该使用分治法吗?

PS:如果没有这样的解决方案,请信誉良好的人删除分而治之的标签。

经过mho的评论:我的意思是完全适合的最大矩形区域。(感谢j_random_hacker进行澄清:))。


2
面积怎么可能是3 * 2!如果列的高度分别为3、2、1,则面积= 3 + 2 + 1 =6。而且,针对什么最大化?你可以改变什么?问题尚不清楚。
Om Deshmane 2010年

11
@mho:我相信“最大化矩形区域”是指“找到完全适合直方图的最大矩形区域”。
j_random_hacker 2010年

您在和那里都有很多解决方案。O(n log n)O(n)
IVlad

Answers:


102

上面的答案给出了代码中最好的O(n)解决方案,但是,它们的解释很难理解。起初,使用堆栈的O(n)算法对我来说似乎是神奇的,但是现在对我来说,它是完全有意义的。好,让我解释一下。

初步观察:

为了找到最大的矩形,如果对于每个小节x,我们都知道其每一侧的第一个较小的小节,例如lr,我们确定这height[x] * (r - l - 1)是通过使用小节的高度可以获得的最佳拍摄x。在下图中,1和2是5中的第一个较小的值。

OK,假设我们可以在O(1)的时间内为每个小节执行此操作,那么我们可以在O(n)中解决此问题!通过扫描每个条。

在此处输入图片说明

然后,问题来了:对于每个小节,我们是否真的可以在O(1)时间中在其左右两侧找到第一个较小的小节?看来不可能吧?...可以通过使用增加的堆栈来实现。

为什么使用增加的堆栈可以跟踪左侧和右侧的第一个较小的堆栈?

也许通过告诉您越来越多的堆栈可以完成此工作并不令人信服,所以我将带您逐步完成。

首先,要保持堆栈增加,我们需要执行以下一项操作:

while x < stack.top():
    stack.pop()
stack.push(x)

然后,您可以检查递增堆栈中的,如下所示,对于stack[x]stack[x-1]它是左边的第一个较小的元素,然后可以弹出的新元素stack[x]是右边的第一个较小的元素。

在此处输入图片说明

还是不敢相信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,我们都可以确定height [x] *(r-l-1 )是使用“ x”杆的高度可以获得的最佳镜头。如果x为5,但l和r为3,怎么办?那么最大面积为3x3 = 9,而不是5 * 1 = 5对吗?
timpham

但这与引用的声明并不矛盾。@Arrakëën所说的5仍然是您可以使用达到的最大面积(x, height[x])
yangmillstheory

20
无论你是谁 !我只是爱你,兄弟!真是个答案!等待了3个月,以便我可以在这里发表评论,从而拥有发表评论的特权!
金克斯信天翁

1
“一个可以弹出stack [x]的新元素是它右边的第一个较小的元素。” 为我做了一天。
Saurav Sahu

1
我不知道为什么在面试中问这个问题。这很困难而且毫无意义,因为如果以前没有人看到过这个问题,那么几乎没人能在短时间内想象出解决方案。
陈金斗

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:使用stack

时间:O(n),空间:O(n)

该解决方案是棘手的,我学会了如何从做这个解释没有图形解释与图表。我建议您先阅读两个链接,然后再阅读下面的说明。没有图形很难解释,所以我的解释可能很难理解。

以下是我的解释:

  1. 对于每个条,我们必须能够找到包含此条的最大矩形。因此,这n个矩形中最大的一个就是我们想要的。

  2. 要获得某个条形的最大矩形(例如,bar [i],第(i + 1)条),我们只需要找出包含该条的最大间隔即可。我们知道的是,此间隔中的所有条形必须与bar [i]至少具有相同的高度。因此,如果我们找出bar [i]的最左端有多少个连续的相同高度或更高的条形,以及bar [i]的右端有多少个连续的相同高度或更高的条形[ i],我们将知道间隔的长度,即bar [i]的最大矩形的宽度。

  3. 要计算bar [i]的最左端连续的相同高度或较高的bar的数量,我们只需要找到最短的bar(比bar [i]短),这是因为此bar和bar [i]将是连续的相同高度或更高的bar。

  4. 我们使用堆栈来动态跟踪所有短于某一条的左条。换句话说,如果我们从第一个bar迭代到bar [i],则当我们刚到达bar [i]而没有更新堆栈时,堆栈应存储不高于bar [i]的所有bar -1],包括bar [i-1]本身。我们将bar [i]的高度与堆栈中的每个bar进行比较,直到找到比bar [i]短的最短的bar。如果bar [i]高于堆栈中的所有条,则表示bar [i]左侧的所有条都高于bar [i]。

  5. 我们可以在第i个栏的右侧执行相同的操作。然后我们知道bar [i]区间中有多少条。

    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

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

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

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

复制粘贴算法的描述(如果页面出现故障):

我们以从左到右的顺序处理元素,并维护有关已开始但尚未完成的亚直方图的信息。每当有新元素到达时,都要遵守以下规则。如果堆栈为空,则通过将元素推入堆栈来打开一个新的子问题。否则,我们将其与堆栈顶部的元素进行比较。如果新的更大,我们再次推动它。如果新的相等,则跳过它。在所有这些情况下,我们继续下一个新元素。如果新的问题较少,我们将通过更新堆栈顶部元素的最大面积来完成最顶层的子问题。然后,我们丢弃顶部的元素,并重复此过程以保留当前的新元素。这样,所有子问题都将完成,直到堆栈变空为止,或其顶部元素小于或等于新元素,则导致上述操作。如果所有元素都已处理,并且堆栈还没有为空,则通过将最大面积wrt更新为顶部的元素来完成剩余的子问题。

对于元素的更新,我们找到了包含该元素的最大矩形。请注意,除跳过的元素外,所有元素都将更新最大面积。但是,如果跳过某个元素,则该元素与当时位于堆栈顶部的元素具有相同的最大矩形,稍后将对其进行更新。当然,最大矩形的高度是元素的值。在更新时,我们知道最大的矩形在元素的右侧延伸了多远,因为这样,第一次出现了一个高度较小的新元素。如果我们也将其存储在堆栈中,则该信息(最大的矩形向元素左侧延伸的距离)将可用。

因此,我们修改了上述程序。如果立即推送一个新元素(由于堆栈为空或大于堆栈的顶部元素),则包含该元素的最大矩形将向左延伸,直到当前元素为止。如果在从堆栈中弹出几个元素之后将其推入,因为它少于这些元素,则包含该元素的最大矩形将向左延伸到最近弹出的元素的矩形。

每个元素最多被推送和弹出一次,并且在该过程的每个步骤中,至少一个元素被推送或弹出。由于决策和更新的工作量是恒定的,因此通过摊销分析,算法的复杂度为O(n)。


我发现这种解释和其他解释很难理解,因为我不知道你们所有人所说的“亚直方图”是什么意思-在哪里开始和结束?还有其他问题,但这不是大问题。
亚伦·沃特斯

@Aaron Watters:子直方图的起始位置显式存储在堆栈中(start属性)。结束位置是pos堆栈弹出时直方图中的当前位置(变量),请参见height < top().height分支。
jfs

14

此处的其他答案在使用两个堆栈表示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)
  }
}

一旦有了笛卡尔树,该算法将花费时间O(n),因为我们只对每个节点进行一次访问,并为每个节点执行O(1)工作。

事实证明,有一个简单的线性时间算法可以构建笛卡尔树。您可能会想构建一个“自然”的方法是扫描阵列,找到最小值,然后从左右两个子阵列递归地构建笛卡尔树。问题在于,找到最小值的过程确实非常昂贵,这可能需要花费时间Θ(n 2)。

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

  • 首先,笛卡尔树遵循heap属性:每个元素都小于或等于其子元素。这样做的原因是,笛卡尔树的根是整个阵列中的最小值,其子女中最小的元素及其子阵等

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

这两个规则为我们提供了很多信息,如果我们从数组的前k个元素的笛卡尔树开始并且想要为前k + 1个元素形成笛卡尔树,将会发生什么。这个新元素必须终止于笛卡尔树的右脊上-该树的一部分是从根开始并仅向右迈出的步伐形成的-因为否则,将有序地遍历它。并且,在该正确的书脊中,必须以使其比其上方的所有内容都大的方式放置它,因为我们需要遵守heap属性。

实际将新节点添加到笛卡尔树的方式是从树中最右边的节点开始并向上走,直到您击中树的根或找到具有较小值的节点为止。然后,使新值的最后一个节点作为其左子节点。

这是在一个小数组上的该算法的痕迹:

+---+---+---+---+
| 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

还有一个使用分而治之的解决方案。其算法为:

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

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

时间复杂度为O(nlogn)


这是很好的解决方案。尽管最坏的情况是O(n2)。
techolic

2

堆栈解决方案是迄今为止我见过的最聪明的解决方案之一。而且可能很难理解为什么会起作用。

我已经在这里详细解释了这一点

帖子的要点:-

  • 我们的大脑通常认为:
    • 创建各种情况,并尝试找到解决问题所需的约束的价值。
    • 我们很乐意将其转换为代码:-找到每种情况(对(i,j))的约束值(最小值)

聪明的解决方案试图解决这个问题。对于constraint/mintha面积的每个值,最佳的左右极值是什么?

  • 因此,如果我们遍历min数组中的每个可能值。每个值的最左端和最右端分别是什么?

    • 没想到,第一个最左边的值小于,current min而第一个最右边的值小于当前的最小值。
  • 因此,现在我们需要看看是否可以找到一种巧妙的方法来找到小于当前值的第一个左和右值。

  • 想一想:如果我们遍历数组直到说min_i,那么如何建立min_i + 1的解?

  • 我们需要第一个小于min_i的值。

  • 反转该语句:我们需要忽略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. 小于该值的第一个元素是最左端。我到我们的分钟。
  • 我们可以从数组的右边做同样的事情,我们将得到j到最小值。

很难解释这一点,但是如果这很有意义,那么我建议您在此处阅读完整的文章因为它具有更多的见解和细节。


1

我不理解其他条目,但是我想我知道如何在O(n)中执行以下操作。

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

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

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

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

困难的部分是实现(A)和(B),我认为这是JF Sebastian可能解决的,而不是所述的一般问题。


1
顺便说一句,这是我从中得到基本想法的网站:tech-queries.blogspot.com/2011/03/…–
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代码包含在您的答案中的maximumRectangleUnder()函数伪代码中。它处理伪代码丢失的一些极端情况。
Azeem

-1

您可以使用O(n)方法,该方法使用堆栈来计算直方图下的最大面积。

long long histogramArea(vector<int> &histo){
   stack<int> s;
   long long maxArea=0;
   long long area= 0;
   int i =0;
   for (i = 0; i < histo.size();) {
    if(s.empty() || histo[s.top()] <= histo[i]){
        s.push(i++);
    }
    else{
        int top = s.top(); s.pop();
        area= histo[top]* (s.empty()?i:i-s.top()-1);
        if(area >maxArea)
            maxArea= area;
    }
  }
  while(!s.empty()){
    int top = s.top();s.pop();
    area= histo[top]* (s.empty()?i:i-s.top()-1);
    if(area >maxArea)
        maxArea= area;
 }
 return maxArea;
}

有关说明,您可以在这里阅读http://www.geeksforgeeks.org/largest-rectangle-under-histogram/

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.