接雨水問題詳解


接雨水這道題目挺有意思,在面試題中出現頻率還挺高的,本文就來步步優化,講解一下這道題。

先看一下題目:

就是用一個數組表示一個條形圖,問你這個條形圖最多能接多少水。

int trap(int[] height);

下面就來由淺入深介紹暴力解法 -> 備忘錄解法 -> 雙指針解法,在 O(N) 時間 O(1) 空間內解決這個問題。

一、核心思路

我第一次看到這個問題,無計可施,完全沒有思路,相信很多朋友跟我一樣。所以對於這種問題,我們不要想整體,而應該去想局部;就像之前的文章處理字符串問題,不要考慮如何處理整個字符串,而是去思考應該如何處理每一個字符。

這么一想,可以發現這道題的思路其實很簡單。具體來說,僅僅對於位置 i,能裝下多少水呢?

能裝 2 格水。為什么恰好是兩格水呢?因為 height[i] 的高度為 0,而這里最多能盛 2 格水,2-0=2。

為什么位置 i 最多能盛 2 格水呢?因為,位置 i 能達到的水柱高度和其左邊的最高柱子、右邊的最高柱子有關,我們分別稱這兩個柱子高度為 l_maxr_max位置 i 最大的水柱高度就是 min(l_max, r_max)

更進一步,對於位置 i,能夠裝的水為:

water[i] = min(
               # 左邊最高的柱子
               max(height[0..i]),  
               # 右邊最高的柱子
               max(height[i..end]) 
            ) - height[i]
    

這就是本問題的核心思路,我們可以簡單寫一個暴力算法:

int trap(vector<int>& height) {
    int n = height.size();
    int ans = 0;
    for (int i = 1; i < n - 1; i++) {
        int l_max = 0, r_max = 0;
        // 找右邊最高的柱子
        for (int j = i; j < n; j++)
            r_max = max(r_max, height[j]);
        // 找左邊最高的柱子
        for (int j = i; j >= 0; j--)
            l_max = max(l_max, height[j]);
        // 如果自己就是最高的話,
        // l_max == r_max == height[i]
        ans += min(l_max, r_max) - height[i];
    }
    return ans;
}

有之前的思路,這個解法應該是很直接粗暴的,時間復雜度 O(N^2),空間復雜度 O(1)。但是很明顯這種計算 r_maxl_max 的方式非常笨拙,一般的優化方法就是備忘錄。

二、備忘錄優化

之前的暴力解法,不是在每個位置 i 都要計算 r_maxl_max 嗎?我們直接把結果都緩存下來,別傻不拉幾的每次都遍歷,這時間復雜度不就降下來了嘛。

我們開兩個數組 r_maxl_max 充當備忘錄,l_max[i] 表示位置 i 左邊最高的柱子高度,r_max[i] 表示位置 i 右邊最高的柱子高度。預先把這兩個數組計算好,避免重復計算:

int trap(vector<int>& height) {
    if (height.empty()) return 0;
    int n = height.size();
    int ans = 0;
    // 數組充當備忘錄
    vector<int> l_max(n), r_max(n);
    // 初始化 base case
    l_max[0] = height[0];
    r_max[n - 1] = height[n - 1];
    // 從左向右計算 l_max
    for (int i = 1; i < n; i++)
        l_max[i] = max(height[i], l_max[i - 1]);
    // 從右向左計算 r_max
    for (int i = n - 2; i >= 0; i--) 
        r_max[i] = max(height[i], r_max[i + 1]);
    // 計算答案
    for (int i = 1; i < n - 1; i++) 
        ans += min(l_max[i], r_max[i]) - height[i];
    return ans;
}

這個優化其實和暴力解法差不多,就是避免了重復計算,把時間復雜度降低為 O(N),已經是最優了,但是空間復雜度是 O(N)。下面來看一個精妙一些的解法,能夠把空間復雜度降低到 O(1)。

三、雙指針解法

這種解法的思路是完全相同的,但在實現手法上非常巧妙,我們這次也不要用備忘錄提前計算了,而是用雙指針邊走邊算,節省下空間復雜度。

首先,看一部分代碼:

int trap(vector<int>& height) {
    int n = height.size();
    int left = 0, right = n - 1;
    
    int l_max = height[0];
    int r_max = height[n - 1];
    
    while (left <= right) {
        l_max = max(l_max, height[left]);
        r_max = max(r_max, height[right]);
        left++; right--;
    }
}

對於這部分代碼,請問 l_maxr_max 分別表示什么意義呢?

很容易理解,l_maxheight[0..left] 中最高柱子的高度,r_maxheight[right..end] 的最高柱子的高度

明白了這一點,直接看解法:

int trap(vector<int>& height) {
    if (height.empty()) return 0;
    int n = height.size();
    int left = 0, right = n - 1;
    int ans = 0;
    
    int l_max = height[0];
    int r_max = height[n - 1];
    
    while (left <= right) {
        l_max = max(l_max, height[left]);
        r_max = max(r_max, height[right]);
        
        // ans += min(l_max, r_max) - height[i]
        if (l_max < r_max) {
            ans += l_max - height[left];
            left++; 
        } else {
            ans += r_max - height[right];
            right--;
        }
    }
    return ans;
}

你看,其中的核心思想和之前一模一樣,換湯不換葯。但是細心的讀者可能會發現次解法還是有點細節差異:

之前的備忘錄解法,l_max[i]r_max[i] 代表的是 height[0..i]height[i..end] 的最高柱子高度。

ans += min(l_max[i], r_max[i]) - height[i];

但是雙指針解法中,l_maxr_max 代表的是 height[0..left]height[right..end] 的最高柱子高度。比如這段代碼:

if (l_max < r_max) {
    ans += l_max - height[left];
    left++; 
} 

此時的 l_maxleft 指針左邊的最高柱子,但是 r_max 並不一定是 left 指針右邊最高的柱子,這真的可以得到正確答案嗎?

其實這個問題要這么思考,我們只在乎 min(l_max, r_max)。對於上圖的情況,我們已經知道 l_max < r_max 了,至於這個 r_max 是不是右邊最大的,不重要,重要的是 height[i] 能夠裝的水只和 l_max 有關。

我最近精心制作了一份電子書《labuladong的算法小抄》,分為【動態規划】【數據結構】【算法思維】【高頻面試】四個章節,共 60 多篇原創文章,絕對精品!限時開放下載,在我的公眾號 labuladong 后台回復關鍵詞【pdf】即可免費下載!

目錄

歡迎關注我的公眾號 labuladong,技術公眾號的清流,堅持原創,致力於把問題講清楚!

labuladong


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM