LC 42 接雨水:暴力超时卡半天?前后缀数组一用就通了

说真的,接雨水这道题,我敢说刷题的朋友都绕不开。 我第一次碰它的时候,上来就写暴力解法,小例子跑得溜溜的。 一提交直接超时,盯着 O (n²) 的复杂度愣了半天。 后来才反应过来,好多计算都是重复的,完全可以提前算好存起来。 今天就跟大家聊聊这个最容易理解的优化思路 ------ 前后缀最大值数组,看完你也能秒写出来。

题目说的啥

题目很直白。 给你一个数组,每个数字代表一根宽度为 1 的柱子高度。 问下雨之后,这些柱子之间的凹槽,总共能接住多少雨水。 本质就是算出每个位置能存多少水,最后加总就行。

从暴力超时想到的优化思路

我最开始的暴力思路其实很朴素。 每个位置能存多少水,不就是看左边最高的柱子和右边最高的柱子,取更矮的那个,再减去当前柱子的高度嘛。 存不了水就按 0 算。

但问题也出在这。 每算一个位置,都要往左扫一遍找最大值,往右扫一遍找最大值。 数据量一大,时间直接就爆了。

当时我就想,这些最大值其实是有递推关系的啊。 比如第 i 个位置左边的最大值,不就是「第 i-1 个位置的左边最大值」和「当前柱子高度」里更大的那个吗? 根本不用每次都从头扫。

说白了就是空间换时间。 我先花一次遍历的功夫,把每个位置左边的最高高度都算出来,存成一个前缀数组。 再倒着遍历一次,把每个位置右边的最高高度也存成一个后缀数组。 最后再扫一遍,挨个算每个位置的存水量,加起来就完事了。

手算一遍就懂了

拿示例 [4,2,0,3,2,5] 来走一遍,一眼就清晰。

先算左边最大值数组 preMax,从左往右推:

  • 第 0 位左边只有自己:4
  • 第 1 位:max (前一位最大值 4, 当前高度 2) = 4
  • 第 2 位:max (4, 0) = 4
  • 第 3 位:max (4, 3) = 4
  • 第 4 位:max (4, 2) = 4
  • 第 5 位:max (4, 5) = 5 最终 preMax 就是 [4,4,4,4,4,5]

再算右边最大值数组 sufMax,从右往左推:

  • 第 5 位右边只有自己:5
  • 第 4 位:max (当前高度 2, 后一位最大值 5) = 5
  • 第 3 位:max (3, 5) = 5
  • 第 2 位:max (0, 5) = 5
  • 第 1 位:max (2, 5) = 5
  • 第 0 位:max (4, 5) = 5 最终 sufMax 就是 [5,5,5,5,5,5]

最后每个位置取两者的最小值,减去当前柱子高度: 第 0 位:min (4,5)-4 = 0 第 1 位:min (4,5)-2 = 2 第 2 位:min (4,5)-0 = 4 第 3 位:min (4,5)-3 = 1 第 4 位:min (4,5)-2 = 2 第 5 位:min (5,5)-5 = 0 加起来 0+2+4+1+2+0 = 9,和示例答案完全对上。

是不是手算一遍就特别清楚? 其实就是把暴力法里重复的计算,提前批量算好了。

代码实现

思路通了,代码写起来就很顺。 这是整理好的 JavaScript 版本,直接提交就能过:

JavaScript 复制代码
var trap = function(height) {
    const n = height.length;
    // preMax[i]:第i个位置(含i本身)左边的最大高度
    const preMax = Array(n);
    preMax[0] = height[0]; // 第一个位置左边只有自己,别漏了初始化
    for (let i = 1; i < n; i++) {
        preMax[i] = Math.max(preMax[i - 1], height[i]);
    }

    // sufMax[i]:第i个位置(含i本身)右边的最大高度
    const sufMax = Array(n);
    sufMax[n - 1] = height[n - 1]; // 最后一个位置同理
    for (let i = n - 2; i >= 0; i--) {
        // 别手滑写成i++,我一开始写反直接数组越界,踩过的坑
        sufMax[i] = Math.max(sufMax[i + 1], height[i]);
    }

    let ans = 0;
    for (let i = 0; i < n; i++) {
        // 左右挡板取更矮的,减去当前柱子高度,就是能存的水量
        ans += Math.min(preMax[i], sufMax[i]) - height[i];
    }
    return ans;
};

复杂度分析

时间复杂度是 O (n),总共遍历了三次数组,每一趟都是线性的。 空间复杂度是 O (n),用了两个长度为 n 的数组来存储最大值。 比起暴力法的 O (n²) 时间,已经提升了一大截,在题目数据范围内完全够用。

我踩过的两个小坑

说两个自己写的时候踩过的坑,大家注意避开。 第一个是边界初始化忘了写。preMax 第一个元素和 sufMax 最后一个元素一定要先赋值,不然循环里取到 undefined,结果直接变 NaN。 第二个是后缀数组的遍历方向搞反。从后往前遍历起始下标是 n-2,步长是 i--,写顺了很容易手滑写成 i++,跑起来直接报错。

最后聊聊

其实这个解法本质就是动态规划的思路,利用之前的计算结果推导出当前值,避免重复劳动。 对于刚刷这道题的朋友来说,这个思路最好理解,也最不容易写错。

你第一次写接雨水用的是什么方法? 是暴力直接莽,还是直接就想到了前后缀优化? 评论区聊聊你的刷题经历,我每条都会看~ 如果觉得文章对你有帮助,点个赞让更多朋友看到呀~

相关推荐
小林ixn1 小时前
从 ??= 到 onKeyDown:一个 React 组件的“自我修养”
前端·javascript·react.js
delishcomcn2 小时前
AI视觉识别+分切算法:电化铝缺陷检测与裁切一体化解锁
人工智能·算法
এ慕ོ冬℘゜2 小时前
前端实战:jQuery 多条件联合搜索(标题模糊查询 + 日历时间段筛选)
前端·javascript·jquery
触底反弹2 小时前
深入理解大模型采样:Temperature、Top-K、Top-P 的原理与实战
人工智能·算法·面试
雪碧聊技术3 小时前
力扣 LCR 091. 粉刷房子 —— 动态规划入门详解
算法·动态规划
随心点儿3 小时前
js postMessage 实现跨域发送消息-应用示例
javascript·ecmascript·postmessage
kyriewen4 小时前
我让AI改一个bug——它偷偷动了5个我没让它碰的地方
前端·javascript·ai编程
CV-Climber5 小时前
检索技术的实际应用
人工智能·算法