攒青豆/接雨水

188 阅读3分钟

当青训营遇上码上掘金

本文为 青训营 X 马上掘金 活动 主题4 的讲解。 活动链接:「青训营 X 码上掘金」主题创作活动入营版 开启! - 掘金 (juejin.cn)

代码见:攒青豆 - 码上掘金 (juejin.cn)

前言

看到攒青豆这个主题,我想起了读小学的时候,班主任常教导我们在学习上不仅要取长补短,更要补齐短板,不要让短板限制我们。就好比使用木板拼接成的木桶,其盛水量会被短板所限制。这便是是著名的"短板理论"(短板理论-百度百科)。

回到题目中,一个柱子上方能够接住的青豆,与这跟柱子所在的“桶”有关。一个柱子,设其位置为 i ,则其所在的桶由 0 到 i 中最高的柱子以及 i 到 n 中最高的柱子组成,而能够接住的青豆则取决于“短板”。

ps:攒青豆与力扣 42 题 接雨水基本一样。

解法1:暴力解法

知道怎么求一个柱子能够接住的青豆后,我们就可以很简单地写出如下地解法:对于每条柱子,分别求出其左右两边最高的柱子,再求出其能接住的青豆。

代码如下:

public int soybeans(int[] height) {
    // 对于每一个位置,首先求其左右最大的 height,再计算能够接到的青豆
    if (height == null || height.length <= 2){
        return 0;
    }
    int res = 0;
    int len = height.length;
    int lmax,rmax;
    for(int i = 1; i < len-1; i ++){
        lmax = -1;
        rmax = -1;
        for(int left = 0; left <= i; left ++){
            lmax = Math.max(lmax, height[left]);
        }
        for(int right = i; right < len; right++){
            rmax = Math.max(rmax, height[right]);
        }
        res += Math.min(lmax, rmax) - height[i];
    }
    return res;
}

时间复杂度:O(n2n^2), 空间复杂度:O(1)

在求左右最高柱子的时候,为什么把自身也算进去了呢?

我们可以思考:什么情况下才能接到青豆:

  • 情况1:中间柱子比左右最高的柱子都要矮,这种情况可以接到青豆;
  • 情况2:中间柱子比左右最高柱子中的其中一个柱子要高,这种情况不能接到青豆
  • 情况3:中间柱子比左右最高的柱子都要高,这种情况不能接到青豆

不将自身算进去的话,需要多一次判断,才能计算接到的青豆; 将自身算进去时,计算 lmax = rmax = height[i], Math.min(lmax, rmax) - height[i] 结果为 0,不影响最终结果。

解法2:动态规划

解法一对于每一条柱子,每一次都需要遍历 height 数组寻找其左右最高的柱子,这样的时间复杂度来到了 O(n2n^2),我们可不可以对空间进行优化呢?

观察不难发现,对于柱子 i,其左边最高柱子的高度 lmaxi=max(lmaxi1,height[i])lmax_i = max(lmax_{i-1}, height[i]),同样其右边最高柱子的高度为 rmaxi=max(rmaxi+1,height[i])rmax_i = max(rmax{i+1}, height[i]),因此,我们可以先用动态规划的思想,求出所有柱子的左右最高柱子,再进一步求青豆的数量.

注意动态规划的 base case。

代码如下:

public int soybeans1(int[] height){
    if (height == null || height.length <= 2){
        return 0;
    }
    int res = 0;
    int len = height.length;
    // lrmax[i][0] 表示 i 及其左边最高的柱子,lrmax[i][1] 表示 i 及其右边最高的柱子
    int[][] lrmax = new int[len][2];
    lrmax[0][0] = height[0];
    lrmax[len-1][1] = height[len-1];
    for(int i = 1; i < len; i ++){
        lrmax[i][0] = Math.max(lrmax[i-1][0], height[i]);
    }
    for(int i = len-2; i > 0; i --){
        lrmax[i][1] = Math.max(lrmax[i+1][1], height[i]);
    }

    // 计算青豆
    for(int i = 1; i < len-1; i ++){
        res += Math.min(lrmax[i][0], lrmax[i][1]) - height[i];
    }
    return res;
}

时间复杂度:O(n),空间复杂度O(n)

解法3:使用双指针

这个解法参考自 Labuladong的算法小抄。

进一步优化空间复杂度:使用双指针,边走边算。 具体解析见下面代码中的注释:

public static int soybeans2(int[] height){
    if (height == null || height.length <= 2){
        return 0;
    }
    int res = 0;
    int left = 0, right = height.length-1;
    int lmax = 0, rmax = 0;
    while(left < right){
        // lmax 是柱子 0 到 left 中最高的柱子的高度
        lmax = Math.max(lmax, height[left]);
        // rmax 是柱子 right 到 0 中最高的柱子的高度
        rmax = Math.max(rmax, height[right]);
        
        // 如果 lamx < rmax 则表明 left 柱子的左边最高柱子一定是比右边最高柱子要矮的
        // 此时就能够计算出 left 柱子能够接到的青豆数量
        if (lmax < rmax){
            res += lmax - height[left];
            left ++;
        } else {
        // 否则, rmax >= lmax,表明 right 柱子的右边最高柱子的高度一定小于等于左边最高柱子的高度
        // 此时能够计算出 right 柱子能够接到的青豆数量
            res += rmax - height[right];
            right --;
        }
    }
    return res;
}

时间复杂度:O(n), 空间复杂度:O(1)

参考

labuladong的算法小抄-接雨水