1. 程式人生 > >LeetCode - 42、407 - Trapping Rain Water I - II

LeetCode - 42、407 - Trapping Rain Water I - II

本文總結以下兩道題目:

42. Trapping Rain Water - 直方圖凹陷接雨水問題 (Hard)

407. Trapping Rain Water II - 上題二維變三維 (Hard)

42. Trapping Rain Water

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.


The above elevation map is represented by array [0,1,0,2,1,0,1,3,2,1,2,1].

In this case, 6 units of rain water (blue section) are being trapped. 

Input: [0,1,0,2,1,0,1,3,2,1,2,1]        Output: 6

解:

    第一反應就是這題不難啊,就用類似掃描線演算法,每一個y值計算下這一行能接多少水,每一行只要找到對應 >= y值的最左最右的兩個位置,然後中間所有位置,只要比當前 y 值小,盛水數就加一,否則就不加,這樣很容易得到結果,不過就是很慢。在下面我的AC

程式碼中,稍微加了一點速,就是最左最右的兩個位置的計算不用每次從 0 和 n-1 向裡找,而是從上一次的位置繼續向裡,因為每次找的是第一個 >= y 的最左和最右,所以不會出現找錯位置的情況。

int trap(vector<int>& height)
{
    if(height.size() <= 1)   return 0;
    int res = 0;
    int maxh = *max_element(height.begin(), height.end());
    int l = 0, r = height.size() - 1;
    for(int i = 1; i <= maxh; i++)
    {
        while(height[l] < i) ++l;
        while(height[r] < i) --r;
        int tmpl = l + 1;   // l 和 r 要為下一次繼續使用,所以需要tmpl用於遍歷
        while(tmpl < r)
            if(height[tmpl++] < i)
                ++res;
    }
    return res;
}

    從下邊這張圖就能看出上邊的程式碼有多慢了(其實是可以再加速的,因為y值不一定是連續的,可能 119911 這種情況123456789的y都需要計算,其實不需要,記錄一下哪個y沒算然後跳過,可以加速)。 

    上面的程式碼是一行一行記算能放多少水,但其實觀察結果可以看到,結果一定是先高後低這樣的趨勢,在height最大值的地方最高,如果只有一個最大值,那就是這一個點最高,如果有多個相同最大值,就是一個平行線最高。水的作用就是將凹陷的地方填平,這就想到了一種方法,就是計算所有方塊的個數,也就是總體面積,用這個總體的面積減去黑色的方塊,也就是height陣列佔用的方塊,就是水的多少。計算黑色方塊個數很簡單,將凹陷填平也很簡單,AC程式碼如下:

int trap(vector<int>& height)
{
    if(height.size() <= 1)   return 0;
    int res = 0;
    int black_area = accumulate(height.begin(), height.end(), 0);
    int maxh = *max_element(height.begin(), height.end());
    int l = find(height.begin(), height.end(), maxh) - height.begin();      // 最左邊maxh的位置
    int r = height.rend() - find(height.rbegin(), height.rend(), maxh) - 1; // 最右邊maxh的位置
    // 填平凹陷
    for(int i = 1; i < l; i++)
        if(height[i] < height[i - 1])
            height[i] = height[i - 1];
    for(int i = height.size() - 2; i > r; i--)
        if(height[i] < height[i + 1])
            height[i] = height[i + 1];
    while(l < r)
        height[l++] = maxh;
    // 總體減黑色就是結果  
    return accumulate(height.begin(), height.end(), 0) - black_area;    // 總面積減bar自身佔的面積
}

    還有更簡單的方式就是,不需要先知道最高的有多高,用兩個哨兵分別從最左最右向內收,不斷更新最大高度,重點是,要低的位置先向內收(短板效應)。

int trap(vector<int>& height)
{
    int res = 0, l = 0, r = height.size() - 1, maxhl = INT_MIN, maxhr = INT_MIN;
    while(l < r) {
        if(height[l] <= height[r]) {
            if(height[l] > maxhl)   maxhl = height[l];
            else                    res += maxhl - height[l];
            ++l;
        }
        else {
            if(height[r] > maxhr)   maxhr = height[r];
            else                    res += maxhr - height[r];
            --r;
        }
    }
    return res;
}

