標籤:
【描述】
Given n non-negative integers representing an elevation map where the width of each bar is 1, compute how much water it is able to trap after raining.
For example,
Given [0,1,0,2,1,0,1,3,2,1,2,1], return 6.
【中文描述】
給定n個非負數,想像它們代表了n堵牆,牆的高度就是Ni, 現在想像下了一場雨,讓求這些牆能儲存多少水?
————————————————————————————————————————————————————————————
【初始思路】
這個題挺有意思, 所以也沒看discuss, 完全自己硬剛出來的。這個題其實不難,主要是考驗思維的縝密程度,然後逐段逐情況分析即可。
我一開始的思路是(brutle的就不討論了吧),用i遍曆數組,遇到比i高的,就停下來,然後算裡面的水量,然後更新i。
這是最基本的思路了,但是,裡面有巨量的細節需要考慮:
細節1: 如果i下一位比i高或和i一樣高,顯然i這堵牆就沒用了, continue;
好了,i 的下一堵牆肯定低於 i 牆,這個時候,我使用了動態規劃的思想,不管三七二十一,先開始按照 i 牆的高度算水量,給一個變數j = i+1, j一直移動到結尾。
那麼在 j 移動到結尾的過程中,有幾種可能性:
(1)遇到了某一堵牆比 i 牆要高, 真是太好了, 那麼計算到這個位置的時候, 這段區間內的水量就是咱們動態規劃計算出來的水量, result加入這個值就可以了。然後j也不用再往後看了, i=j,continue即可, 可以看協助理解這種情況, 淺綠色的面積 == maxPoten, 這就是留水量。這個值加入result後, i移動到j,然後繼續用j往後找就行了。
(2)從頭到尾,j都沒有遇到比i還高的牆,那怎麼辦?這個時候,最簡單的辦法是,i 直接continue。 但是,這樣的話,我們剛才算過的值全都白算了,而且這樣算,時間複雜度肯定就接近O(n2)了。所以我根本沒實現這個思路(雖然這個思路是最簡單的,面試時候可以直接這麼寫);
(2改)我的思路是,已經算的不能白費,怎麼辦呢?既然比 i 牆高或者等於 i 牆的沒找到,那我們就爭取找到比 i 低的最遠處的一堵牆。如果最壞情況下,沒找到比 i 牆高或者等於 i 牆的, 我們還可以用比 i 牆稍低一點的這個牆來補償,剛才做的計算也不會白費。 並且!!最重要的是, 這樣計算完後, i可以直接跳到這堵稍低點的牆的位置,時間複雜度大大降低。最優情況可以接近O(n)!
好了, 這樣的話,我們在計算j->結尾的這個過程中, 有幾個變數需要即時計算:
maxPoten, 代表了最大可能留水量。當找到比i牆高或者等於i牆的, 這個值就是留水量, 絕對不會錯!
lower, 代表了沒找到理想牆,但是找到了比 i 牆稍低一點的牆, 那麼在這種情況下, lower牆決定的留水量就是這個區間內的留水量,絕對不會錯!
lowerPoten,代表由lower牆決定的留水量。 並且,lowerPoten和maxPoten之間有算術關係,稍後我們討論這個關係,並且給出公式。
這些是否足夠了?我們來看看下面的圖,協助理解這種情況下可能的問題:
如, 找到lower後,實際的存水量應該是lowerPoten,那麼lowerPoten怎麼算出來。maxPoten -(兩牆高差)*(兩牆距離) = lowerPoten?光憑抽象理解,很容易得出這樣一個錯誤的式子,但是看上面圖就一目瞭然了, 細節2: lower後面的面積,也需要減去!正上標示的一樣,這個面積怎麼算?
我們根據lower的更新機制來看, 這個面積其實和lower息息相關,我們即時計算的時候,除了即時計算maxPoten,再計算這個面積,然後每次求出新的lower的時候,這個面積歸零。最終,我們就可以求得這個面積了。
所以,我們還需要一個變數: lowerBehind.
那麼, lowerPoten = maxPoten - lowerBehind - (兩牆高差)*(兩牆距離)。
這樣是否就OK了?答案是否定的,我們來看下面的圖:
可以很明顯看出來, 由於左上方橘紅色區塊的存在,之前的公式就是錯誤的了: lowerPoten = maxPoten - lowerBehind - (兩牆高差)*(兩牆距離)。
因為顯然,細節3: maxPoten需要減掉step上的水體積,然後再減(lower和step之間的間距) * (兩牆高差) 才能得到正確答案。
所以,需要考慮下怎麼算step上的水量。
思考一下step為何會產生,從圖上可以看出來,因為step高度>=lower牆的高度。step的確定看似簡單:j從i+1位開始往後推移的時候,遇到比lower高且和i牆緊鄰的都是step。但是實現的時候這個想法就不好實現了,因為lower還沒有產生,我怎麼知道當前這個牆是不是step。 所以,我的思路是:
只要 j 牆緊鄰 i 牆,就把 j 牆先算作step,然後存入一個list裡。
怎麼確定緊鄰?簡單,給一個boolean的緊鄰標記=true, 只要當前 j 牆不為0且緊鄰標記為true,那麼當前 j 牆肯定緊鄰 i 牆。如果一旦 j 牆==0了,緊鄰標誌置為false即可。
在最終確定了lower後,我們再遍曆這個list,把從左往右最後一個大於lower的牆標記為最右step牆。 那麼 step1, step2, .. stepn的水量加起來就是中的step水量。同時,因為已經算出了最靠右的step牆,那麼lower和這個牆之間的差距就可以用來算lowerPoten了。
到此,我們得出最後的公式:
lowerPoten = maxPoten - lowerBehind - stepWater -(兩牆高差)*(lower - 最右step牆)
最後整個演算法最核心的點:lower,怎麼算?
首先,lower肯定是從最小往最大去更新。 那麼lower最開始=0, 然後遇到比當前lower大的,lower就更新。但是這樣做的話,有一種情況就無法解決了。看:
根據上面描述,那麼遇到這種遞減數列到結束的情況, lower就會在i + 1的位置。這顯然是不合理的。因為這樣的話,這種情況下也能留水!那麼lower應該怎麼選?
顯然,lower只要在數列不是遞減的情況下,才會至少找到一個。如果數列遞減,lower肯定不能找到。
所以,lower的更新機制,除了上面提到的之外,還需要加上,細節4: j 牆 > j - 1牆的情況下, lower開始更新。一旦出現了j牆>j-1牆的情況, 肯定不是遞減數列了,那麼就可以更新lower了。
到此,整個演算法裡的細節就都分析到了,綜上,可以寫出代碼。
【Show me the Code!!!】
1 if (height == null || height.length == 0) return 0; 2 3 int result = 0; 4 int i = 0; 5 while (i < height.length) { 6 //每次i移動後, 幾個變數要歸0 7 int maxPoten = 0; 8 int lowerPoten = 0; 9 int lower = -1;10 int lowerBehind = 0;//lower後的總水量,必要時候要減掉,每次lower更新後,此值更新到0,並重新累積11 if (i < height.length - 1 && height[i] <= height[i + 1]) {12 i++;13 continue;//當前i比後一個矮, 直接continue, 細節114 }15 List<Integer> union = new ArrayList<Integer>();//儲存和i牆連續的低牆16 boolean isUnion = true;17 int j = i + 1;18 while (j < height.length) {19 if (height[j] >= height[i]) {20 //找到了比height[i]還高或者一樣高的, 直接break;21 //當前的maxPoten就是臨時結果,直接加入result即可22 break;23 }24 if (lower == -1 && height[j] > height[j - 1]) {25 //第一次出現了升序, lower更新到第一個位置, 細節426 lower = j;27 }28 if (height[j] == 0) {29 isUnion = false;30 }31 maxPoten += height[i] - height[j];//每一步都要計算maxPoten32 lowerBehind += height[i] - height[j];//細節2, lower後的面積需要計算出來,必要時候要減去33 if (isUnion) {34 union.add(j);//最終計算的時候, union部分也需要考慮35 }36 if (lower != -1 && height[j] >= height[lower]) {37 lower = j;//更新lower到最遠,且高度僅次於height[i]的位置38 //每次更新lower後,lowerBehind從頭算39 lowerBehind = 0;40 }41 j++;42 }43 // 有2個可能性:44 // (1) break出來的, 那麼maxPoten直接加入result, continue45 // (2) j遍曆完了整個數組, 說明maxPoten不可用, 那麼計算lowerPoten46 if (j == height.length) {47 //說明是正常遍曆完的, lower起作用了48 //計算lowerPoten49 //先算起初台階的存水,這部分要區分對待50 if (lower == -1) {51 //說明也沒找到lower, 說明就是一直降的台階, 迴圈直接結束52 break;53 }54 int stepWater = 0;55 int k = 0;56 if (union.size() > 0) {//計算台階57 while (k < union.size() && height[union.get(k)] > height[lower]) {58 stepWater += height[i] - height[i + k + 1];//細節3, 計算step上的水量59 k++;60 }61 }62 //此外, maxPoten應截止到lower, lower後的水不能再算進去.63 lowerPoten = maxPoten - stepWater - lowerBehind - (height[i] - height[lower]) * (lower - (i + k));64 result += lowerPoten;65 i = lower; // i更新到lower位置66 } else {67 //說明是break出來的68 result += maxPoten;69 i = j; //i移動到j的位置70 }71 }72 73 return result;74 }trap
代碼有點長,但是效率絕對高,leetCode上跑一遍全部用例,只用了5ms。
時間複雜度上來看,由於每次算出lower 或者 找到比 i 牆還高的牆之後, i 直接移到了lower或更高的牆去。 所以理想情況下是O(n)的複雜度。平均攤還上來看,也比較接近O(n)。
LeetCode (42): Trapping Rain Water