题目描述

image.png

题目链接

https://leetcode.cn/problems/trapping-rain-water/

思路

1. 动态规划

由题目可知:对于下标【42】接雨水 - 图2,下雨后水能到达的最大高度等于下标【42】接雨水 - 图3两边的最大高度的最小值,下标【42】接雨水 - 图4处能接的雨水量等于下标【42】接雨水 - 图5处的水能到达的最大高度减去【42】接雨水 - 图6

朴素的做法是对于数组【42】接雨水 - 图7中的每个元素,分别向左和向右扫描并记录左边和右边的最大高度,然后计算每个下标位置能接的雨水量。假设数组【42】接雨水 - 图8的长度为【42】接雨水 - 图9,该做法需要对每个下标位置使用【42】接雨水 - 图10的时间向两边扫描并得到最大高度,因此总时间复杂度是【42】接雨水 - 图11

上述做法的时间复杂度较高是因为需要对每个下标位置都向两边扫描。如果已经知道每个位置两边的最大高度,则可以在【42】接雨水 - 图12的时间内得到能接的雨水总量。使用动态规划的方法,我们可以在【42】接雨水 - 图13的时间内预处理得到每个位置两边的最大高度。

创建两个长度为【42】接雨水 - 图14的数组【42】接雨水 - 图15【42】接雨水 - 图16。对于【42】接雨水 - 图17【42】接雨水 - 图18表示下标【42】接雨水 - 图19及其左边的位置中【42】接雨水 - 图20的最大高度,【42】接雨水 - 图21表示下标【42】接雨水 - 图22及其右边的位置中【42】接雨水 - 图23的最大高度。显然,初始条件为:【42】接雨水 - 图24【42】接雨水 - 图25。数组中其余元素的计算如下:

  • 【42】接雨水 - 图26时,【42】接雨水 - 图27
  • 【42】接雨水 - 图28时,【42】接雨水 - 图29

因此可以正向遍历数组【42】接雨水 - 图30得到数组【42】接雨水 - 图31的每个元素值,反向遍历数组【42】接雨水 - 图32得到数组【42】接雨水 - 图33的每个元素值。在得到数组【42】接雨水 - 图34【42】接雨水 - 图35的每个元素值之后,对于【42】接雨水 - 图36,下标【42】接雨水 - 图37处能接的雨水量等于【42】接雨水 - 图38。遍历每个下标位置即可得到能接的雨水总量。

动态规划做法可以由下图体现:
image.png
具体代码实现如下:

  1. public int trap(int[] height) {
  2. if (height == null || height.length == 0) {
  3. return 0;
  4. }
  5. // 预处理leftMax
  6. int n = height.length;
  7. int[] leftMax = new int[n];
  8. leftMax[0] = height[0];
  9. for (int i = 1; i < n; i++) {
  10. leftMax[i] = Math.max(leftMax[i - 1], height[i]);
  11. }
  12. // 预处理rightMax
  13. int[] rightMax = new int[n];
  14. rightMax[n - 1] = height[n - 1];
  15. for (int i = n - 2; i >= 0; i--) {
  16. rightMax[i] = Math.max(rightMax[i + 1], height[i]);
  17. }
  18. // 计算雨水总量
  19. int ans = 0;
  20. for (int i = 0; i < n; i++) {
  21. ans += Math.min(leftMax[i], rightMax[i]) - height[i];
  22. }
  23. return ans;
  24. }
  • 时间复杂度:【42】接雨水 - 图40,其中【42】接雨水 - 图41是数组【42】接雨水 - 图42的长度。计算数组【42】接雨水 - 图43【42】接雨水 - 图44的元素值各需要遍历数组【42】接雨水 - 图45一次,计算能接的雨水总量还需要遍历一次。
  • 空间复杂度:【42】接雨水 - 图46,其中【42】接雨水 - 图47是数组【42】接雨水 - 图48的长度。需要创建两个长度为【42】接雨水 - 图49的数组【42】接雨水 - 图50【42】接雨水 - 图51

    2. 单调栈

    除了方法一中计算并存储每个位置两边的最大高度以外,也可以用「单调栈」计算能接的雨水总量。我们维护一个单调栈,单调栈存储的是下标,满足从栈底到栈顶的下标对应的数组【42】接雨水 - 图52中的元素递减,即栈顶元素对应的数组【42】接雨水 - 图53中的值是当前栈中最小的。

