1. 程式人生 > >LeetCode - Container With Most Water、Largest Rectangle in Histogram - 直方圖中面積問題

LeetCode - Container With Most Water、Largest Rectangle in Histogram - 直方圖中面積問題

本文會總結以下兩道題目:

11 - Container With Most Water - 直方圖中盛水問題 (Medium)

84 - Largest Rectangle in Histogram - 直方圖中最大矩形面積問題 (Hard)

還有兩道題目也是直方圖中面積問題,在另一篇中總結,(還沒寫- -)

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

407 - Trapping Rain Water II - 42題的二維變三維 (Hard)


11. Container With Most Water

Given n non-negative integers a1a2, ..., an , where each represents a point at coordinate (iai). n vertical lines are drawn such that the two endpoints of line i is at (iai) and (i, 0). Find two lines, which together with x-axis forms a container, such that the container contains the most water.

Note: You may not slant the container and n is at least 2.

The above vertical lines are represented by array [1,8,6,2,5,4,8,3,7].

In this case, the max area of water (blue section) the container can contain is 49.

解:

    這道題的意思就是用直方圖中任意兩個柱子當邊界,中間部分盛水,求最大的能盛水的多少,也就是最大的形成的矩形的面積。很明顯,由於短板效應,能盛水多少取決於兩個板子中間短的那個,而且中間部分是完全不用考慮的,兩個任意板子 h[i] 和 h[j] (0 <= i < j <= n)形成的面積就是 min(h[i], h[j]) * (j - i)

    出於以上分析,這道題就很適合用夾逼的方式計算,也就是兩個哨兵分別從最左和最右向內收,直到兩個哨兵相遇位置結束。我們需要設計的就是兩個哨兵想內遍歷時候的判斷方式,也就是到底誰動。程式碼結構如下:

int maxArea(vector<int>& height)
{
    int res;
    int l = 0, r = height.size() - 1;
    while(l < r)
    {
        // 計算res
    }
    return res;
}

    由於上面提到的短板效應,兩個板子中間短的決定矩形面積,長的板子是沒有實質性作用的。如果我們將長的板子的哨兵向內移動,短的不動的話,無論如何矩形面積都不可能變大的,就算板子變長了也不會影響盛水高度,反而由於板子之間距離減少了一而使得面積變小。所以在哨兵移動的時候,應該是短的向內移動,長的板子不動,只有這樣才有可能讓盛水高度變高,這樣才有可能在板子間距離變少的情況下,盛水總量變大。兩個板子一樣高的情況下,兩個哨兵哪個向內移動都可以。分析後很容易寫出程式碼:

int maxArea(vector<int>& height)
{
    int l = 0, r = height.size() - 1, max_water = -1;
    while(l < r)
    {
        max_water = max(max_water, (r - l) * min(height[l], height[r]));
        if(height[l] < height[r])   ++l;
        else                        --r;
    }
    return max_water;
}

84. Largest Rectangle in Histogram

Given n non-negative integers representing the histogram's bar height where the width of each bar is 1, find the area of largest rectangle in the histogram.


Above is a histogram where width of each bar is 1, given height = [2,1,5,6,2,3].


The largest rectangle is shown in the shaded area, which has area = 10 unit.

Example:

Input: [2,1,5,6,2,3] Output: 10

解:

    這道題和上邊的盛水一樣是求最大矩形面積,不過約束條件從兩端的板子高度變成了板子內部,也就是在這個直方圖所有板子組成的圖形中畫矩形,面積最大的是多少。

    由於約束條件是最短的板子,所以用夾逼的方式是不行的(中間的板子可能比兩端的板子更低)。應該是從每個板子出發,看看以這個板子為高度最大的矩形面積是多少,比如上圖中第一個高度為2的板子,高為2,寬就只有1,因為後邊的1比它低。也就是說,對於每個板子,從它所在位置分別向左向右走,走到比這個它低的板子停止,這些和它連著的高度 >= 它的板子組合起來就是當前這個板子為高度組成的最大矩形。簡單地說就是,對於每個板子,矩形的寬就是板子的高,我們要計算的就是矩形的長。AC程式碼如下:

