从装水瓶到接雨水:一道经典动态规划问题的深度解析|LeetCode 42 接雨水
LeetCode 42 接雨水
点此看全部题解 LeetCode必刷100题:一份来自面试官的算法地图(题解持续更新中)
生活中的算法
你有没有注意过,很多户外运动的水壶都有不规则的凹凸形状?这些凹凸不仅便于握持,而且在横放时会形成天然的积水区域。如果下雨,雨水就会积聚在这些凹槽中。
这就是我们今天要讲的"接雨水"问题的生活映射。在算法中,我们要计算在一排高低不平的柱子之间,能够积攒多少雨水。每个凹陷的地方都可能积水,但具体能积多少,要看它左右两边最高柱子的情况。
问题描述
LeetCode第42题"接雨水"是这样描述的:给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
例如,输入 height = [0,1,0,2,1,0,1,3,2,1,2,1],能接的雨水总量为6个单位。
最直观的解法:按列求解法
最容易想到的方法是:对于每一个位置,看看它能积多少水。要知道一个位置能积多少水,我们需要知道这个位置左右两边最高的柱子。这个位置能积的水就是两边最高柱子中较矮的那个减去当前位置的高度。
具体步骤是这样的:
- 遍历每个位置
- 找出这个位置左边最高的柱子
- 找出这个位置右边最高的柱子
- 用两边最高柱子中较矮的减去当前位置的高度
- 如果结果大于0,就累加到总量中
让我们用一个小例子来模拟这个过程:
height = [3,1,2,4]
位置1(高度1):
- 左边最高3
- 右边最高4
- min(3,4) - 1 = 2,可以积2单位水
位置2(高度2):
- 左边最高3
- 右边最高4
- min(3,4) - 2 = 1,可以积1单位水
总共可以积3单位水
这种思路可以用Java代码这样实现:
public int trap(int[] height) {
int totalWater = 0;
// 遍历每个位置(除去两端)
for (int i = 1; i < height.length - 1; 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]);
}
// 计算当前位置能积的水
totalWater += Math.min(leftMax, rightMax) - height[i];
}
return totalWater;
}
优化解法:动态规划法
细想一下就会发现,我们在计算每个位置时,都要重复查找左右最高柱子。其实我们可以提前计算好每个位置的左右最大高度,这样就能避免重复计算。
这就是动态规划的思想:通过空间换时间,将计算结果保存下来重复使用。
动态规划法的原理
- 创建两个数组,分别记录每个位置左边和右边的最大高度
- 第一次遍历,从左向右计算每个位置左边的最大高度
- 第二次遍历,从右向左计算每个位置右边的最大高度
- 第三次遍历,根据左右最大高度计算每个位置能积的水量
算法步骤(伪代码)
- 初始化leftMax和rightMax数组,长度等于height数组长度
- 从左向右遍历:
- 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]
示例运行
让我们用height = [3,1,2,4]模拟这个过程:
第一次遍历(计算leftMax):
leftMax[0] = 3
leftMax[1] = max(3,1) = 3
leftMax[2] = max(3,2) = 3
leftMax[3] = max(3,4) = 4
第二次遍历(计算rightMax):
rightMax[3] = 4
rightMax[2] = max(4,2) = 4
rightMax[1] = max(4,1) = 4
rightMax[0] = max(4,3) = 4
第三次遍历(计算积水):
位置1:min(3,4) - 1 = 2
位置2:min(3,4) - 2 = 1
总积水 = 3
Java代码实现
public int trap(int[] height) {
if (height == null || height.length <= 2) return 0;
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 totalWater = 0;
for (int i = 1; i < n-1; i++) {
totalWater += Math.min(leftMax[i], rightMax[i]) - height[i];
}
return totalWater;
}
进一步优化:双指针法
我们还可以做得更好。注意到一个位置能积的水,取决于左右两边最高柱子中较矮的那个。利用这个特性,我们可以使用双指针来进一步优化空间复杂度。
双指针法的核心思想
- 使用左右两个指针从两端向中间移动
- 同时维护左右两边见过的最大高度
- 较小的那一边可以确定积水量,然后向中间移动
- 这样就不需要额外的数组来存储左右最大高度
Java代码实现
public int trap(int[] height) {
int left = 0, right = height.length - 1;
int leftMax = 0, rightMax = 0;
int totalWater = 0;
while (left < right) {
// 更新左右最大高度
leftMax = Math.max(leftMax, height[left]);
rightMax = Math.max(rightMax, height[right]);
// 选择较小的一边计算积水
if (leftMax < rightMax) {
totalWater += leftMax - height[left];
left++;
} else {
totalWater += rightMax - height[right];
right--;
}
}
return totalWater;
}
解法比较
让我们比较这三种解法:
按列求解法:
- 时间复杂度:O(n²)
- 空间复杂度:O(1)
- 优点:直观易懂
- 缺点:效率低,有大量重复计算
动态规划法:
- 时间复杂度:O(n)
- 空间复杂度:O(n)
- 优点:避免重复计算,思路清晰
- 缺点:需要额外空间
双指针法:
- 时间复杂度:O(n)
- 空间复杂度:O(1)
- 优点:时间和空间都达到最优
- 缺点:理解起来较难
题目模式总结
这道题体现了几个重要的算法思想:
- 空间换时间:通过预处理或记忆化来避免重复计算
- 双指针技巧:使用双指针来优化空间复杂度
- 动态规划:将问题分解为子问题并存储中间结果
这种解题模式在很多问题中都有应用,比如:
- 容器盛水问题
- 柱状图中最大的矩形
- 股票买卖的最佳时机
解决这类问题的通用思路是:
- 先想最直观的解法
- 观察是否存在重复计算
- 考虑是否可以通过预处理优化
- 进一步思考是否可以优化空间复杂度
小结
通过这道题,我们不仅学会了如何计算接雨水的容量,更重要的是理解了如何一步步优化算法的思维过程。从最直观的解法开始,通过观察特点,利用空间换时间,最后找到时间空间都最优的解法。
记住,算法优化往往是一个渐进的过程。当你遇到一个复杂问题时,先写出最基础的解法,然后再一步步优化。有时候,最优解往往就藏在最基础解法的深入思考中!
作者:忍者算法
公众号:忍者算法
从装水瓶到接雨水:一道经典动态规划问题的深度解析|LeetCode 42 接雨水的更多相关文章
- Java实现 LeetCode 42 接雨水
42. 接雨水 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水. 上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这 ...
- Leetcode 42.接雨水
接雨水 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水. 上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下 ...
- leetcode 42. 接雨水 JAVA
题目: 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水. 上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下 ...
- Leetcode 42 接雨水 双指针
地址 https://leetcode-cn.com/problems/trapping-rain-water/ 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能 ...
- LeetCode 42. 接雨水(Trapping Rain Water)
题目描述 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水. 上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况 ...
- LeetCode(42.接雨水)多解法详解
接雨水解法详解: 题目: 基本思路:从图上可以看出要想接住雨水,必须是凹字形的,也就是当前位置的左右两边必须存在高度大于它的地方,所以我们要想知道当前位置最多能存储多少水,只需找到左边最高处max_l ...
- [LeetCode]42. 接雨水(双指针,DP)
题目 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水. 上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下, ...
- LeetCode 42接雨水 按行求解(差分+排序)
按行求解的思路比较清晰明了,但是这个方法的复杂度高达O(heightSize*sum(height[i])),几乎高达O(N^2). 但是也并不是不可以解决,经观察我们可以发现,这个算法的缺点在于要遍 ...
- 每日一题 LeetCode 42.接雨水 【双指针】
题目链接 https://leetcode-cn.com/problems/trapping-rain-water/ 题目说明 题解 主要方法:双指针 + 正反遍历 解释说明: 正向遍历:先确定池子左 ...
- 解析js中作用域、闭包——从一道经典的面试题开始
如何理解js中的作用域,闭包,私有变量,this对象概念呢? 就从一道经典的面试题开始吧! 题目:创建10个<a>标签,点击时候弹出相应的序号 先思考一下,再打开看看 //先思考一下你会怎 ...
随机推荐
- QT5.15.2 连接MySQL 驱动问题解决方案,无论菜鸟🐦️还是老鸟🦜,解决了就是好鸟🦚
最新在学QT,现在QT只能在线安装了,用了几天,看到数据库时,需要用MySQL,结果出现了问题. QSqlDatabase: QMYSQL driver not loaded. QSqlDatabas ...
- typeScript 基础类型 (三)
typeScript 的基础类型包含 Boolean.Number.String.null.undefined 以及 ES6 的 Symbol 和 ES10 的 BigInt. 下面介绍每种类型的使 ...
- Spring AOP进行日志记录,管理 (使用Spring的拦截器功能获取对action中每个方法的调用情况,在方法调用前和调用后记录相关日志。)
在java开发中日志的管理有很多种.我一般会使用过滤器,或者是Spring的拦截器进行日志的处理.如果是用过滤器比较简单,只要对所有的.do提交进行拦截,然后获取action的提交路径就可以获取对每个 ...
- 你应该了解的hooks式接口编程 - useSWR
什么是 useSWR ? 听名字我们都知道是一个 React 的 hooks,SWR 是stale-while-revalidate的缩写, stale 的意思是陈旧的, revalidate 的意思 ...
- 3.QMainWindow
QMainWindow介绍 QMainWindow是一个为用户提供主窗口程序的类,包含一个菜单栏(menu bar),多个工具栏(tool bars),多个铆接部件(dock widgets),一个状 ...
- rabbitmq3.7.3 发布了一个新的 exchange x-random
direct exchange 同一个 routing key 可以绑定多个 queue,当给这个routing key发消息时,所有 queue 都会投递.这个行为对于一些场景不适用,有时我们希望只 ...
- Shiro-BasicHttpAuthenticationFilter 鉴权过滤器的使用方式
它的作用是用来根据路径匹配结果,调用相应过滤器 onPreHandle 这里是正在的执行逻辑,之前的都是判断,它返回了两个方法: isAccessAllowed() onAccessDenied() ...
- MockQueryable:LINQ查询模拟测试工具
我们在项目开发过程中,单元测试是确保代码质量的重要环节.涉及数据库LINQ查询逻辑,就需要数据库配合,但这样可能出现性能和数据库并发等问题.下面给推荐一个开源库,为开发者提供了一种简便的方式来模拟LI ...
- 龙哥量化:通达信(KT交易师)分时图主图叠加5分钟K线图
股友要求在通达信分时图主图叠加5分钟K线,但是通达信的分时图取不到最高价,最低价等数据,限制非常多. 然后我在KT交易师上写的公式测试成功了,效果如下. 如果您需要公式源码或者代写公式, 请联系我. ...
- Unity 3D简单使用C#脚本,脚本的执行顺序
Unity3D脚本间执行顺序 Unity3D中一个场景有时候需要多个脚本,可以挂在同一物体上执行,也可以挂在不同物体上执行 那么执行顺序是怎样的?我们来测试下 在上个项目基础上,再建一个Test2脚本 ...