当前位置:网站首页>LeetCode 接雨水系列 42.(一维) 407.(二维)
LeetCode 接雨水系列 42.(一维) 407.(二维)
2022-06-26 09:33:00 【抠脚的大灰狼】
42.接雨水
题目描述:
给定n个非负整数表示每个宽度为1的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水
思路:
依次看每根柱子能接多少雨水。
某根柱子能接的雨水,取决于该柱子左侧的最高柱子leftMax,和右侧的最高柱子rightMax。根据木桶效应,能接的水取决于左右两侧最高柱子的较小者,即h = min(leftMax,rightMax),若h比当前柱子大,则当前柱子能接到的雨水。那么一个比较直观的思路就是,我们先预处理出每个位置的左侧最大值和右侧最大值,然后再遍历一次,在每个柱子的位置接雨水即可。
class Solution {
public int trap(int[] height) {
int n = height.length;
int[] leftMax = new int[n];
int[] rightMax = new int[n];
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]);
}
int ans = 0;
for (int i = 0; i < n; i++) {
int h = Math.min(leftMax[i], rightMax[i]);
if (h > height[i]) ans += h - height[i];
}
return ans;
}
}
上面两次预处理,加一次遍历计算,总共花费的时间为3n,其实可以优化为2n,我们可以先预处理出rightMax,然后,再边维护leftMax,边接雨水。
class Solution {
public int trap(int[] height) {
int n = height.length;
int[] rightMax = new int[n];
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, leftMax = height[0];
for (int i = 1; i < n; i++) {
leftMax = Math.max(leftMax, height[i]);
int h = Math.min(leftMax, rightMax[i]);
if (h > height[i]) ans += h - height[i];
}
return ans;
}
}
实际上,我们还可以用双指针来进行优化,只需要遍历一次即可算出答案。
核心的思路是:用2个指针,i从左往右移动,j从右往左移动。我们能够在过程中正确维护i的左侧最大值,和j的右侧最大值,然后我们每次计算在i或者j处的接雨水的量。
我们用iLeftMax表示位置i处左侧的最高柱子高度,iRightMax表示i右侧最高的柱子高度。jLeftMax和jRightMax同理。
我们在双指针的遍历过程中,能够得到iLeftMax和jRightMax的值。
由于i在j的左侧,我们容易得知如下关系:iLeftMax <= jLeftMax。因为j左侧的最大值,要么是i左侧的最大值,要么是i和j之间出现了比iLeftMax更大的值。
同样,容易得到:iRightMax >= jRightMax,因为i比j要多考虑区间[i,j],iRightMax只可能比jRightMax更大。
整理一下,我们有:
iLeftMax <= jLeftMax
iRightMax >= jRightMax
并且我们能获取到iLeftMax和jRightMax的值。
当iLeftMax <= jRightMax时,根据上面的关系,有iLeftMax <= jRightMax <= iRightMax,进而有iLeftMax <= iRightMax,此时位置i能接的雨水,只取决于iLeftMax,而iLeftMax我们是已知的,所以此时可以计算i处的雨水。
当iLeftMax > jRightMax时,根据上面的关系,有jLeftMax >= iLeftMax > jRightMax,进而有jLeftMax > jRightMax,此时位置j能接的雨水,只取决于jRightMax,而jRightMax我们是已知的,所以此时可以计算j处的雨水。
双指针优化后,只需要一趟遍历即可完成计算。
class Solution {
public int trap(int[] height) {
int n = height.length;
int leftMax = height[0], rightMax = height[n - 1];
int i = 1, j = n - 2;
// iLeftMax <= jLeftMax
// iRightMax >= jRightMax
int ans = 0;
while (i <= j) {
if (leftMax <= rightMax) {
// iLeftMax <= jRightMax <= iRightMax
leftMax = Math.max(leftMax, height[i]);
ans += leftMax - height[i];
i++;
} else {
// jLeftMax >= iLeftMax > jRightMax
rightMax = Math.max(rightMax, height[j]);
ans += rightMax - height[j];
j--;
}
}
return ans;
}
}
其余解法:单调栈。
换个角度考虑,什么时候某个位置可能接到雨水呢,是不是只有柱子高度先下降,后上升,形成一个 “盆地”时(一个V字形),在中间的低洼处才能接到水呢?于是我们考虑用单调栈。栈里面存的是高度递减的柱子,从左往右遍历的时候,只要遇到一个比栈顶的柱子高度大的,则说明能和前面的柱子形成一个低洼处,能够接到雨水。
class Solution {
public int trap(int[] height) {
int n = height.length;
int[] stack = new int[n];
int top = -1; // 栈顶
int ans = 0;
for (int i = 0; i < n; i++) {
// 栈非空, 且栈顶的柱子高度小于当前柱子, 则开始接雨水
while (top >= 0 && height[stack[top]] < height[i]) {
int j = stack[top--]; // 准备接j这个位置的雨水
if (top < 0) break; // 没有前一个柱子了, 无法接j处的雨水
int k = stack[top]; // k和i围住了j, 可以接j处雨水, 注意不要把k也弹出栈
int minH = Math.min(height[k], height[i]);
ans += (minH - height[j]) * (i - k - 1);
}
// 该柱子能和前面的柱子接的雨水, 计算完毕, 插入该柱子
stack[++top] = i;
}
return ans;
}
}
407.接雨水II
题目描述:
给你一个 m x n 的矩阵,其中的值均为非负整数,代表二维高度图每个单元的高度,请计算图中形状最多能接多少体积的雨水。
上一道题是一维的,这道题是二维的。
思路:
第一次见这道题时,不太容易想清楚 [满足什么条件才能接到雨水] 这一点。比较直觉但是错误的想法是,对于某个位置[i,j],找到其上下左右4个方向上,高度最大的4根柱子,取4根柱子中高度最小的,再和当前这跟柱子比较高度。
这样是错误的,水还可能从其他边边角角的地方流走。一种相对正确但是不太好想象的想法是:对于[i,j],找到其四周围成一圈的一堵墙,这堵墙中,高度最小的柱子,再和[i,j]的柱子比高度。但这种想法很难付诸实践。
我们换个角度来看,当下完雨后,每个柱子有个最终高度,若这个最终高度大于该柱子原有高度,则说明在这个柱子的位置,接到了雨水。我们用f(i,j)来表示在位置[i,j]处的柱子的最终高度。引入这个最终高度的概念后,就很好想了。
f(i,j),其实就取决于其周围4个相邻位置的最终高度。容易得到状态转移方程如下:
f(i,j) = max{ min{f(i-1, j), f(i + 1, j), f(i, j - 1), f(i, j + 1)}, height[i][j] }
即,位置[i,j]的柱子的最终高度,为其4个相邻位置的最终高度的最小值,再和[i,j]柱子原本的高度取一个max。
然后是状态的边界。容易得知,整个矩形最外层的柱子,都是接不到水的,水一定会从边界流出去。所以对所有最外层的柱子,初始化其f(i,j) = height[i][j]。我们每次,从已经求得f(i,j)的柱子中,找到f(i,j)值最小的,并更新其相邻的,还未求出f(i,j)的柱子。
为什么这样能求出正确答案。首先,根据木桶效应,f(i,j)只取决于其相邻的4个位置的最小者。所以我们每次从已经确定f(i,j)的柱子中,找出值最小的。假设我们选中的f(i,j)最小的柱子是a。那么, 与柱子a相邻的柱子,其f(i,j)一定是由柱子a转移过来的吗?
答案是肯定的,假设与柱子a相邻的某个柱子b,其还有其他3个相邻的柱子,若这3个相邻的柱子的最终高度已经求出,而我们选择的最小值为柱子a,说明这3个相邻柱子的最终高度都大于柱子a的。若这3个相邻的柱子里,有些柱子的最终高度还没有求出,则这些还没有求出最终高度的柱子,其最终高度,都是由已经求出最终高度的某根柱子转移过来的,而状态转移的过程中,f(i,j)只可能增大,不可能变小,所以这些没有求出最终高度的柱子的最终高度,一定是大于其状态转移链的初始那个状态,而这个初始状态一定是此时f(i,j)已知的,而此时已知的f(i,j)中,柱子a是最小的。也能够推出,其他3个相邻柱子的最终高度都一定大于a。所以柱子b的最终高度,一定是由柱子a的状态更新过来的。
再换个角度想,我们将最外层的柱子的状态f(i,j)进行了初始化,相当于在最外圈,围起了一堵墙(边界),这堵墙中间的某根柱子,是否能接到水,一定取决于最外圈这堵墙中(边界上),高度最小的那根柱子。所以我们每次需要取高度最小的。
于是,我们的算法就出来了。其实和Dijkstra特别像。每次从f(i,j)已知的柱子中,选择一个最小的,更新其相邻的柱子。我们用小根堆来做。
class Solution {
class Node {
int x;
int y;
int h; // 下雨之后该柱子的最终高度
Node(int x, int y, int h) {
this.x = x;
this.y = y;
this.h = h;
}
}
public int trapRainWater(int[][] heightMap) {
int n = heightMap.length, m = heightMap[0].length;
boolean[][] st = new boolean[n][m]; // 用来标记那些已经算出最终高度的柱子
// 按最终高度从小到大
PriorityQueue<Node> heap = new PriorityQueue<>((o1, o2) -> o1.h - o2.h);
// 将矩阵最外层的柱子插进堆
for (int i = 0; i < m; i++) {
heap.offer(new Node(0, i, heightMap[0][i])); // 第一行
heap.offer(new Node(n - 1, i, heightMap[n - 1][i])); // 最后一行
st[0][i] = st[n - 1][i] = true;
}
// 重复元素不影响最终答案
for (int i = 0; i < n; i++) {
heap.offer(new Node(i, 0, heightMap[i][0])); // 第一列
heap.offer(new Node(i, m - 1, heightMap[i][m - 1])); // 最后一列
st[i][0] = st[i][m - 1] = true;
}
int ans = 0;
int[] dx = {
1, -1, 0, 0};
int[] dy = {
0, 0, 1, -1};
while (!heap.isEmpty()) {
Node node = heap.poll();
for (int i = 0; i < 4; i++) {
int nx = node.x + dx[i];
int ny = node.y + dy[i];
if (nx < 0 || nx >= n || ny < 0 || ny >= m || st[nx][ny]) continue; // 越界或已求出最终高度
ans += Math.max(node.h, heightMap[nx][ny]) - heightMap[nx][ny]; // 接雨水
heap.offer(new Node(nx, ny, Math.max(node.h, heightMap[nx][ny])));
st[nx][ny] = true; // 该柱子已求出最终高度
}
}
return ans;
}
}
上面的思路是我自己看了yxc的视频讲解后,总结的一种比较简单,方便记忆和理解的思路。
yxc的原思路,参考这个链接。其证明和推导过程十分严谨,但不是特别容易理解。
边栏推荐
- Understanding of swing transformer
- Construction practice of bank intelligent analysis and decision-making platform
- halcon 光度立体
- 【CVPR 2021】Unsupervised Multi-Source Domain Adaptation for Person Re-Identification (UMSDA)
- jz2440---使用uboot燒錄程序
- 《一周搞定模电》—电源电路
- 同花顺炒股软件安全性怎样?在同花顺怎么开户
- jz2440---使用uboot烧录程序
- How does flutter transfer parameters to the next page when switching pages?
- 工企专利匹配数据(数十万数据量)1998-2014年
猜你喜欢