从左到右遍历数组,遍历到下标【42】接雨水 - 图54时,如果栈内至少有两个元素,记栈顶元素为【42】接雨水 - 图55【42】接雨水 - 图56的下面一个元素是【42】接雨水 - 图57,则一定有【42】接雨水 - 图58。如果【42】接雨水 - 图59,则得到一个可以接雨水的区域,该区域的宽度是【42】接雨水 - 图60,高度是【42】接雨水 - 图61,根据宽度和高度即可计算得到该区域能接的雨水量。
image.png
为了得到【42】接雨水 - 图63,需要将【42】接雨水 - 图64出栈。在对【42】接雨水 - 图65计算能接的雨水量之后,【42】接雨水 - 图66变成新的【42】接雨水 - 图67,重复上述操作,直到栈变为空,或者栈顶下标对应的【42】接雨水 - 图68中的元素大于或等于【42】接雨水 - 图69。在对下标【42】接雨水 - 图70处计算能接的雨水量之后,将【42】接雨水 - 图71入栈,继续遍历后面的下标,计算能接的雨水量。遍历结束之后即可得到能接的雨水总量。
image.png
下面用一个例子【42】接雨水 - 图73来帮助理解单调栈的做法。 具体代码实现如下:

  1. public int trap(int[] height) {
  2. if (height == null || height.length == 0) {
  3. return 0;
  4. }
  5. int ans = 0;
  6. Stack<Integer> stack = new Stack<>();
  7. for (int i = 0; i < height.length; i++) {
  8. // 保持栈顶到栈底对应的height数组中的元素是递减的(单调栈)
  9. while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
  10. int top = stack.pop();
  11. if (stack.isEmpty()) {
  12. break;
  13. }
  14. // 计算雨水面积
  15. int left = stack.peek();
  16. int curWidth = i - left - 1;
  17. int curHeight = Math.min(height[left], height[i]) - height[top];
  18. ans += curWidth * curHeight;
  19. }
  20. stack.push(i);
  21. }
  22. return ans;
  23. }
  • 时间复杂度:【42】接雨水 - 图74,其中【42】接雨水 - 图75是数组【42】接雨水 - 图76的长度。从【42】接雨水 - 图77【42】接雨水 - 图78的每个下标最多只会入栈和出栈各一次。
  • 空间复杂度:【42】接雨水 - 图79,其中【42】接雨水 - 图80是数组【42】接雨水 - 图81的长度。空间复杂度主要取决于栈,栈的大小不会超过【42】接雨水 - 图82

    3. 双指针

    动态规划的做法中,需要维护两个数组【42】接雨水 - 图83【42】接雨水 - 图84,因此空间复杂度是【42】接雨水 - 图85。是否可以将空间复杂度降到【42】接雨水 - 图86呢?答案是可以的,注意到下标【42】接雨水 - 图87处能接的雨水量由【42】接雨水 - 图88【42】接雨水 - 图89中的最小值决定。由于数组【42】接雨水 - 图90是从左往右计算,数组【42】接雨水 - 图91是从右往左计算的,因此可以使用双指针和两个变量代替两个数组。

维护两个指针【42】接雨水 - 图92【42】接雨水 - 图93以及两个变量【42】接雨水 - 图94【42】接雨水 - 图95,初始时【42】接雨水 - 图96【42】接雨水 - 图97【42】接雨水 - 图98【42】接雨水 - 图99。指针【42】接雨水 - 图100只会向右移动,指针【42】接雨水 - 图101只会向左移动,在移动指针的过程中动态维护两个变量【42】接雨水 - 图102【42】接雨水 - 图103的值。

当两个指针没有相遇时,进行如下操作:

  • 使用【42】接雨水 - 图104【42】接雨水 - 图105的值更新【42】接雨水 - 图106【42】接雨水 - 图107的值;

  • 如果【42】接雨水 - 图108,则必有【42】接雨水 - 图109,下标【42】接雨水 - 图110处能接的雨水量等于【42】接雨水 - 图111,将下标【42】接雨水 - 图112处能接的雨水量加到能接的雨水总量,然后将【42】接雨水 - 图113右移一位;

  • 如果【42】接雨水 - 图114,则必有【42】接雨水 - 图115,下标【42】接雨水 - 图116处能接的雨水量等于【42】接雨水 - 图117,将下标【42】接雨水 - 图118处能接的雨水量加到能接的雨水总量,然后将【42】接雨水 - 图119减去【42】接雨水 - 图120,即左移一位。

当两个指针相遇时,即可得到能接的雨水总量。下面用一个例子【42】接雨水 - 图121来帮助理解双指针的做法。 具体代码实现如下:

  1. public int trap(int[] height) {
  2. int ans = 0;
  3. int left = 0, right = height.length - 1;
  4. int leftMax = 0, rightMax = 0;
  5. // 按照列来求面积
  6. while (left < right) {
  7. // 动态维护leftMax、rightMax的值
  8. leftMax = Math.max(leftMax, height[left]);
  9. rightMax = Math.max(rightMax, height[right]);
  10. // 雨水容量取决于 max 值减当前值
  11. if (height[left] < height[right]) {
  12. ans += leftMax - height[left];
  13. // 右移
  14. left++;
  15. } else {
  16. ans += rightMax - height[right];
  17. // 左移
  18. right--;
  19. }
  20. }
  21. return ans;
  22. }
  • 时间复杂度:【42】接雨水 - 图122,其中【42】接雨水 - 图123是数组【42】接雨水 - 图124的长度。两个指针的移动总次数不超过【42】接雨水 - 图125
  • 空间复杂度:【42】接雨水 - 图126,只需要使用常数的额外空间。