news 2026/9/6 3:47:36

LeetCode 42:接雨水|前后最大值DP

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LeetCode 42:接雨水|前后最大值DP

一、题目

给定一个非负整数数组height,其中每个元素表示某一列柱子的高度,要求计算这些柱子之间最多能接多少雨水。

例如:

height = [0,1,0,2,1,0,1,3]

可以把它理解成一排高低不同的柱子。

这道题最容易一开始不知道从哪里入手,但真正核心其实只有一句:

每一列能接的水 = min(左边最高柱子, 右边最高柱子) - 当前柱子高度

也就是:

water[i] = Math.min(leftMax[i], rightMax[i]) - height[i];

只要这个公式理解了,整道题就已经解决了一大半。


二、为什么要看左右最高柱子?

假设当前位置:

height[i] = 1

左边最高柱子:

5

右边最高柱子:

3

那么水最多能涨到多高?

不能涨到:

5

因为右边只有:

3

超过 3 的水会从右侧流出去。

所以真正能形成的水面高度是:

min(5,3) = 3

当前柱子本身高度是:

1

所以当前位置能存:

3 - 1 = 2

单位水。

因此公式就是:

Math.min(leftMax[i], rightMax[i]) - height[i]

三、这道题真正的核心不是“水”,而是边界

对于任意一个位置i

水面 ---------------- 左墙 当前柱子 右墙

能接多少水完全取决于:

左边最高墙 右边最高墙

而且只能取较矮的一侧。

所以可以形成一个固定思维:

接雨水不是看相邻柱子,而是看当前位置左右两侧的最高边界。


四、最直接的暴力思路

对于每个位置i

  1. 向左找最高柱子

  2. 向右找最高柱子

  3. 计算当前水量

可以写成:

for (int i = 0; i < height.length; i++) { int leftMax = 0; int rightMax = 0; for (int j = 0; j <= i; j++) { leftMax = Math.max(leftMax, height[j]); } for (int j = i; j < height.length; j++) { rightMax = Math.max(rightMax, height[j]); } ans += Math.min(leftMax, rightMax) - height[i]; }

这个思路正确。

但是每一个位置都重新扫描左右两边。

时间复杂度:

O(n²)

明显存在大量重复计算。


五、优化思路:提前把左右最大值算出来

既然:

每个位置都要知道左边最高 每个位置都要知道右边最高

那就提前保存。

定义:

int[] leftMax = new int[n]; int[] rightMax = new int[n];

其中:

leftMax[i] = 从 0 到 i 的最高柱子

而:

rightMax[i] = 从 i 到 n-1 的最高柱子

这样后面每个位置就不用重新扫描。


六、最终代码

这是我目前掌握并推荐先固定下来的版本:

class Solution { public int trap(int[] height) { // 每一列水量: // min(leftMax[i], rightMax[i]) - height[i] int n = height.length; int[] leftMax = new int[n]; int[] rightMax = new int[n]; int ans = 0; // 初始化左右边界 leftMax[0] = height[0]; rightMax[n - 1] = height[n - 1]; // 计算前缀最大值 for (int i = 1; i < n; i++) { leftMax[i] = Math.max(leftMax[i - 1], height[i]); } // 计算后缀最大值 for (int i = n - 2; i >= 0; i--) { rightMax[i] = Math.max(rightMax[i + 1], height[i]); } // 累加每一列能够存的水 for (int i = 0; i < n; i++) { ans += Math.min(leftMax[i], rightMax[i]) - height[i]; } return ans; } }

七、为什么 leftMax[0] = height[0]?

代码:

leftMax[0] = height[0];

因为对于最左边这个位置:

0 ~ 0

范围里只有它自己。

所以:

左边最高柱子 = height[0]

因此:

leftMax[0] = height[0];

这是前缀最大值的初始条件。


八、为什么 leftMax 要从 1 开始?

代码:

for (int i = 1; i < n; i++) {

因为:

leftMax[0]

已经初始化好了。

对于:

i = 1

开始,我们可以使用:

leftMax[i - 1]

也就是:

leftMax[0]

所以自然从:

1

开始。


九、leftMax 的状态转移怎么理解?

核心:

leftMax[i] = Math.max(leftMax[i - 1], height[i]);

翻译成人话:

到当前位置i为止的最大高度,要么是前面已经出现过的最大高度,要么是当前柱子更高。

例如:

height = [0, 1, 0, 2, 1]

计算:

leftMax[0] = 0

然后:

i = 1 max(0,1) = 1

所以:

leftMax[1] = 1

继续:

i = 2 max(1,0) = 1

所以:

leftMax[2] = 1

继续:

i = 3 max(1,2) = 2

所以:

leftMax[3] = 2

最终:

height: 0 1 0 2 1 leftMax: 0 1 1 2 2

十、rightMax 完全对称

初始化:

rightMax[n - 1] = height[n - 1];

因为最后一个位置右边只有自己。

然后从右往左:

for (int i = n - 2; i >= 0; i--) {

状态转移:

rightMax[i] = Math.max(rightMax[i + 1], height[i]);

意思:

从当前位置i往右的最高柱子,要么是右边已经找到的最大值,要么是当前柱子。


十一、为什么 rightMax 从 n - 2 开始?

因为:

rightMax[n - 1]

已经初始化。

而:

rightMax[i]

需要依赖:

rightMax[i + 1]

所以第一个可以计算的位置就是:

n - 2

例如数组长度:

n = 5

最后一个下标:

4

已初始化。

所以从:

3

开始向左。


十二、最终公式

有了:

leftMax[i] rightMax[i]

以后,每一列水量直接:

Math.min(leftMax[i], rightMax[i]) - height[i]

总水量:

ans += Math.min(leftMax[i], rightMax[i]) - height[i];

这就是整道题最终核心。


十三、用一个小例子完整走一遍

假设:

height = [3,0,2]

先计算:

leftMax

得到:

[3,3,3]

因为:

位置0: 最大 = 3 位置1: max(3,0) = 3 位置2: max(3,2) = 3

再计算:

rightMax

得到:

[3,2,2]

因为:

位置2: 最大 = 2 位置1: max(2,0) = 2 位置0: max(2,3) = 3

所以:

height = [3,0,2] leftMax = [3,3,3] rightMax = [3,2,2]

逐个计算水量。


i = 0

min(3,3) - 3 = 0

i = 1

min(3,2) - 0 = 2

i = 2

min(3,2) - 2 = 0

总水量:

2

正确。


十四、为什么最左边和最右边也可以直接计算?

直觉上最左边和最右边肯定接不了水。

代码却写:

for (int i = 0; i < n; i++)

从头到尾全部算。

为什么没问题?

因为最左边:

leftMax[0] = height[0]

所以:

min(leftMax[0], rightMax[0]) <= leftMax[0] = height[0]

实际上最终水量一定为:

0

最右边同理。

所以不需要特殊处理。


十五、为什么水量不会是负数?

公式:

Math.min(leftMax[i], rightMax[i]) - height[i]

因为:

leftMax[i]

本身包含:

height[i]

所以一定:

leftMax[i] >= height[i]

同理:

rightMax[i] >= height[i]

因此:

min(leftMax[i], rightMax[i]) >= height[i]

所以:

水量 >= 0

不会出现负数。


十六、这是不是动态规划?

可以把它理解成:

前缀 / 后缀 DP。

因为:

leftMax[i]

依赖:

leftMax[i - 1]

而:

rightMax[i]

依赖:

rightMax[i + 1]

都有明显的状态递推关系。

但面试里说:

前后缀最大值

其实会更直观。


十七、复杂度分析

一共进行了三次遍历。

第一次:

计算 leftMax O(n)

第二次:

计算 rightMax O(n)

第三次:

计算答案 O(n)

总时间:

O(n) + O(n) + O(n) = O(n)

所以时间复杂度:

O(n)

额外创建:

leftMax rightMax

两个长度为n的数组。

所以空间复杂度:

O(n)

十八、还能不能优化?

可以。

当前:

时间 O(n) 空间 O(n)

还可以进一步通过:

双指针

将空间复杂度优化到:

O(1)

但当前这版已经非常适合理解和面试保底。

因为它直接对应最核心公式:

water[i] = min(leftMax[i], rightMax[i]) - height[i]

逻辑最清楚。


十九、双指针优化的思想

前后缀数组实际上是为了提前知道:

左边最高 右边最高

但如果使用两个指针:

left right

并同时维护:

leftMax rightMax

就不需要两个数组。

因此可以优化成:

时间 O(n) 空间 O(1)

不过学习顺序应该是:

先理解每列公式 ↓ 再理解前后缀最大值 ↓ 最后学双指针空间优化

而不是直接死背双指针。


二十、面试讲法

如果面试官让我讲,我会这样回答:

对于任意位置i,它能够接的水量取决于它左侧最高柱子和右侧最高柱子中较矮的那个,因此当前水量为min(leftMax[i], rightMax[i]) - height[i]

为了避免对每个位置都重新向左右扫描,我分别预处理一个前缀最大值数组leftMax和后缀最大值数组rightMax

leftMax[i] = max(leftMax[i-1], height[i])rightMax[i] = max(rightMax[i+1], height[i])

最后遍历数组,将每个位置的水量累加。

整体时间复杂度 O(n),额外空间复杂度 O(n)。如果进一步要求 O(1) 空间,可以使用双指针优化。

这段就已经非常完整。


二十一、最容易写错的地方

1. leftMax 初始化错误

应该:

leftMax[0] = height[0];

不能直接从:

0

默认值开始递推而忽略第一个柱子。


2. rightMax 初始化错误

应该:

rightMax[n - 1] = height[n - 1];

3. rightMax 循环方向写反

应该从右向左:

for (int i = n - 2; i >= 0; i--)

因为它依赖:

rightMax[i + 1]

4. 公式用了 max

错误:

Math.max(leftMax[i], rightMax[i])

应该:

Math.min(leftMax[i], rightMax[i])

因为水面由较矮的边界决定。


5. 忘记减当前柱子

错误:

ans += Math.min(leftMax[i], rightMax[i]);

正确:

ans += Math.min(leftMax[i], rightMax[i]) - height[i];

因为柱子本身占据空间,不是水。


二十二、面试前 30 秒速记

看到:

接雨水

先想公式:

每一列水量 = min(左边最高, 右边最高) - 当前高度

代码模板:

int n = height.length; int[] leftMax = new int[n]; int[] rightMax = new int[n]; leftMax[0] = height[0]; for (int i = 1; i < n; i++) { leftMax[i] = Math.max(leftMax[i - 1], height[i]); } rightMax[n - 1] = height[n - 1]; for (int i = n - 2; i >= 0; i--) { rightMax[i] = Math.max(rightMax[i + 1], height[i]); } int ans = 0; for (int i = 0; i < n; i++) { ans += Math.min(leftMax[i], rightMax[i]) - height[i]; } return ans;

三条公式:

leftMax[i] = max(leftMax[i-1], height[i]) rightMax[i] = max(rightMax[i+1], height[i]) water[i] = min(leftMax[i], rightMax[i]) - height[i]

复杂度:

时间 O(n) 空间 O(n)

二十三、最终总结

这道题最开始容易被图形吓到,但真正拆开以后,每一个位置其实都是独立计算:

当前位置能接多少水?

答案就是:

左右两边最高墙中较矮的那个 - 当前柱子高度

也就是:

Math.min(leftMax[i], rightMax[i]) - height[i]

为了避免每个位置重复向左右扫描,通过:

前缀最大值 leftMax 后缀最大值 rightMax

提前保存边界信息。

最终思维链:

接雨水 ↓ 逐列计算 ↓ 需要左右最高墙 ↓ 前后缀最大值 ↓ O(n) 求解

当前这版代码已经足够作为一个稳定的面试解法。

后续如果继续优化,只需要进一步把:

leftMax[] rightMax[]

两个数组压缩成:

leftMax rightMax

两个变量,再配合双指针,就可以做到:

O(n) 时间 O(1) 空间

但无论怎么优化,整道题最核心的公式始终不变:

water[i] = min(leftMax[i], rightMax[i]) - height[i]
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/6 3:45:29

菲涅耳公式与半波损失:从符号到物理图像的深度解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 3:45:25

粒子群算法优化一次调频PID参数:从建模到工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 3:44:34

系统级思维+深度实测:猫王妙播AI智慧收音机SR2 MK2体验

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 3:43:18

perf 常用命令速查手册(建议收藏)

Linux 内核自带了一个性能分析工具叫 perf。它能做函数级和指令级的热点采样&#xff0c;也能配合 tracepoint 采集系统调用、网络事件、文件系统操作等内核事件。因为代码就在内核源码树里&#xff0c;算得上是 Linux 平台上最顺手的性能工具了。 原理 perf 基于内核的性能计…

作者头像 李华
网站建设 2026/9/6 3:40:57

直播录播系统搭建:OBS录制与FFmpeg批量处理实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 3:38:54

vibe coding实战:从能力边界到工具选型的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华