leetcode-84-柱状图中最大的矩形-java

论坛 期权论坛 脚本     
匿名技术用户   2021-1-6 15:24   29   0

题目及测试

package pid084;
/*柱状图中最大的矩形

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

https://leetcode-cn.com/problems/largest-rectangle-in-histogram/

以上是柱状图的示例,其中每个柱子的宽度为 1,给定的高度为 [2,1,5,6,2,3]。

 

图中阴影部分为所能勾勒出的最大矩形面积,其面积为 10 个单位。

 

示例:

输入: [2,1,5,6,2,3]
输出: 10



*/
public class main {
 
 public static void main(String[] args) {
  int[][] testTable = {{2,1,5,6,2,3},{2,1,1,2}};
  for (int[] ito : testTable) {
   test(ito);
  }
 }
   
 private static void test(int[] ito) {
  Solution solution = new Solution();
  int rtn;
  long begin = System.currentTimeMillis();
  for (int i = 0; i < ito.length; i++) {
      System.out.print(ito[i]+" ");
  }//开始时打印数组
  rtn = solution.largestRectangleArea(ito);//执行程序
  long end = System.currentTimeMillis();  
  System.out.println("rtn=" + rtn);
  /*for (int i = 0; i < rtn; i++) {
      System.out.print(ito[i]+" ");
  }//打印结果几数组
*/  System.out.println();
  System.out.println("耗时:" + (end - begin) + "ms");
  System.out.println("-------------------");
 }

}

解法1(成功,763ms,较慢)

这个方法是枚举宽,固定宽度,计算对应的高度

计算从i开始到i+1...length为止所有nowWidth * nowHeight,nowWidth不断+1,nowHeight取一路上height的最小值

package pid084;

class Solution {
    public int largestRectangleArea(int[] heights) {
     int length = heights.length;
     if(length == 0){
      return 0;
     }
     int maxSize= 0;
     for(int i=0;i<length;i++){
      int nowHeight = heights[i];
      int nowWidth = 0;
      for(int j=i;j<length;j++){
       if(heights[j] < nowHeight){
        nowHeight = heights[j];
       }
       nowWidth++;
       if(maxSize < nowHeight * nowWidth){
           maxSize = nowHeight * nowWidth;
          }
      }
      
     }    
     return maxSize;
    }
}

解法2(别人的)

如果我们枚举「高」,我们可以使用一重循环枚举某一根柱子,将其固定为矩形的高度 h。随后我们从这跟柱子开始向两侧延伸,直到遇到高度小于 h 的柱子,就确定了矩形的左右边界。如果左右边界之间的宽度为 w,那么对应的面积为 wh

    class Solution {
    public:
        int largestRectangleArea(vector<int>& heights) {
            int n = heights.size();
            int ans = 0;
            for (int mid = 0; mid < n; ++mid) {
                // 枚举高
                int height = heights[mid];
                int left = mid, right = mid;
                // 确定左右边界
                while (left - 1 >= 0 && heights[left - 1] >= height) {
                    --left;
                }
                while (right + 1 < n && heights[right + 1] >= height) {
                    ++right;
                }
                // 计算面积
                ans = max(ans, (right - left + 1) * height);
            }
            return ans;
        }
    };

解法3(别人的)

我们归纳一下枚举「高」的方法:

首先我们枚举某一根柱子 i 作为高 h=heights[i];

随后我们需要进行向左右两边扩展,使得扩展到的柱子的高度均不小于 h。换句话说,我们需要找到左右两侧最近的高度小于 h 的柱子,这样这两根柱子之间(不包括其本身)的所有柱子高度均不小于 h,并且就是 i 能够扩展到的最远范围。

那么我们先来看看如何求出一根柱子的左侧且最近的小于其高度的柱子。除了根据「前言」部分暴力地进行枚举之外,我们可以通过如下的一个结论来深入地进行思考:

对于两根柱子 j0 以及 j1,如果 j0<j1 并且 heights[j0]≥heights[j1],那么对于任意的在它们之后出现的柱子 i(j1<i),j0 一定不会是 i 左侧且最近的小于其高度的柱子。

