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

时间:2010-11-30 08:08:19

标签: algorithm

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

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

对此的答案是6,3 * 2,使用col1和col2。

O(n ^ 2)蛮力对我来说很清楚,我想要一个O(n log n)算法。我试图按照最大增加子序列O(n log n)算法的方式来思考动态编程,但我没有继续前进。我应该使用分而治之的算法吗?

PS:如果没有这样的解决方案,请求具有足够声誉的人删除分而治之标签。

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

10 个答案:

答案 0 :(得分:67)

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

首先观察:

要查找最大矩形,如果对于每个条形x,我们知道其每一侧的第一个较小的条形,假设为lr,我们确定height[x] * (r - l - 1) 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]的新元素out是右边第一个较小的。

enter image description here

还是不能相信堆栈[x-1]是堆栈[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].

因此,堆栈[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

答案 1 :(得分:43)

除蛮力方法外,还有三种方法可以解决这个问题。我会写下所有这些。 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)

此解决方案很棘手,我从explanation without graphsexplanation with graphs学习了如何执行此操作。我建议你在阅读下面的解释之前阅读这两个链接。没有图表很难解释,所以我的解释可能难以理解。

以下是我的解释:

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

  2. 要获得某个条形的最大矩形(比如bar [i],第(i + 1)条),我们只需找出最大的区间 包含这个栏。我们所知道的是,这个区间中的所有条都必须至少与bar [i]相同。所以,如果我们弄清楚有多少 在bar [i]的左边有连续的相同高度或更高的条形,并且在条形图的右边有多少连续的相同高度或更高的条形[i],我们 将知道间隔的长度,即bar [i]的最大矩形的宽度。

  3. 要计算bar [i]左边的连续相同高度或更高的条形数量,我们只需要找到左边最近的较短的条形 因为bar [i]之间的所有条形将是连续的相同高度或更高的条形。

  4. 我们使用堆栈动态跟踪所有比某个条形更短的左边条。换句话说,如果我们从第一个栏重复到bar [i],当我们到达栏[i]并且没有更新堆栈时, 堆栈应存储不高于bar [i-1]的所有条形,包括bar [i-1]本身。我们将bar [i]的高度与堆栈中的每个条形进行比较,直到我们找到一个比bar [i]更短的条,这是最短的条形。 如果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;
    }
    

答案 2 :(得分:15)

the @IVlad's answer 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

Linear search using a stack of incomplete subproblems

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

  

我们处理元素   从左到右的顺序并保持一个   关于开始的一堆信息但是   尚未完成的亚组织图。每当   一个新元素到来它受到了   遵守以下规则。如果堆栈   是空的我们打开一个新的子问题   将元素推入堆栈。   否则我们将它与元素进行比较   在堆栈顶部。如果新的是   更大的我们再次推动它。如果是新的   一个是平等的我们跳过它。在所有这些   案件,我们继续下一个新的   元件。如果新的更少,我们   完成最顶层的子问题   更新最大面积w.r.t.该   堆栈顶部的元素。然后,   我们丢弃顶部的元素,并且   重复保持的程序   目前的新元素。这样,全部   子问题完成直到   堆栈变空,或者它的顶部   element小于或等于   新元素,导致行动   如上所述。如果所有元素都有   已经处理,堆栈不是   但是空了,我们完成剩下的   通过更新最大值来解决子问题   地区w.r.t.对于元素   顶部。

     

更新w.r.t.一个元素,我们   找到最大的矩形   包括那个元素。观察一下   更新最大区域   除了那些之外的所有元素   跳过。如果跳过某个元素,   然而,它具有相同的最大值   矩形作为元素的顶部   堆叠在那个时候将是   稍后更新。的高度   当然,最大的矩形是   元素的价值。在那个时间   更新,我们知道有多远   最大的矩形向右延伸   元素,因为那时,为了   第一次,一个较小的新元素   身高来了。信息,如何   最大的矩形延伸到   元素的左边是可用的   如果我们也将它存储在堆栈中。

     

因此我们修改了程序   如上所述。如果是新元素   立刻推,要么是因为   堆栈为空或大于   堆栈的顶部元素,   包含它的最大矩形   向左延伸不远   当前的元素。如果被推了   经过几个元素之后   弹出堆栈,因为它是   少于这些元素,最大的   包含它的矩形延伸到   离开最远的那个   最近弹出的元素。

     

每个元素都被推送并弹出   大多数曾经和每一步   程序至少有一个要素是   推或弹。自金额   为决策和更新工作   是不变的,复杂的   算法是通过摊销的O(n)   分析

答案 3 :(得分: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;
}

答案 4 :(得分:2)

使用Divide and Conquer还有另一种解决方案。它的算法是:

1)将阵列分成2个部分,其中最小高度为断点

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

时间复杂性来自O(nlogn)

答案 5 :(得分:2)

堆栈解决方案是迄今为止我见过的最聪明的解决方案之一。并且可能有点难以理解为什么这样有效。

我已经详细解释了同样的问题here

帖子摘要点: -

  • 我们的大脑认为的一般方式是: -
    • 创建所有情况并尝试找到解决问题所需的约束值。
    • 我们乐意将其转换为代码为: - 为每种情况找到约束(min)的值(对(i,j))

聪明的解决方案试图解决问题。对于tha区域的每个constraint/min值,左右极端的最佳可能性是什么?

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

    • 很少有人想到,第一个最左边的值小于current min,同样也是第一个最右边的值小于当前最小值。
  • 所以现在我们需要看看我们是否能找到一种聪明的方法来找到小于当前值的第一个左右值。

  • 想想:如果我们遍历数组部分说到min_i,那么如何建立min_i + 1的解决方案?

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

  • 反转语句:我们需要忽略min_i左边大于min_i的所有值。当我们发现第一个值小于min_i(i)时,我们停止。 一旦我们越过它,曲线中的低谷就会变得毫无用处。在直方图中,(2 4 3)=&gt;如果3是min_i,则4更大是不感兴趣的。
  • Corrollary :在范围(i,j)中。 j是我们正在考虑的最小值.j和它的左值i之间的所有值都是无用的。即使是进一步的计算。
  • 右边的任何直方图,其最小值大于j,将在j处绑定。左边的感兴趣的值形成单调递增的序列,其中j是最大值。 (此处感兴趣的值是可能对后面的数组感兴趣的值)
  • 因为,我们从左到右行进,每个最小值/当前值 - 我们不知道数组的右侧是否有一个小于它的元素。
    • 所以我们必须将它保留在内存中,直到我们知道这个值是无用的。 (因为找到了一个较小的值)
  • 所有这些都会导致我们使用我们自己的stack结构。

    • 我们一直堆着,直到我们不知道它没用。
    • 一旦我们知道事情是垃圾,我们就会从堆栈中删除。
  • 因此,对于每个最小值,要找到其左侧较小的值,我们执行以下操作: -

    1. 弹出更大的元素(无用的值)
    2. 小于该值的第一个元素是左极端。我到我们的最小。
  • 我们可以从数组的右侧做同样的事情,我们将获得j到我们的分钟。

很难解释这一点,但如果这是有道理的,那么我建议阅读完整的文章here,因为它有更多的见解和细节。

答案 6 :(得分:1)

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

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

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

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

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

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

答案 7 :(得分: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)

答案 8 :(得分: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);
}

}

答案 9 :(得分:-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/