题目描述

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

示例 1:
**接雨水问题 - 图1

  1. 输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
  2. 输出:6
  3. 解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。

示例 2:

  1. 输入:height = [4,2,0,3,2,5]
  2. 输出:9

提示:

  • n == height.length
  • 0 <= n <= 3 * 104
  • 0 <= height[i] <= 105

如图所示,对于某一个具体的位置接雨水问题 - 图2来说,给定的柱子宽度都是1,因此,该位置可能接的雨水量就只和高度相关。根据木桶盛水的短板原则,接雨水问题 - 图3位置能接多少水,还取决于它两边的情况。即左边的最大高度接雨水问题 - 图4和右边的最大高度接雨水问题 - 图5,并且最多接水量只和两者中最小的高度(接雨水问题 - 图6)相关。

  • 如果接雨水问题 - 图7,那么接雨水问题 - 图8位置接水量就是两者的差值
  • 如果接雨水问题 - 图9,那么接雨水问题 - 图10位置无法接水

如何理解呢?以题目中给的🌰为例:

  • 如果当前位置为接雨水问题 - 图11,那么情况如下所示:

image.png
左边最高为0,右边最高为3,当前位置为1。根据上述的原则,该位置无法接水

  • 如果当前位置为接雨水问题 - 图13,情况如下所示:

image.png
左边最高为1,右边最高为3,当前位置为1,那么该位置也无法接水

  • 如果当前位置为接雨水问题 - 图15,情况如下所示:

image.png
左边最高为2,右边最高为3,当前位置为1。满足上述能接水的原则,因此,该位置接水量为
接雨水问题 - 图17

所以,问题的关键就在于如何找到某一位置接雨水问题 - 图18的左最高接雨水问题 - 图19和右最高接雨水问题 - 图20

首先,我们来看最直观的方法,即到达一个具体的位置先找接雨水问题 - 图21接雨水问题 - 图22。假设当前位置为接雨水问题 - 图23,那么接雨水问题 - 图24只能在它的左半部分找且不包含自身,即:

  1. int max_left = 0;
  2. for(int j = i - 1; j >= 0; j--){
  3. if(height[j] > max_left){
  4. max_left = height[j];
  5. }
  6. }

对应的接雨水问题 - 图25只能在它的右半部分找且不含自身,即:

  1. int max_right = 0;
  2. for(int j = i + 1; j < height.length; j++){
  3. if(height[j] > max_right){
  4. max_right = height[j];
  5. }
  6. }

当找到了接雨水问题 - 图26接雨水问题 - 图27,按照前面的分析,自然就可以得到该位置的接水量。由于两端不可能接雨水,因此,讨论的范围为接雨水问题 - 图28。下面逐步的通过图示的方式理解下上述的流程。

  • 接雨水问题 - 图29:当前位置为1,接雨水问题 - 图30接雨水问题 - 图31,接水量为0

image.png

  • 接雨水问题 - 图33:当前位置为0,接雨水问题 - 图34接雨水问题 - 图35,接水量为1

image.png

  • 接雨水问题 - 图37:当前位置为12,接雨水问题 - 图38接雨水问题 - 图39,接水量为0

image.png

  • 接雨水问题 - 图41:当前位置为2,接雨水问题 - 图42接雨水问题 - 图43,接水量为0

image.png

  • 接雨水问题 - 图45:当前位置为1,接雨水问题 - 图46接雨水问题 - 图47,接水量为1

image.png

  • 接雨水问题 - 图49:当前位置为0,接雨水问题 - 图50接雨水问题 - 图51,接水量为2

image.png

  • 接雨水问题 - 图53:当前位置为1,接雨水问题 - 图54接雨水问题 - 图55,接水量为1

image.png

  • 接雨水问题 - 图57:当前位置为1,接雨水问题 - 图58接雨水问题 - 图59,接水量为0

image.png

  • 接雨水问题 - 图61:当前位置为2,接雨水问题 - 图62接雨水问题 - 图63,接水量为0

image.png
通过上述的逐步分析,最终的接水量为6。详细的解题代码如下所示:

  1. /**
  2. * @Author dyliang
  3. * @Date 2020/11/10 22:39
  4. * @Version 1.0
  5. */
  6. public class _42 {
  7. public static void main(String[] args) {
  8. int[] height = {0,1,0,2,1,0,1,3,2,1,2,1};
  9. System.out.println(trap4(height));
  10. }
  11. public static int trap2(int[] height){
  12. int sum = 0;
  13. for (int i = 1; i < height.length - 1; i++) {
  14. int max_left = 0;
  15. for(int j = i - 1; j >= 0; j--){
  16. if(height[j] > max_left){
  17. max_left = height[j];
  18. }
  19. }
  20. int max_right = 0;
  21. for(int j = i + 1; j < height.length; j++){
  22. if(height[j] > max_right){
  23. max_right = height[j];
  24. }
  25. }
  26. int min_one = Math.min(max_left,max_right);
  27. sum += Math.max(0, min_one - height[i]);
  28. }
  29. return sum;
  30. }
  31. }

