接雨水问题
问题描述:给定一个整形数组arr,已知其中所有的值都是非负的,将这个数组看作一个柱子高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例:
输入:[3,1,2,5,2,4]
返回值:5
说明:数组 [3,1,2,5,2,4] 表示柱子高度图,在这种情况下,可以接 5个单位的雨水,蓝色的为雨水 ,使用图形解释如下:
方法一
思路分析
相信每个人都知道木桶原理,就是说一只木桶能盛多少水并不取决于桶壁上最长的那块木板,而恰恰取决于最短的那块。通过类比发现,每一根柱子所能承受的雨水=其左侧最高柱子与右侧最高柱子中的较小值-其自身柱子高度。因此最直接的办法是通过循环遍历数组arr,例如arr[i],找到数组中arr[i]左侧最大值与右侧最大值中的较小值m,则第i个柱子所能承受的雨水=m-arr[i],最后将所有的结果相加,然后这种方法由于存在双层循环,时间复杂度大大提升。在每一次寻找arr[i]的左侧最大值与右侧最大值,可能会存在重复寻找的问题,因此我们可以想到定义二维数组将arr每一个变量左右两侧的最大值保存,从而减少计算。步骤如下:
• 定义二维数组$dp[n][2]$,其中$dp[i][0]$用于保存arr[i]左侧柱子的最大值,$dp[i][1]$用于保存arr[i]右侧柱子的最大值。
• 寻找arr[i]左侧柱子的最大值可以直接使用arr[i-1]左侧柱子的最大值,其值已经保存在$dp[i-1][0]$中,从而减少循环计算
• 寻找arr[i]右侧柱子的最大值可以直接使用arr[i+1]右侧柱子的最大值,其值已经保存在$dp[i+1][0]$中,减少循环计算
• 每一个柱子的雨水量=其左侧最高柱子与右侧最高柱子中的较小值-其自身柱子高度
实例分析
给定数组arr[6]={3,1,2,5,2,4}
arr[i] | 3 | 1 | 2 | 5 | 2 | 4 |
左侧最大值$dp[i][0]$ | 3 | 3 | 3 | 5 | 5 | 5 |
右侧最大值$dp[i][1]$ | 5 | 5 | 5 | 5 | 4 | 4 |
雨水为$min(dp[i][0],dp[i][1])-arr[i]$ | 0 | 2 | 1 | 0 | 2 | 0 |
总的雨水 | 5 |
C++核心代码
class Solution { public: /** * max water * @param arr int整型vector the array * @return long长整型 */ long long maxWater(vector<int>& arr) { // write code here long n = arr.size(); if (n == 0) { return 0; } long res = 0; vector<vector<int> > dp(n); //定义二维数组,其中dp[i][0]用于保存arr[i]左侧柱子的最大值,dp[i][1]用于保存arr[i]右侧柱子的最大值。 for(int i=0;i<n;i++) { dp[i].resize(2); } dp[0][0] = arr[0];//左侧第一个柱子的左侧柱子最大值为本身 dp[n-1][1] = arr[n-1];//右侧第一个柱子的右侧柱子最大值为本身 for (int i = 1; i < n; i++) { dp[i][0] = max(dp[i - 1][0], arr[i]);//寻找arr[i]左侧柱子的最大值可以直接使用arr[i-1]左侧柱子的最大值,其值已经保存在dp[i-1][0]中 } for (int i = n - 2; i >= 0; i--) { dp[i][1] = max(dp[i + 1][1], arr[i]);//寻找arr[i]右侧柱子的最大值可以直接使用arr[i+1]右侧柱子的最大值,其值已经保存在dp[i+1][0]中 } for (int i = 0; i < n; i++) { res += min(dp[i][0], dp[i][1]) - arr[i];//每一个柱子的雨水量=其左侧最高柱子与右侧最高柱子中的较小值-其自身柱子高度 } return res; } int max(int a,int b) { return (a>b?a:b); } int min(int a,int b) { return (a>b?b:a); } };
复杂度分析
• 时间复杂度:代码中使用四次单层循环遍历,,因此时间复杂度为$T(n)=O(3*n)$,总的时间复杂度为$O(n)$。
• 空间复杂度:使用了一个n×2的二维数组,因此空间复杂度为$O(n)$。
方法二
思路分析
在上面的方法中,在计算arr[i]左侧柱子最大值与右侧柱子最大值时,使用了两次循环,即从左到右的循环和从右到左的循环,由此我们可以想到链表中的双指针,即通过设置双指针的办法,通过指针的移动,从而计算每个柱子可存储的水量,直到两个指针相遇。具体步骤如下:
• 初始化左侧柱子最大值left_max=arr[0],右侧柱子最大值right_max=arr[n-1]
• 设置左侧指针为left,初始化为0,指向第一个柱子,向右移动
• 设置右侧指针为right,初始化为n-1,指向最后一个柱子向左移动
• 两个指针还未相遇时:
• 如果左侧柱子最大值left_max小于右侧柱子最大值,并且left_max大于arr[left],则可以计算当前左侧柱子的雨水,即为left_max-arr[left],否则将更新left_max的值为arr[left]。之后left向右侧移动
• 如果左侧柱子最大值left_max大于右侧柱子最大值,并且right_max大于arr[right],则可以计算当前右侧柱子的雨水,即为right_max-arr[right],否则将更新right_max的值为arr[right]。之后right向左侧移动
• 当两个指针相遇时即为运行结束的条件,此时左右柱子的雨水都已计算完。
实例分析如图所示
JAVA核心代码
import java.util.*; public class Solution { /** * max water * @param arr int整型一维数组 the array * @return long长整型 */ public long maxWater (int[] arr) { // write code here int n = arr.length; if(n == 0) { return 0; } long res = 0; int left = 0, right = n - 1; int left_max = arr[0], right_max =arr[n-1]; while(left <= right) { if (left_max < right_max)//左侧柱子最大值left_max小于右侧柱子最大值 { if (left_max > arr[left])//left_max大于arr[left] { res +=left_max-arr[left];//计算当前左侧柱子的雨水 } else left_max = arr[left]; left++; } else//左侧柱子最大值left_max大于右侧柱子最大值 { if (right_max > arr[right])//right_max大于arr[right] { res+=right_max-arr[right];//计算当前右侧柱子的雨水 } else { right_max=arr[right]; } right--; } } return res; } }
复杂度分析
• 时间复杂度:单层循环,时间复杂度为$O(n)$
• 空间复杂度: 使用了几个变量作为指针,因此空间复杂度为$O(1)$