这道题是 LeetCode 第84题,liweiwei题解暴力解法、栈(单调栈、哨兵技巧)

题意

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1。求在该柱状图中,能够勾勒出来的矩形的最大面积。

histogram.png

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

histogram_area.png

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

思路

首先,要想找到第 i 个位置最大面积是什么?这个其实才是这个问题最关键的点。

是以 i 为中心,向左找第一个小于 heights[i] 的位置 left_i;向右找第一个小于 heights[i] 的位置 right_i

即最大面积为 heights[i] * (right_i - left_i - 1),如下图所示:

441ac778821dc26689b31466bced9f61ec241f092bf7e4f0f8699ef4fa3be1b2-1559826097853.png

所以,我们的问题就变成如何找到 right_ileft_i

1. 暴力解法

最简单的思路就是暴力解法(Brute Force),直接利用循环,当循环到 i 时,分别在 i 左右两边分别再接一次循环来寻找相应的值。

2. 动态规划

当我们找 i 左边第一个小于 hegihts[i] 如果 heights[i - 1] >= heights[i],其实就是和求 heights[i - 1]左边第一个小于 heights[i - 1]一样。以此类推,右边同理。

3. 单调栈

其实上面动态规划的解法使用空间换时间,同样的单调栈也是利用栈来记录信息从而达到时间换空间的目的。

那记录什么信息呢?记录高度是不是可以呢?其实是不够的,因为计算矩形还需要计算宽度,很容易知道宽度是由下标确定的,记录了下标其实对应的高度就可以直接从输入数组中的得出,因此,应该记录的是下标。

代码

1. 暴力解法

  1. public class Solution {
  2. public int largestRectangleArea(int[] heights) {
  3. int len = heights.length;
  4. int res = 0;
  5. for (int i = 0; i < len; i++) {
  6. int curHeight = heights[i];
  7. // 找左边最后 1 个大于等于 heights[i] 的下标
  8. int left = i;
  9. while (left > 0 && heights[left - 1] >= curHeight) {
  10. left--;
  11. }
  12. // 找右边最后 1 个大于等于 heights[i] 的下标
  13. int right = i;
  14. while (right < len - 1 && heights[right + 1] >= curHeight) {
  15. right++;
  16. }
  17. int width = right - left + 1;
  18. res = Math.max(res, width * curHeight);
  19. }
  20. return res;
  21. }
  22. }

2. 动态规划

  1. class Solution {
  2. public int largestRectangleArea(int[] heights) {
  3. if (heights == null || heights.length == 0) {
  4. return 0;
  5. }
  6. int n = heights.length;
  7. int[] left_i = new int[n];
  8. int[] right_i = new int[n];
  9. left_i[0] = -1;
  10. right_i[n - 1] = n;
  11. int res = 0;
  12. for (int i = 1; i < n; i++) {
  13. int temp = i - 1;
  14. while (temp >= 0 && heights[temp] >= heights[i]) {
  15. temp = left_i[temp];
  16. }
  17. left_i[i] = temp;
  18. }
  19. for (int i = n - 2; i >= 0; i--) {
  20. int temp = i + 1;
  21. while (temp < n && heights[temp] >= heighes[i]) {
  22. temp = right_i[temp];
  23. }
  24. right_i[i] = temp;
  25. }
  26. for (int i = 0; i < n; i--) {
  27. res = Math.max(res, (right_i[i] - left_i[i] - 1) * heights[i]);
  28. }
  29. }
  30. }

3. 单调栈

  1. class Solution {
  2. public int largestRectangleArea(int[] heights) {
  3. int len = heights.length, res = 0;
  4. Deque<Integer> stack = new ArrayDeque<>(len);
  5. for (int i = 0; i < len; i++) {
  6. // 这个 while 很关键,因为可能不止一个柱形的最大宽度可以被计算出来
  7. while (!stack.isEmpty() && heights[i] < heights[stack.peekLast()]) {
  8. int curHeight = heights[stack.pollLast()];
  9. while (!stack.isEmpty() && heights[stack.peekLast()] == curHeight) {
  10. stack.pollLast();
  11. }
  12. int curWidth;
  13. if (stack.isEmpty()) {
  14. curWidth = i;
  15. } else {
  16. curWidth = i - stack.peekLast() - 1;
  17. }
  18. res = Math.max(res, curHeight * curWidth);
  19. }
  20. stack.addLast(i);
  21. }
  22. while (!stack.isEmpty()) {
  23. int curHeight = heights[stack.pollLast()];
  24. while (!stack.isEmpty() && heights[stack.peekLast()] == curHeight) {
  25. stack.pollLast();
  26. }
  27. int curWidth;
  28. if (stack.isEmpty()) {
  29. curWidth = len;
  30. } else {
  31. curWidth = len - stack.peekLast() - 1;
  32. }
  33. res = Math.max(res, curHeight * curWidth);
  34. }
  35. return res;
  36. }
  37. }

以上代码需要考虑两种特殊的情况:

  1. 弹栈的时候,栈为空;
  2. 遍历完成以后,栈中还有元素;

为此我们可以在输入数组的两端加上两个高度为 0 (或者是 0.5,只要比 1 严格小都行)的柱形,可以回避上面这两种分类讨论。

这两个站在两边的柱形有一个很形象的名词,叫做哨兵(Sentinel)。

有了这两个柱形:

  1. 左边的柱形(第一个柱形)由于它一定比输入数组里任何一个元素小,它肯定不会出栈,因此栈一定不为空;
  2. 右边的柱形(最后的柱形)也正是因为它一定比输入数组里任何一个元素小,它会让所有输入数组里的元素出栈(第一个哨兵元素除外)。

这里栈对应到高度,呈单调增加不减的形态,因此称为单调栈(Monotone Stack)。它是暴力解法的优化,计算每个柱形的高度对应的最大矩形的顺序由出栈顺序决定。

下面是加入了哨兵的写法:

  1. class Solution {
  2. public int largestRectangleArea(int[] heights) {
  3. int len = heights.length, res = 0;
  4. Deque<Integer> stack = new ArrayDeque<>();
  5. int[] new_heights = new int[len + 2];
  6. System.arraycopy(heights, 0, new_heights, 1, len);
  7. for (int i = 0; i < new_heights.length; i++) {
  8. while (!stack.isEmpty() && new_heights[stack.peek()] > new_heights[i]) {
  9. int cur = stack.pop();
  10. res = Math.max(res, (i - stack.peek() - 1) * new_heights[cur]);
  11. }
  12. stack.push(i);
  13. }
  14. return res;
  15. }
  16. }