换句话说,如果有两根柱子 j0 和 j1,其中 j0 在 j1 的左侧,并且 j0 的高度大于等于 j1,那么在后面的柱子 i 向左找小于其高度的柱子时,j1 会「挡住」j0,j0 就不会作为答案了。

这样以来,我们可以对数组从左向右进行遍历,同时维护一个「可能作为答案」的数据结构,其中按照从小到大的顺序存放了一些 j 值。根据上面的结论,如果我们存放了 j0,j1, ,js,那么一定有 height[j0]<height[j1]<<height[js],因为如果有两个相邻的 j 值对应的高度不满足 < 关系,那么后者会「挡住」前者,前者就不可能作为答案了。

当我们枚举到第 i 根柱子时,我们的数据结构中存放了 j0,j1, ,js,如果第 i 根柱子左侧且最近的小于其高度的柱子为 ji,那么必然有

height[j0]<height[j1]<<height[ji]<height[i]≤height[ji+1]<<height[js]

这样我们就可以使用二分查找的方法找到 i 对应的 jij_iji,但真的需要吗?当我们枚举到 i+1i+1i+1 时,原来的 i 也变成了 j 值,因此 i 会被放入数据结构。由于所有在数据结构中的 j 值均小于 i,那么所有高度大于等于 height[i] 的 j 都不会作为答案,需要从数据结构中移除。而我们发现,这些被移除的 j 值恰好就是

ji+1, ,js

这样我们在枚举到第 i 根柱子的时候,就可以先把所有高度大于等于 height[i] 的 j 值全部移除,剩下的 j 值中高度最高的即为答案。在这之后,我们将 i 放入数据结构中,开始接下来的枚举。此时,我们需要使用的数据结构也就呼之欲出了,它就是栈。

栈中存放了 j 值。从栈底到栈顶,j 的值严格单调递增,同时对应的高度值也严格单调递增;

当我们枚举到第 i 根柱子时,我们从栈顶不断地移除 height[j]≥height[i] 的 j 值。在移除完毕后,栈顶的 j 值就一定满足 height[j]<height[i],此时 j 就是 i 左侧且最近的小于其高度的柱子。
这里会有一种特殊情况。如果我们移除了栈中所有的 j 值,那就说明 i 左侧所有柱子的高度都大于 height[i],那么我们可以认为 i 左侧且最近的小于其高度的柱子在位置 j=1,它是一根「虚拟」的、高度无限低的柱子。这样的定义不会对我们的答案产生任何的影响,我们也称这根「虚拟」的柱子为「哨兵」。

我们再将 i 放入栈顶。

栈中存放的元素具有单调性,这就是经典的数据结构「单调栈」了。

例子

我们用一个具体的例子 [6,7,5,2,4,5,9,3]来帮助读者理解单调栈。我们需要求出每一根柱子的左侧且最近的小于其高度的柱子。初始时的栈为空。

我们枚举 6,因为栈为空,所以 6 左侧的柱子是「哨兵」,位置为 -1。随后我们将 6 入栈。
栈:[6(0)]。(这里括号内的数字表示柱子在原数组中的位置)

我们枚举 7,由于 6<7,因此不会移除栈顶元素,所以 7 左侧的柱子是 6,位置为 0。随后我们将 7 入栈。
栈:[6(0), 7(1)]

我们枚举 5,由于 7≥5,因此移除栈顶元素 7。同样地,6≥5,再移除栈顶元素 6。此时栈为空,所以 5 左侧的柱子是「哨兵」,位置为 1。随后我们将 5 入栈。
栈:[5(2)]

接下来的枚举过程也大同小异。我们枚举 2,移除栈顶元素 5,得到 2 左侧的柱子是「哨兵」,位置为 1。将 2 入栈。
栈:[2(3)]

我们枚举 4,5 和 9,都不会移除任何栈顶元素,得到它们左侧的柱子分别是 2,4 和 5,位置分别为 3,4 和 5。将它们入栈。
栈:[2(3), 4(4), 5(5), 9(6)]

我们枚举 3,依次移除栈顶元素 9,5 和 4,得到 3 左侧的柱子是 2,位置为 3。将 3 入栈。
栈:[2(3), 3(7)]