0 basic how to make a cool leadership cockpit?

php提取txt文本存储json数据中的域名

jz2440---使用uboot燒錄程序

Learning to Generalize Unseen Domains via Memory-based Multi-Source Meta-Learning for Person Re-ID

Behavior tree file description

"One week to solve the model electricity" - negative feedback

【CVPR 2021】Unsupervised Multi-Source Domain Adaptation for Person Re-Identification (UMSDA)

Shared by Merrill Lynch data technology expert team, smoking detection related practice based on Jetson nano

"One week's work on Analog Electronics" - integrated operational amplifier

MapReduce&Yarn理论
随机推荐
thinkphp6.0的第三方扩展包,支持上传阿里云,七牛云
Detectron2 outputs validation loss during training
工企专利匹配数据(数十万数据量)1998-2014年
Upgrade idea to 2021.2 shortcut keys
《一周搞定模电》-二极管
Common circuit design
《一周搞定模电》—基本放大电路
The shutter tabbar listener is called twice
3大问题!Redis缓存异常及处理方案总结
"One week's work on Analog Electronics" - power amplifier
jz2440---使用uboot烧录程序
0 basic how to make a cool leadership cockpit?
"One week's work on Analog Electronics" - optocoupler and other components
SQL query duplicate record
Shared by Merrill Lynch data technology expert team, smoking detection related practice based on Jetson nano
Origin of QPM
Construction practice of bank intelligent analysis and decision-making platform
Curriculum learning (CL)
QPM suspended window setting information
Creation and use of XSync synchronization script (taking debian10 cluster as an example)