当青训营遇上码上掘金 | 青豆拌雨水味道更好 (力扣42. 接雨水)

88 阅读1分钟

1. 题目

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

💡 以下为上图例子的解析:

输入:height = [5,0,2,1,4,0,1,0,3] 输出:17

解析:上面是由数组 [5,0,2,1,4,0,1,0,3] 表示的柱子高度,在这种情况下,可以接 17个单位的青豆。

2. 思路

这道题目原题为著名题目42. 接雨水,本题是攒青豆,一共有几种解题方法。参考接雨水思路,我将总结2种较为常见的解法:按列解与动态规划。

法1:按列算

这是最直观也是最容易想到的方法,后面的动态规划方法也是基于本方法优化,因此需要掌握。我们遍历数组height的所有元素,按照列来算。主要思路如下:

对于每一列,我们都求当前列左右最高边界,然后进行判断:

  1. 如果左右列都找到大于当前列的高度,那么:计算当前列高度与最小值的高度差,并加入总数
  2. 如果存在一边小于等于当前列高度,则说明会朝至少一遍漏水,因此:我们需要跳过当前列
  3. 时间复杂度:O(n^2)

代码

class Solution {
    public int trap(int[] height) {
        int count = 0;
        for (int i = 0; i < height.length; i++) {
            int left = height[i];
            int right = height[i];
            // 找到当前列左右最高的 且 比当前列高的index作为边界
            for (int j = i; j >= 0; j--) {
                left = Math.max(left, height[j]);
            }
            for (int j = i; j < height.length; j++) {
                right = Math.max(right, height[j]);
            }
            // 如果左右列有一边最高等于当前列,说明装不了水
            if (left == height[i] || right == height[i]) continue;
            // 算当前列能装水的单位数
            count += Math.min(left, right) - height[i];
        }
        return count;
    }
}

法2:动态规划

由于需要对每列寻找左边与右边的最大高度,法1是在遍历的时候同时寻找,因此时间复杂度是O(n^2)。但是我们可以在遍历之前用2个数组保存每个列的左右最大高度,从而可以把时间复杂度优化到O(n)。主要思路总结如下:

  1. 数组保存第i个位置的左右墙最高值
  2. 从头到尾遍历,按列求每列的雨水值
  3. 状态转移方程:当前第i列左右墙最高值 = Max(第i-1列最高值, 当前列墙高)

不过我们需要注意dp数组的定义,不同定义后面遍历的条件处理有些许区别。这里我们把leftMax[i]数组定义成:在第i列中左侧(包括自身)的最大高度,leftMax[i]同理。

代码1:dp最大高度包括当前列,因此最小高度=当前列

class Solution {
    public int trap(int[] height) {
        // 打表,首先记录当前列的左右最大高度
        // dp[i]数组意义:在第i列中左/右边最大高度(包括自身)
        int[] leftMax = new int[height.length];
        int[] rightMax = new int[height.length];
				
	// 保存第i个元素的左侧最高的墙高
        leftMax[0] = height[0];
        for (int i = 1; i < height.length; i++) {
            // 状态转移方程:比较前面列的最高和当前列谁高
            leftMax[i] = Math.max(leftMax[i - 1], height[i]);
        }

        // 保存第i个元素的右侧最高的墙高
        rightMax[height.length - 1] = height[height.length - 1];
        for (int i = height.length - 2; i >= 0; i--) {
            rightMax[i] = Math.max(rightMax[i + 1], height[i]);
        }

        int count = 0;
        for (int i = 0; i < height.length; i++) {
            // 如果和当前列相等,说明当前列高度最大,回漏水,直接跳过
            if (leftMax[i] == height[i] || rightMax[i] == height[i]) continue;
            count += Math.min(leftMax[i], rightMax[i]) - height[i];
        }
        return count;
    }
}

代码2:dp最大高度只看前一列,因此遍历需要判断是否≤当前列

class Solution {
    public int trap(int[] height) {
        //时间复杂度:O(n),空间复杂度:O(n)

        int[] leftHighest = new int[height.length];
        int[] rightHighest = new int[height.length];
        
        // 初始值,可以省略
        // leftHighest[0] = 0;
        // rightHighest[0] = 0;
				
	// 保存第i个元素的左侧最高的墙高
        for (int i = 1; i < leftHighest.length; i++) {
            leftHighest[i] = Math.max(leftHighest[i - 1], height[i - 1]);
        }
	// 保存第i个元素的右侧最高的墙高
        for (int i = rightHighest.length - 2; i >= 0; i--) {
            rightHighest[i] = Math.max(rightHighest[i + 1], height[i + 1]);
        }

        int sum = 0;
        for (int i = 0; i < height.length; i++) {
            // 需要跳过小于等于当前列的情况
            int diff = Math.min(leftHighest[i], rightHighest[i]) - height[i];
            if (diff > 0) sum += diff;
        }
        return sum;
    }
}