手摸手提桶跑路——LeetCode42. 接雨水

179 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第33天,点击查看活动详情

题目描述

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

示例 1:

捕获.PNG

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 

示例 2:

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

提示:

  • n == height.length
  • 1 <= n <= 2 * 104
  • 0 <= height[i] <= 105

解题思路

老观众都熟悉,这道题有 手摸手提桶跑路——LeetCode11. 盛最多水的容器 内味儿了。

实际上还是 短板理论 的支撑。不过这道题不同的是“板”变成了“柱子”,也就是变粗了,这意味着柱子的宽度不能忽略不计了。

我们仔细看看示例一的图。height[0]height[1] 之间是没有水的,最后一根柱子后面也是没有水的。这意味着当柱子的数量为 n 时,对于任意一根柱子 height[i] 来说,只有左右两边都有柱子的情况下,height[i] 这根柱子才 有可能 可以接到雨水,即 height[0, i) 和 height(i,n-1] 两个区间中,各至少有一根柱子的长度大于 0

仔细观察,有的柱子 height[i] 即使左右两边有高度大于 0 的其他柱子,好像也接不到雨水嗷。

微信图片_20201117133144.jpg

嗷,具体情况具体分析是吧,行。

其实我们可以发现,在 height[i] 左右两侧都有比 height[i] 的柱子时,就一定能接到雨水了。

那么我遍历 height 数组,对于每个 height[i],我去左边找到比它更高的柱子 height[left],去右边找到比它更高的柱子 height[right],根据短板理论,短的那根柱子决定接水量,所以短板 minHeight = Math.min(height[left], height[right]),那么当前柱子的接水量就是 minHeight - height[i]。最后累加就完事了。

最后,点击执行代码!

答案错了,少了1滴水,为啥呢。

捕获.PNG

我检查了一下,问题出在这里,我中间高度为 0 的柱子,可接水量为 2,但是实际上代码只给我算成 1,因为找的是 height[i] 左右比它高的柱子,找到就完事了,所以只找到了比较短的那两根柱子。而实际上它真正容量为 Math.min(左右两边高度最高的柱子) 决定的。

这逻辑就没问题了,提交代码,通过,撒花~~

题解

var trap = function(height) {
    const lens = height.length;
    // 找一个左边右边都比当前高的
    let res = 0;
    for(let i=0; i<lens; ++i) {
        let left = i - 1, right = i + 1, lMax = height[i], rMax = height[i];
        while(left >= 0) {
            lMax = Math.max(height[left], lMax);
            left--;
        }
        while(right < lens) {
            rMax = Math.max(height[right], rMax);
            right++;
        }
        if(lMax != 0 && rMax != 0) {
            res += Math.min(lMax, rMax) - height[i];
        }
    }
    return res;
};

捕获.PNG

优化

老观众都知道了吧,我们是什么?

我们是顶级前端好吧,这个执行用时 1s 不得优化一下吗?

这里耗时的地方应该是对于每个柱子 height[i] 都要找到左右两边最高的柱子。我一下子就想到 最高温度 了,那题找第i天右侧第一高温度是过几天,用的单调栈,那我这题是找两边最高的,是不是也能用单调栈。

于是乎我维护了一份左侧单调栈和右侧单调栈,每次查找左右最高柱子时就只要读取值,提升了不少效率。

题解

/**
 * @param {number[]} height
 * @return {number}
 */
var trap = function(height) {
    const lens = height.length;

    const stack = [];
    let max = 0;

    for(let i=0; i<lens; ++i) {
        if(height[i] > max) {
            max = height[i];
        }
        stack.push(max);
    }

    const stack2 = [];
    max = 0;

    for(let i=lens-1; i>=0; --i) {
        if(height[i] > max) {
            max = height[i];
        }
        stack2.push(max);
    }

    return height.reduce((f, c, i)=>{
        return f += Math.min(stack[i], stack2[stack2.length-i-1]) - c;
    }, 0);
};

捕获.PNG

题外话

其实后面单调栈的优化我原本是用一个单调栈省去左侧遍历的,因为右侧我不太熟练,想着要遍历两次生成两个栈,有点怪怪的。测试发现一个单调栈在第一版的基础上效率快了一倍,从 1000ms 到 500ms,两个单调栈效率从 1000ms 到 70ms,基本上都快翻了一倍。

想起个笑话,老板急着赶需求,让程序加班,程序说要一个月才能完成,老板想着再招个人,15天应该就搞定了吧,按这个理论30个人一天就能搞定了。

这道题让我有了不少收获,学以致用,只有真正理解的东西才是属于自己的。一个单调栈会用,两个就懵逼了,这是很严重的问题,暴露出我的不足之处,日后还是要静下心来,勤能补拙。