这是我参与2022首次更文挑战的第16天,活动详情查看:2022首次更文挑战
题目
42. 接雨水
解析
这道题也是不知道第多少次做了,还是没有思路,因此再做一次。
先思考一个问题:
- 如何判断某个位置是否可以蓄水?
条件就是:这个位置的左右都有严格大于这个位置高度的格子。
那么,再思考一个问题:
- 如何判断某个位置的蓄水量?
在满足上面的条件下,某个位置的蓄水量就是:这个数往左右看的最高的板子的最小值,减去这个位置板子的高度。
那么,只要求得对于位置i,往左右看的最高的板子的高度,就可以轻易求出题目所求的蓄水量。
转化一下,往左看就代表着从这个数开始往左的序列中的最大值,往右看亦然。
那么,将上面的设计转化为所给的即可得以下代码:
public int trap(int[] height){
int n = height.length;
//max[i][0]:左最大值,max[i][1]:右最大值
int[][] max = new int[n][2];
//需要给边界设定上值,第0个左边肯定是自己最大,第n-1个必然是的右边必然是自己最大
max[0][1] = max[0][0] = height[0];
max[n-1][0] = max[n-1][1] = height[n-1];
//我们依次求取向左边看的最高
for (int i = 1; i < height.length; i++) {
max[i][0] = Math.max(max[i-1][0],height[i]);
}
//再依次求取右边看的最高
for(int i = n-2;i>=0;i--){
max[i][1] = Math.max(max[i+1][1],height[i]);
}
//最后求值,某一个位置是否能蓄水,取决于左边看和右边看的最大值之间小的那个
int total = 0;
for (int i = 0; i < n; i++) {
total += Math.min(max[i][0],max[i][1]) - height[i];
}
return total;
}
执行用时:2 ms, 在所有 Java 提交中击败了39.17%的用户
内存消耗:40.9 MB, 在所有 Java 提交中击败了5.05%的用户
OK,现在题解AC了,思考一下有没有改进的空间?毕竟这个5.05%的空间复杂度,必然是有更优的解法的。
滚动数组
注意到:
- 在更新往左看的最大值的时候,后一个的值只由当前位置的值和前一个的值决定;
- 往右看的时候亦然。
考虑一下:如果我们用的是DP的话,那么这种情况就可以用滚动数组的方式来优化(毕竟只由前一个的值确定)。
那么,思考一下以什么方式滚动?
暂时想不出来如何抽象到使用指针的方式,那么看看是否可以优化到一维数组?
注意到:我们求结果的时候,是以左右最大值的最小值来确定的。
那么,可以优化到只用一个数组,只保存最小值即可。
流程假定还跟上面相同,从左到右,再从右到左。
那么,从右到左的时候,如果maxR<maxL,保存的是maxR;对于左边的数,maxL比maxR大,实际上对于最后的计算是没有意义的,因为只要最小值。
代码如下:
public int trap3(int[] height){
int n = height.length;
int[] dp = new int[n];
dp[0] = height[0];
for (int i = 1;i<n;i++)
dp[i] = Math.max(dp[i-1],height[i]);
dp[n-1] = height[n-1];
for(int i = n-2;i>=0;i--)
dp[i] = Math.min(Math.max(dp[i+1],height[i]),dp[i]);
int total = 0;
for (int i1 = 0; i1 < dp.length; i1++) {
total += dp[i1] - height[i1];
}
return total;
}
执行用时:1 ms, 在所有 Java 提交中击败了79.57%的用户
内存消耗:40.9 MB, 在所有 Java 提交中击败了5.05%的用户
吐槽一下这个统计,明明二维变一维了内存消耗还是一样的就很不科学。。。
在上面的版本的基础上,我们很容易地可以将第二次遍历求右最大值的过程,用一个指针来取代更新。(同时可以不用再遍历dp数组求和):
public int trap4(int[] height){
int n = height.length;
int[] dp = new int[n];
dp[0] = height[0];
for (int i = 1;i<n;i++)
dp[i] = Math.max(dp[i-1],height[i]);
int rMax = height[n-1];
int total = 0;
for(int i = n-1;i>=0;i--){
rMax = Math.max(rMax,height[i]);
total += Math.min(rMax,dp[i]) - height[i];
}
return total;
}
执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:41.2 MB, 在所有 Java 提交中击败了5.05%的用户
进一步地想,既然单指针可以有,那么双指针呢?
回顾一下上面为什么在从右遍历的时候,可以不用再更新dp表?
- 因为当计算右最大值的时候,左最大值已经确定了。
但其实,回顾一下trap2中,所做的工作:所求的是左右最大值的小的那一个。[1]
因此,如果使用双指针一次遍历,得知右边有比左边更高的板子(不一定要确切地知道是多少,只要知道有即可),那么左边就可以得知:当前位置的蓄水量就变成单相关了:只和左边的最大值有关。
举个抽象的例子:使用双指针,lp为左指针,rp为右指针,lMax为左到lp的最大值,rMax为rp到右的最大值,那么:
- 如果rMax<lMax,那么即使lp到rp之间有比rMax更大的值,rp位置的蓄水量也仅由rp决定(见[1]).
- lMax<rMax,依然,此时rp换成lp。
- lMax=rMax,此时lp到rp之间的蓄水量也同样的由lMax决定。
终止条件也简单,当lp==rp的时候,意味着已经遍历完了所有的板子,此时就剩一个,此时可以得到在该位置的左右最大值,取小的即可。
根据上述思路就可以拟出以下答案:
//彻底抛弃dp数组
public int trap5(int[] height){
int lp = 0 , rp = height.length-1;
int lMax = height[lp],rMax = height[rp];
int total = 0;
while(lp < rp){
if(lMax>rMax){
total += rMax - height[rp];
rp--;
rMax = Math.max(height[rp],rMax);
}else{
total += lMax - height[lp];
lp++;
lMax = Math.max(height[lp],lMax);
}
}
return total + Math.min(lMax,rMax) - height[lp];
}
执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:41.5 MB, 在所有 Java 提交中击败了5.05%的用户
PS:我是不是黑号啊这个内存消耗越来越大
嗯没事,加个gc看这个版本的空间使用情况的最终结果:
public int trap5(int[] height){
System.gc();
//...
}
执行用时:4 ms, 在所有 Java 提交中击败了14.80%的用户
内存消耗:37.8 MB, 在所有 Java 提交中击败了94.28%的用户
嗯,这样就对了。