攒青豆|「青训营 X 码上掘金」主题创作

99 阅读2分钟

当青训营遇上码上掘金。

前言

呦,这不接雨水吗?

本文是「青训营 X 码上掘金」主题创作活动主题4“攒青豆”的题解。接雨水是常考的面试题,本文将介绍动态规划、单调栈、双指针三种解题思路。

主题:攒青豆

现有 n 个宽度为 1 的柱子,给出 n 个非负整数依次表示柱子的高度,排列后如下图所示,此时均匀从上空向下撒青豆,计算按此排列的柱子能接住多少青豆。(不考虑边角堆积)

演示

点击码上掘金,查看详细代码。

思路

其实应该是能接住多少格豆子。

暴力做法就是对每个位置向左向右进行扫描,记录左边和右边的最大高度,然后取两边最小值减去自身高度,得出此位置能接多少青豆。这种做法时间复杂度会来到 O(n^2) ,并不是较好的做法,还有明显的优化空间。

思路 1:动态规划

但其实按照朴素算法,在每个点向左右两边扫描时存在重复扫描的情况,可以维护两个数组 leftMaxrightMax一次性扫描出每个位置的单侧最大高度值,这样时间复杂度可以达到 O(n) 。动态规划的解法十分通俗易懂。

var trap = function (height) {
  const n = height.length;
  if (n == 0) {
    return 0;
  }
  // leftMax
  const leftMax = new Array(n).fill(0);
  leftMax[0] = height[0];
  for (let i = 1; i < n; ++i) {
    leftMax[i] = Math.max(leftMax[i - 1], height[i]);
  }
  // rightMax
  const rightMax = new Array(n).fill(0);
  rightMax[n - 1] = height[n - 1];
  for (let i = n - 2; i >= 0; --i) {
    rightMax[i] = Math.max(rightMax[i + 1], height[i]);
  }
  // ans
  let ans = 0;
  for (let i = 0; i < n; ++i) {
    ans += Math.min(leftMax[i], rightMax[i]) - height[i];
  }
  return ans;
}

思路 2:双指针

因为动态规划需要维护两个数组表示左侧和右侧最大值,空间复杂度任然很高,但数组每个元素最后其实只被用到了一次,十分浪费。我们可以使用双指针的方法,进一步降低空间复杂度。
一个指针从左向右遍历,一个指针从右向左遍历,只要左指针位置高度小于右指针位置高度,则代表其右侧支持形成水坑,但还需左侧 leftMax 支持,即 leftMax - height[left] > 0 ,但此处无需用 if 额外判断,反之亦然。若右侧支持,则左指针可不断向右增加,并更新 leftMax 直到右边不支持,反之右侧向左减少,直到指针相等。

var trap = function (height) {
  let ans = 0,
    left = 0,
    right = height.length - 1,
    leftMax = 0,
    rightMax = 0;
  while (left < right) {
    leftMax = Math.max(leftMax, height[left]);
    rightMax = Math.max(rightMax, height[right]);
    if (height[left] < height[right]) {
      ans += leftMax - height[left];
      ++left;
    } else {
      ans += rightMax - height[right];
      --right;
    }
  }
  return ans;
}

思路 3:单调栈

类似括号匹配,维护一个栈,低柱子如果匹配到高柱子,代表它们之间有空间盛水(豆子)。

  • 情况一:遍历柱子高度,若栈为空,将此位置入栈;
  • 情况二:若此处高度小于等于栈顶(上一位置),代表此位置可能可以盛水(如果后面又更高的柱子),将此位置入栈;
  • 情况三:若此处高度大于栈顶(上一位置),代表之前可能有位置可以盛水,此时将栈顶出栈,计算盛水量,直到此处高度小于等于栈顶(回到上一情况)。

计算盛水量时,指针 left 指向下一个栈顶位置,其实就是积水左边的柱子,而此时位置 i 就是右边的柱子,两者最小值减去之前 top 位置的高度就是可以形成积水的高度。
小细节:为什么两根柱子最小值一定大于等于 top 位置高度?即为什么 top 位置高度一定小于等于 left 位置高度?因为如果 top 位置大于 left 位置就会进入情况三, left 就被出栈结算了。
单调栈方法很优秀但不太容易思考。

var trap = function (height) {
  let ans = 0;
  const stack = [];
  const n = height.length;
  for (let i = 0; i < n; ++i) {
    while (stack.length && height[i] > height[stack[stack.length - 1]]) {
      const top = stack.pop();
      if (stack.length) {
        const left = stack[stack.length - 1],
        currWidth = i - left - 1,
        currHeight = Math.min(height[left], height[i]) - height[top];
        ans += currWidth * currHeight;
      }
    }
    stack.push(i);
  }
  return ans;
}

参考资料

42. 接雨水
接雨水 - 接雨水
【接雨水】单调递减栈,简洁代码,动图模拟 - 接雨水
详细通俗的思路分析,多解法 - 接雨水