这样以来,我们得到它们左侧的柱子编号分别为 [1,0,1,1,3,4,5,3]。用相同的方法,我们从右向左进行遍历,也可以得到它们右侧的柱子编号分别为 [2,2,3,8,7,7,7,8],这里我们将位置 8看作「哨兵」。

在得到了左右两侧的柱子之后,我们就可以计算出每根柱子对应的左右边界,并求出答案了。

分析

单调栈的时间复杂度是多少?直接计算十分困难,但是我们可以发现:

每一个位置只会入栈一次(在枚举到它时),并且最多出栈一次。

因此当我们从左向右/总右向左遍历数组时,对栈的操作的次数就为 O(N)。所以单调栈的总时间复杂度为 O(N)。

class Solution {
    public int largestRectangleArea(int[] heights) {
        int n = heights.length;
        int[] left = new int[n];
        int[] right = new int[n];
        
        Stack<Integer> mono_stack = new Stack<Integer>();
        for (int i = 0; i < n; ++i) {
            while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i]) {
                mono_stack.pop();
            }
            left[i] = (mono_stack.isEmpty() ? -1 : mono_stack.peek());
            mono_stack.push(i);
        }

        mono_stack.clear();
        for (int i = n - 1; i >= 0; --i) {
            while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i]) {
                mono_stack.pop();
            }
            right[i] = (mono_stack.isEmpty() ? n : mono_stack.peek());
            mono_stack.push(i);
        }
        
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            ans = Math.max(ans, (right[i] - left[i] - 1) * heights[i]);
        }
        return ans;
    }
}

解法4(别人的)

单调栈 + 常数优化

在方法一中,我们首先从左往右对数组进行遍历,借助单调栈求出了每根柱子的左边界,随后从右往左对数组进行遍历,借助单调栈求出了每根柱子的右边界。那么我们是否可以只遍历一次就求出答案呢?

答案是可以的。在方法一中,我们在对位置 i 进行入栈操作时,确定了它的左边界。从直觉上来说,与之对应的我们在对位置 i 进行出栈操作时可以确定它的右边界!仔细想一想,这确实是对的。当位置 i 被弹出栈时,说明此时遍历到的位置 i0 的高度小于等于 height[i],并且在 i0 与 i 之间没有其他高度小于等于 height[i]的柱子。这是因为,如果在 i 和 i0 之间还有其它位置的高度小于等于 height[i] 的,那么在遍历到那个位置的时候,i 应该已经被弹出栈了。所以位置 i0 就是位置 i 的右边界。

等等,我们需要的是「一根柱子的左侧且最近的小于其高度的柱子」,但这里我们求的是小于等于,那么会造成什么影响呢?答案是:我们确实无法求出正确的右边界,但对最终的答案没有任何影响。这是因为在答案对应的矩形中,如果有若干个柱子的高度都等于矩形的高度,那么最右侧的那根柱子是可以求出正确的右边界的,而我们没有对求出左边界的算法进行任何改动,因此最终的答案还是可以从最右侧的与矩形高度相同的柱子求得的。读者可以仔细思考一下这一步。

在遍历结束后,栈中仍然有一些位置,这些位置对应的右边界就是位置为 n 的「哨兵」。我们可以将它们依次出栈并更新右边界,也可以在初始化右边界数组时就将所有的元素的值置为 n。

我们沿用方法一中的例子 [6,7,5,2,4,5,9,3],下面的幻灯片给出了优化后的流程。

https://leetcode-cn.com/problems/largest-rectangle-in-histogram/solution/zhu-zhuang-tu-zhong-zui-da-de-ju-xing-by-leetcode-/

class Solution {
    public int largestRectangleArea(int[] heights) {
        int n = heights.length;
        int[] left = new int[n];
        int[] right = new int[n];
        Arrays.fill(right, n);
        
        Stack<Integer> mono_stack = new Stack<Integer>();
        for (int i = 0; i < n; ++i) {
            while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i]) {
                right[mono_stack.peek()] = i;
                mono_stack.pop();
            }
            left[i] = (mono_stack.isEmpty() ? -1 : mono_stack.peek());
            mono_stack.push(i);
        }
        
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            ans = Math.max(ans, (right[i] - left[i] - 1) * heights[i]);
        }
        return ans;
    }
}

分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:7942463
帖子:1588486
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP