1 題目
題目鏈接:
https://leetcode-cn.com/problems/trapping-rain-water/
接雨水問題在leetcode中是“困難”,但同時也是面試中常遇到的問題。
1.1 題目描述:
給定 n 個非負整數表示每個寬度爲 1 的柱子的高度圖,計算按此排列的柱子,下雨之後能接多少雨水。
上面是由數組 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度圖,在這種情況下,可以接 6 個單位的雨水(藍色部分表示雨水)。
示例:
輸入: [0,1,0,2,1,0,1,3,2,1,2,1]
輸出: 6
1.2 題目分析:
這道題很想我們常說的“木桶效應”,說白了就是一個柱子能接多少水,取決於它兩邊“較短的板”,另外一個前提條件就是,兩邊的柱子高度都要比所要裝水的柱子的高度要高,否則肯定是無法裝水的。
有了這個認識之後,再取解這道題目就不難了。
如圖,我們要計算柱子i的接水容量的時候,能否接水取決於兩邊最高(left_max, right_max)的高度,但是接水的上限則取決於left_max和right_max中較小的一方(即上邊所說的“短板”)。
2 題解
前三種解法都是(如上圖)縱向考慮每個柱子能裝多少水。
第4種解法從橫向考慮問題,具體解釋見下文。
2.1 暴力
暴力解法比較好理解,但是時間複雜度O(n^2).
// 42. 接雨水
// 暴力
public int trap(int[] height) {
int len = height.length;
if(len<=1) return 0;
int res = 0;
for (int i = 0; i < len; i++) {
int l_max = height[0], r_max = height[len-1];
for (int j = 0; j <= i; j++) {
l_max = Math.max(l_max,height[j]);
}
for(int j=i; j<len;j++){
r_max = Math.max(r_max,height[j]);
}
// System.out.println(l_max+" "+r_max);
res += (Math.min(l_max,r_max)-height[i]);
}
return res;
}
2.2 動態規劃
動態規劃,可理解爲“用空間換時間”,也就是說,空間複雜度增大,但是時間複雜度降低。(另外避免一些重複計算)
上文暴力解法,在內層for去尋找左右max的時候,其實是有重複計算的,而利用額外的空間去存儲前一個狀態的左右max則可以避免這些重複計算。
即:創建兩個數組(l_max,r_max)去存儲,l_max[i]和r_max[i]在index=i的時候,其左右各自最高的高度。
這種解法,時間空間複雜度都是O(N).
public int trap(int[] height) {
int len = height.length;
if(len<=1) return 0;
int res = 0;
int[] l_max = new int[len];
int[] r_max = new int[len];
l_max[0] = height[0];
r_max[len-1] = height[len-1];
for (int i = 1; i < len; i++) {
l_max[i] = Math.max(height[i],l_max[i-1]);
}
for(int i = len-2; i>=0;i--){
r_max[i] = Math.max(height[i],r_max[i+1]);
}
for (int i = 0; i < len; i++) {
res += Math.min(l_max[i],r_max[i])-height[i];
}
return res;
}
2.3 雙指針(前後指針)
雙指針和前邊兩種方法類似,但是又有一些細微的區別,看下圖:
參考: https://www.cnblogs.com/labuladong/p/12320514.html
如計算index=left處的接水容量,雙指針法只在意l_max是(l_max,r_max)二者中較小的一方,但是並不關係,r_max是不是left右邊最高的(r_max只是right右側最高的。)
時間複雜度O(N),空間複雜度O(1).
// 雙指針(左右指針)
public int trap3(int[] height) {
int len = height.length;
if(len<=1) return 0;
int res = 0;
int l_max = height[0], r_max = height[len-1];
int left = 0, right = len-1;
while (left<right){
l_max = Math.max(height[left],l_max);
r_max = Math.max(height[right],r_max);
if(l_max<r_max){
res += Math.min(l_max,r_max)-height[left];
left++;
} else{
res += Math.min(l_max,r_max)-height[right];
right--;
}
}
return res;
}
2.4 單調棧
單調棧的思路跟上述三種方法則完全不同,它從橫向去考慮問題,如圖:
類似於leetcode84-柱狀圖中的最大矩形,不過這裏使用單調(遞減)棧,即:height[i]比stack的height[peek]小的時候才入棧,否則出棧,出棧則計算接水量。
同樣類似“最大矩形問題”,兩邊加入兩個哨兵(兩個0)
詳細圖解:
這裏用單調遞減棧,與單調第增棧,有所不同的是,有元素出棧後,stack可能爲空,則無法取棧頂元素peek,這裏需要“仍把它pop,但是不計算面積”,因爲新的元素比pop的元素大,所以小的元素就沒有用了,(從座標系來看)我們只關心左邊最高的柱子,而且,如果不pop它,加入新元素後,棧內順序就不是遞減了。
入棧、出棧的棧圖可以自己動手畫一下,更有助於理解,這裏只講一下流程,棧圖省略。
代碼:
// 單調“遞減”棧
public int trap4(int[] height) {
int len = height.length;
if(len<=1) return 0;
int res = 0;
Deque<Integer> stack = new ArrayDeque<>();
int[] new_height = new int[len+2];
System.arraycopy(height,0,new_height,1,len);
for (int i = 0; i < len+2; i++) {
while(!stack.isEmpty() && new_height[i]>new_height[stack.peek()]){
int cur = stack.pop();
if(stack.isEmpty())
break;
int pk = stack.peek();
int area = (i - pk - 1) * (Math.min(new_height[pk], new_height[i]) - new_height[cur]);
res += area;
// System.out.println(pk);
}
stack.push(i);
}
return res;
}
單調棧解法的時間複雜度O(N),空間複雜度也是O(N). 其實看起來單調棧方法還沒有“動態規劃”和“雙指針”優越,這裏主要是用於方法學習。
以上是我的個人理解,如有錯誤,歡迎批評指正!謝謝!