仔细分析,这种解法需要每个位置都遍历一次给定的数组找接雨水问题 - 图65接雨水问题 - 图66,时间复杂度为接雨水问题 - 图67。由于过程中没有使用额外的存储空间,空间复杂度为接雨水问题 - 图68

如果详细的走过每一个位置可以发现,接雨水问题 - 图69接雨水问题 - 图70在很多位置上并没有发生改变,如下图黄框所示的位置,每一个框内所有位置的的接雨水问题 - 图71接雨水问题 - 图72都是相同的。如果我们提前将每个位置的接雨水问题 - 图73接雨水问题 - 图74找到并存储起来,那么求接水量的时间复杂度就会下降到接雨水问题 - 图75。虽然,此时空间复杂度变成了接雨水问题 - 图76,但是空间换时间是完全可接受的。
image.png
那么如何找每个位置的接雨水问题 - 图78接雨水问题 - 图79呢?假设从左往右找接雨水问题 - 图80,当前遍历到了接雨水问题 - 图81,那么它对应的接雨水问题 - 图82取决于接雨水问题 - 图83接雨水问题 - 图84位置对应的接雨水问题 - 图85,而且接雨水问题 - 图86的值是两者的最大值。

接雨水问题 - 图87数组的值取决于每个位置右半部分的情况,因此需要从右往左遍历,对应的原则是接雨水问题 - 图88

详细的解题代码如下:

  1. /**
  2. * @Author dyliang
  3. * @Date 2020/11/10 22:39
  4. * @Version 1.0
  5. */
  6. public class _42 {
  7. public static void main(String[] args) {
  8. int[] height = {0,1,0,2,1,0,1,3,2,1,2,1};
  9. System.out.println(trap4(height));
  10. }
  11. public static int trap3(int[] height){
  12. int sum = 0;
  13. int h = height.length;
  14. int[] max_left = new int[h];
  15. int[] max_right = new int[h];
  16. for (int i = 1; i < h - 1; i++) {
  17. max_left[i] = Math.max(max_left[i - 1], height[i - 1]);
  18. }
  19. for (int i = h - 2; i >= 0; i--) {
  20. max_right[i] = Math.max(max_right[i + 1], height[i + 1]);
  21. }
  22. for (int i = 1; i < h - 1; i++) {
  23. int min_one = Math.min(max_left[i], max_right[i]);
  24. sum += Math.max(0, min_one - height[i]);
  25. }
  26. return sum;
  27. }
  28. }

上面基于备忘录的方法的时间复杂度和空间复杂度都是接雨水问题 - 图89,由于总是至少要遍历一遍数组,因此,时间复杂度最低也只能是接雨水问题 - 图90,那么能否不用额外的存储空间完成呢?仔细分析上面的解法可以发现,对于接雨水问题 - 图91来说,它只关心它对应的接雨水问题 - 图92,而不关心曾经出现过的接雨水问题 - 图93。同样对于接雨水问题 - 图94来说,也只关心它对应的接雨水问题 - 图95,而不关心曾经的接雨水问题 - 图96。因此,我们可以使用两个变量来记录目前最大的接雨水问题 - 图97接雨水问题 - 图98。而两端位置的遍历,可以使用指针接雨水问题 - 图99接雨水问题 - 图100进行记录。

而且对于接雨水问题 - 图101来说,它对应的接雨水问题 - 图102是不会在变化的,而此时的接雨水问题 - 图103却不一定。因为,接雨水问题 - 图104接雨水问题 - 图105对应索引位置之间可能存在更大的值。同样,对于接雨水问题 - 图106来说,接雨水问题 - 图107是不会变化的,但是接雨水问题 - 图108却可能变化。但是,只要接雨水问题 - 图109成立,即时出现更大的接雨水问题 - 图110,根据接水的原则可知,它不会影响当前的结果。同理,只要接雨水问题 - 图111成立,即时出现更大的接雨水问题 - 图112,同样不会影响当前的结果。

详细的解题代码如下:

  1. /**
  2. * @Author dyliang
  3. * @Date 2020/11/10 22:39
  4. * @Version 1.0
  5. */
  6. public class _42 {
  7. public static void main(String[] args) {
  8. int[] height = {0,1,0,2,1,0,1,3,2,1,2,1};
  9. System.out.println(trap4(height));
  10. }
  11. public static int trap(int[] height) {
  12. int sum = 0;
  13. int left = 0, right = height.length - 1;
  14. int left_max = 0, right_max = 0;
  15. while(left <= right){
  16. // 处理left所指的位置
  17. if(left_max < right_max){
  18. // 记录接水量
  19. sum += Math.max(0, left_max - height[left]);
  20. // 更新可能的max_left
  21. left_max = Math.max(left_max, height[left]);
  22. left++;
  23. // 处理right所指的位置
  24. } else {
  25. // 记录接水量
  26. sum += Math.max(0, right_max - height[right]);
  27. // 更新可能的max_right
  28. right_max = Math.max(right_max, height[right]);
  29. right--;
  30. }
  31. }
  32. return sum;
  33. }
  34. }