int largestRectangleArea(vector<int>& heights)
{
    int res = 0;        // 如果heights是空的,直接返回0
    int len = heights.size();
    for(int i = 0; i < len; i++)
    {
        if(i > 0 && heights[i] == heights[i - 1])
            continue;
        int l = i - 1, r = i + 1;
        while(l >= 0 && heights[l] >= heights[i])
            --l;
        while(r <= len - 1 && heights[r] >= heights[i])
            ++r;
        res = max(res, (r - l - 1) * heights[i]);
    }
    return res;
}

     演算法思路的核心就是:限制一個板子能組成的矩形面積的是,它左邊第一個比它低的板子,和它右邊第一個比它低的板子。上邊的程式碼雖然AC,但是效率較低,主要是因為每個板子相當於遍歷整個直方圖,時間複雜度為 O(n^2) 。對於每個板子,我們其實不需要遍歷它左右的所有板子,如果我們存起來它前邊的板子,我們就知道它左邊能到哪裡了。

    我們可以利用一個stack,比如叫 bar_stack,從左往右遍歷直方圖的每個板子 bar[i],如果 bar_stack.top() < bar[i] ,那就說明這個板子比 stack裡的都高,push進stack,如果 bar_stack.top() = bar[i],那就說明有連續的相同高度的板子,但是不能和上邊的程式碼一樣直接跳過,不過進行 if 和 else 那種處理都行。重點是,如果 bar_stack.top() > bar[i],那就是說,我們找到了 bar_stack.top() 這個板子右邊第一個比它低的板子!!而stack中它前一個板子一定它左邊第一個比它低的板子!!我們就很容易計算這個板子組成的矩形了,也就是(右邊板子的索引 - 左邊板子的索引)* bar_stack.top() 這個板子的高度

    理解了上面的分析(更加詳細的英文分析在 https://www.geeksforgeeks.org/largest-rectangle-under-histogram/),就能明白 stack 裡push的不是板子高度,而是板子索引,這樣就很容易寫出程式碼。有一個小細節就是,因為 bar[i] 可能會比stack中不止一個bar高度要低,所以需要pop很多次,而且對於小的 bar[i] ,i 還是要放進去的,所以用 while 會比 for 更好寫。(英文講解文件中相同高度的板子也被放入stack中,也就是push之後,再pop,不過 hist[s.top()] <= hist[i] 這個判斷改為 < 也是對的,也就是pop出去,再push)。比如 3, 9, 9, 9 這個例子,每個9用這個演算法算出來的是不一樣的結果,不過最終算出來最大的是對的。感覺這道題這麼做還是挺複雜的,還不如用我第一種方法。。。。。但是這種方法快,省去了從每塊板子向左向右的遍歷。

int largestRectangleArea(vector<int>& heights)
{
    int res = 0, i = 0;
    stack<int> hs;
    while (i < heights.size())
    {
        if (hs.empty() || heights[i] >= heights[hs.top()]) // 比它高,或相同就push進去
            hs.push(i++);
        else if (heights[i] < heights[hs.top()])          // 重點部分
        {
            int t = hs.top();
            hs.pop();
            res = max(res, heights[t] * (hs.empty() ? i : i - hs.top() - 1));	// i 表示的是 i - 0
        }
    }
    // 現在 i 是 heights.size()
    // 所有height遍歷完成,還要將hs遍歷一遍,如果12345,那現在stack裡就是12345,res還沒有計算過
    while (!hs.empty())
    {
        int t = hs.top();
        hs.pop();
        res = max(res, heights[t] * (hs.empty() ? i : i - hs.top() - 1));	// i 表示的是 i - 0
    }
    return res;
}