1. 程式人生 > >單調棧的學習及例題(左右側最近更大數的距離問題和直方圖最大矩形問題)

單調棧的學習及例題(左右側最近更大數的距離問題和直方圖最大矩形問題)

單調佇列和單調棧很相似,他們是什麼區別呢? 首先引用
http://blog.sina.com.cn/s/blog_6ffc3bde01015l2m.html
的話:
單調棧解決的是以某個值為最小(最大)值的最大區間,實現方法是:求最小值(最大值)的最大區間,維護一個遞增(遞減)的棧,當遇到一個比棧頂小的值的時候開始彈棧,彈棧停止的位置到這個值的區間即為此值左邊的最大區間;同時,當一個值被彈掉的時候也就意味著比它更小(更大)的值來了,也可以計算被彈掉的值得右邊的最大區間。
單調佇列解決的是區間最小(最大)值,實現方法是:求區間最小(最大)值,就維護一個遞增的雙端佇列,隊中儲存原始序列的標號,當即將入隊的元素的值比隊尾的元素的值小(大)的時候就不斷彈掉隊尾,知道出現比它更小的值,當即將入隊的元素隊首元素的跨度(即將入隊元素的序號到隊首元素序列的區間)大於規定區間時就不斷彈掉隊首,直到跨度小於或等於所規定的區間。如此可保證隊首元素為最小(最大)值,(但不能保證隊尾就是原始序列中的最大(最小)值),並維護區間長度。

補充一些個人總結:
1) 單調佇列可以從隊首和隊尾pop值,而單調棧只能從棧頂pop值。從這個意義來看,單調佇列並不是嚴格意義的佇列(不能用queue而必須用deque),而單調棧卻是嚴格意義的棧(可以用stack,當然也可以用deque)。
2) 單調佇列是從隊尾push值,單調棧是從棧頂push值。從這點來看,單調佇列的隊尾跟單調棧的棧頂是一樣的。
3) 單調佇列通常還有區間長度限制 ,而單調棧不一定有區間長度限制(我看到的題目好像都沒有)。所以單調棧其實更簡單,因為不需要實時考慮區間溢位。
4) 單調佇列求區間最大值用遞減佇列,求區間最小值用遞增佇列。
單調棧求左(或右)側比當前值大的邊界用遞減佇列,求左(或右)比當前值小的邊界用遞增佇列。為啥求比當前值大的邊界是遞減佇列呢?因為這樣才能保證棧頂比新元素小的時候,棧頂的下一個元素(和下下一個元素…,都比棧頂大)能夠挨個和新元素比較。

單調棧例題:
例題1:左右側最近更大數的距離問題。 給一個數組,返回一個大小相同的陣列。返回的陣列的第i個位置的值應當是,對於原陣列中的第i個元素,至少往右走多少步,才能遇到一個比自己大的元素(如果之後沒有比自己大的元素,或者已經是最後一個元素,則在返回陣列的對應位置放上-1)。

簡單的例子:
input: 5,3,1,2,4
return: -1 3 1 1 -1

此題用暴力法複雜度是O(n^2)。用單調棧的話複雜度是O(n)。這裡單調棧裡面從棧底到棧頂為遞減排列。具體執行順序:
1) 5(對應的序號)入棧。
2) 因為3比5小,3(對應的序號)入棧。
3) 因為1比3小,1(對應的序號)入棧。
4) 因為2比1大,1對應的距離就是2的序號-1的序號=1,記錄在1對應的output陣列中。然後1出棧,3成為棧頂。因為2比3小,所以3不出棧,2入棧。
5) 因為4比2大,2對應的距離就是4的序號-2的序號=1,記錄在2對應的output陣列中,然後2出棧,3成為棧頂。然後4還是比3大,3對應的距離就是4的序號-3的序號=3,記錄在3對應的output陣列中,然後3出棧。因為4沒有5大,所以5不出站。4入棧。
6) 程式跑完了,5和4在棧中,它們對應的output陣列的元素還是-1。

vector<int> NextLarger(vector<int> &data) {
    vector<int> output(data.size(), -1); //首先都初始化為-1
    stack<int> monoStack;

    for (int i=0; i<data.size(); ++i) {
        while(!monoStack.empty() && data[monoStack.top()]<data[i]) {
            output[monoStack.top()] = i-monoStack.top();
            monoStack.pop();
        }
        monoStack.push(i);
    }

    return output;
}

在上面的程式碼中,data[monoStack.top()] < data[i] 保證一旦新元素比棧頂大,說明棧頂元素剛剛找到右側比它大的數,此時對應的output位置馬上就要更新。同時該棧頂元素也完成了任務,不能戀棧了,要馬上pop出來讓下面的元素跟這個新元素比試比試。如此反覆,直到while迴圈裡面條件不成立,說明棧已空,或新元素已經小於棧頂元素了 。

因為所有元素最多出棧入棧一次,相當於n個操作平攤在for迴圈中,所以複雜度還是O(n)。詳見演算法中的amortized analysis。

另外稍微回顧一下C++的內容。NextLarger()返回的是vector,這裡返回的時候會呼叫拷貝建構函式,所以雖然output是區域性變數,但不會出錯,因為返回的是區域性變數的拷貝。這裡返回值不可以加引用,vector &會導致直接返回區域性變數,但是函式結束時區域性變數已經被析構了。

這題稍微修改一下,就可以變成求左側更大數的距離問題(for迴圈倒過來,並且data[monoStack.top()]>data[i])。

例題2: Largest Rectangle in Histogram,給定一個直方圖,假定每個矩形寬度為1,求直方圖中能夠組成的所有矩形中,面積最大為多少。
簡單的例子:
input: 2,1,5,6,2,3
return: 10

容易看出面積最大的矩形為高度為5和6的直方圖組成的矩形,其面積為5 * 2 = 10。

解法1:這題實際上等價於:對每個矩形求左右最近的一個比他低的矩形的邊界,然後左右兩側距離相加(還要-1,因為自身算了2遍)×該矩形高度。然後找出所有矩形中該操作的最大值。 這樣我們前面例題1就可以馬上拿來用了。注意這裡是要求每個元素,左右兩側比它小的元素,所以要用遞增佇列 (data[monoStack.top()] > data[i])。

#include <iostream>
#include <stack>
#include <vector>
#include <map>

using namespace std;

//rightwards is TRUE, leftwards is FALSE
map<bool, vector<int> > dataMap;

void NextSmaller(vector<int> &data) {
    vector<int> toRight(data.size(), -1);
    vector<int> toLeft(data.size(), -1);
    dataMap[true] = toRight;
    dataMap[false] = toLeft;
    stack<int> monoToRightStack;
    stack<int> monoToLeftStack;

    for (int i=0; i<data.size(); ++i) {
        while(!monoToRightStack.empty() && data[monoToRightStack.top()]>data[i]) {
            dataMap[true][monoToRightStack.top()] = i-monoToRightStack.top();
            monoToRightStack.pop();
        }
        monoToRightStack.push(i);
    }

    for (int i=data.size()-1; i>=0; --i) {
        while(!monoToLeftStack.empty() && data[monoToLeftStack.top()]>data[i]) {
            dataMap[false][monoToLeftStack.top()] = monoToLeftStack.top() - i;
            monoToLeftStack.pop();
        }
        monoToLeftStack.push(i);
    }

    return;
}

int LargestRec1(vector<int> &data) {
    //add two dummy boundaries
    data.insert(data.begin(), -1);
    data.push_back(-1);
    NextSmaller(data);

    //cout<<"Rightwards"<<endl;
    //for (int i=0; i<data.size(); i++) {
    //    cout<<dataMap[true][i]<<" ";
    //}
    //cout<<endl;

    //cout<<"Leftwards"<<endl;
    //for (int i=0; i<data.size(); i++) {
    //    cout<<dataMap[false][i]<<" ";
    //}
    //cout<<endl;

    int maxV=0;
    int index=0;
    for (int i=0; i<data.size(); i++) {
        int tempV= heights[i]>0 ? data[i]*(dataMap[true][i]+dataMap[false][i]-1) : 0;
        if (maxV < tempV) {
           index = i;
        maxV = tempV;
        }
    }
    return maxV;
}

這裡dataMap[true][i]和dataMap[false][i]分別對應元素i往右和往左遇到最近的小於它的元素的距離。

注意上面是求左右兩側最近更小數的距離問題,所以是data[monoStack.top()]>data[i]。該不等式表面一旦新元素比棧頂元素小,說明棧頂元素已經找到一側最近更小數了,此時要馬上記錄下棧頂元素在output陣列中對應的距離,並pop棧頂陣列,如此反覆,直到棧空或新元素比棧頂元素大。

注意這題要特別注意的是邊界條件,即左右邊界特別大的情況。比如說input是 200,1,5,6,2,3 或 2,1,5,6,2,300,則output應該分別是200, 300。所以在LargestRec1()中特地在data[]的左右兩側加入兩個dummy -1,確保左右兩側會被考慮到。

另外回顧一下C++的內容。上面的例子中為了練習stl,用了map

    vector<int> toRight(data.size(), -1);
    dataMap[true] = toRight;

這裡dataMap[true] = toRight是將toRight陣列拷貝到dataMap[true]。所以dataMap[true]後來變了,toRight還是沒動。

解法2:
解法1向左向右各掃一遍,其實只需要掃一遍就可以了。

int LargestRec2(vector<int> &data) {
    stack<int> monoStack; //單調遞增棧
    int maxV = 0;

    //add two dummy boundaries
    data.insert(data.begin(), -1);
    data.push_back(-1);

    for (int i=0; i<data.size(); ++i) {
        while(!monoStack.empty() && data[monoStack.top()]>data[i]) {
            int oldTop = monoStack.top();
            monoStack.pop();
            maxV = max(maxV, data[oldTop]*(i-monoStack.top()-1));
        }
        monoStack.push(i);
    }

    return maxV;
}

int main()
{
    vector<int> data = {2,7,5,6,2,3};
    cout<<LargestRec2(data)<<endl;
    return 0;
}

注意:解法2的

data[oldTop]*(i-monoStack.top()-1)

是不是和解法1的
data[i]*(dataMap[true][i]+dataMap[false][i]-1)
很相似? 這裡實際上i-oldTop就是oldTop到右邊比它小的最近一個元素的距離,oldTop-monoStack.top()就是oldTop到左邊比它小的最近一個元素的距離。兩者相加要減一,因為oldTop本身算了2次。

以input為[2,7,5,6,2,3]為例,解法2步驟為:
0) maxV = 0。
1) 2(對應序號)入棧。
2) 2比7小,7(對應序號)入棧。
3) 5比7小,記下7的數值,7出棧。7*(2-0-1)=7。 這裡2和0分別是5和第1個2對應的序號,也就是7右側和左側最近的更小數的序號。maxV=7。注意:為簡便起見,這裡的序號沒有考慮dummy邊界。
4) 6比5大,6入棧;
5) 2比6小。記下6的數值,6出棧。6*(4-2-1)=6。這裡4和2分別是第2個2和5對應的序號,也就是6右側和左側最近的更小數的序號。maxV=7。
while迴圈繼續,2比5小,記下5的數值,5出棧,5*(4-0-1)=15。這裡4和0分別是第2個2和第一個2對應的序號,也就是5右側和左側最近的更小數的序號。maxV=15。
while迴圈繼續,(第1個)2不大於(第2個)2,所以第2個2入棧。
6) 2比3小。3入棧。
7) 這裡實際上還要考慮左右兩邊邊界的問題。在此兩邊邊界對應的maxV都小於15,所以對結果無影響。

再總結一下:為啥要用單調遞增棧呢?因為這樣可以保證棧內每個元素的下面一個元素(往棧bottom方向)就是該元素左側最近的更小數,當棧頂比新元素大時,新元素就是棧頂元素右側最近的更小數。這樣,棧頂元素的左右兩側最近的更小數都同時確定了。
所以,解法2和解法1是等價的,但更巧妙。