// 簡化版如下
int trap(vector<int>& height)
{
    int l = 0, r = height.size() - 1, tmpmax = 0, res = 0;
    while (l < r) {
        int lower = height[height[l] <= height[r] ? l++ : r--]; // 左右兩個哨兵位置上,低的那個位置
        if(lower > tmpmax)  tmpmax = lower;
        else                res += tmpmax - lower;
    }
    return res;
}

    其實我自己覺得上邊這個我自己的演算法比LeetCode Solution 以及網上的解法都簡單,但是還是要記錄下邊這種用佇列來存高度,然後通過高度計算每個位置能存多少水的方法,因為不看懂這個方法,下邊那道三維上的題的解法很難理解。。

    這種用佇列的思想其實和上邊的程式碼差不多,也是記錄目前最高的板子高度,這樣能知道後邊的每個位置最高能裝多高的水。

int trap(vector<int>& height)
{
    int ans = 0, current = 0;
    stack<int> st;
    while (current < height.size()) {
        while (!st.empty() && height[current] > height[st.top()]) {
            int top = st.top();
            st.pop();
            if (st.empty())
                break;
            //printf("current = %d, st.top() = %d\n", current, st.top());
            int distance = current - st.top() - 1;
            int bounded_height = min(height[current], height[st.top()]) - height[top];       
            ans += distance * bounded_height;
            //printf("ans增加 %d\n", distance * bounded_height);
        }
        st.push(current++);
    }
    return ans;
}

    可以通過下圖更加直觀地理解利用堆疊的演算法,根據輸出,能夠理解為什麼第三次增加0,,第四次增加3,就很好理解這個演算法了。 

407. Trapping Rain Water II

Given an m x n matrix of positive integers representing the height of each unit cell in a 2D elevation map, compute the volume of water it is able to trap after raining.

Note:
Both m and n are less than 110. The height of each unit cell is greater than 0 and is less than 20,000.

Given the following 3x6 height map:
[
  [1,4,3,1,3,2],
  [3,2,1,3,2,4],
  [2,3,3,2,3,1]
]
   Return 4.

The above image represents the elevation map [[1,4,3,1,3,2],[3,2,1,3,2,4],[2,3,3,2,3,1]] before the rain.

After the rain, water is trapped between the blocks. The total volume of water trapped is 4.

解:

    這道題和前邊的題看山去這不過是二維變三維,但是上題的 “填平後總數減去原有方塊數的” 以及 “雙指標夾逼” 兩種方式就不好實現了。在第一題中基本思路是每次從兩端的高度較小的一端移動,這樣做的意義在於我們每次都是遍歷最短的一個位置,也就是通過短板效應計算能盛水的多少(因為長的板子不影響結果),同時還需要維護一個當前邊界的最大值,這樣才知道哪個是短的。

    這道題目中,邊界從兩個點,變成了一圈點,也就是對這一圈點,找一個最矮的,然後遍歷,如果有比它矮的,且沒有遍歷過的,那就說明,這裡可以盛水!根據這個思想,能夠想到用一個容器記錄邊界,但是用stack肯定是不行的了,要用到priority_queue,這樣能夠很方便地找到最矮的板子的位置,因為我們記錄方塊高度的同時還需要記錄位置,所以資料結構就比較複雜了。而且由於STL中,優先佇列預設是大根堆,而我們需要小根堆,所以需要下邊的第三種寫法。

priority_queue<int> q;    // 大根堆
priority_queue<int, vector<int>, less<int> >; // 等價於上邊
priority_queue<int, vector<int>, greater<int> >; // 小根堆

    程式碼如下:

int trapRainWater(vector<vector<int>>& heightMap)
{
    if(heightMap.size()==0) return 0;
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> q;    // 小根堆
    int rows = heightMap.size(), cols = heightMap[0].size();
    vector<vector<int> > visited(rows, vector<int>(cols, 0));
    int res = 0, tMax = INT_MIN;
    for(int i = 0; i < rows; i++)
        for(int j = 0; j < cols; j++)
            if(i == 0 || i == rows - 1 || j == 0 || j == cols - 1)    // 只遍歷邊界一圈
            {
                q.push(make_pair(heightMap[i][j], i * cols + j));
                visited[i][j] = 1;                   
            }
    int dir[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; // 上下左右
    while(!q.empty())
    {
        auto t = q.top();
        q.pop();
        int h = t.first, x = t.second / cols, y = t.second % cols;
        tMax = max(tMax, h);
        for(auto d: dir)
        {
            int x2 = x + d[0], y2 = y + d[1];
            if(x2 >= rows || x2 < 0 || y2 < 0 || y2 >= cols || visited[x2][y2]) continue; // 超出邊界或已經訪問過
            visited[x2][y2] = 1;
            if(heightMap[x2][y2] < tMax)
                res += tMax - heightMap[x2][y2];
            q.push(make_pair(heightMap[x2][y2], x2 * cols + y2));
        }
    }
    return res;
}