单调栈解木板倒水问题(单调栈的简单应用)

题目描述:

地上从左到右竖立着 n 块木板,从 1 到 n 依次编号,如下图所示。我们知道每块木板的高度,在第 n 块木板右侧竖立着一块高度无限大的木板,现对每块木板依次做如下的操作:对于第 i 块木板,我们从其右侧开始倒水,直到水的高度等于第 i 块木板的高度,倒入的水会淹没 ai 块木板(如果木板左右两侧水的高度大于等于木板高度即视为木板被淹没),求 n 次操作后,所有 ai 的和是多少。如图上所示,在第 4 块木板右侧倒水,可以淹没第 5 块和第 6 块一共 2 块木板,a4 = 2。


解析:

从对每块木板的操作:

对于第 i 块木板,我们从其右侧开始倒水,直到水的高度等于第 i 块木板的高度,倒入的水会淹没 ai 块木板(如果木板左右两侧水的高度大于等于木板高度即视为木板被淹没)

我们可以思考,要怎么样倒入的水才会停止"淹没" 木板呢?其实我们看图示就很清楚了,要使水不再往右延伸的话,那么必然要遇到一个比当前操作的木板更高的木板。

所以这道题转换成整型数组有n个元素,然后找到每个元素右边第一个大于该元素的数,并记录下标temp_id,那么第 i 块木板(下标为i)淹没木板的块数即为:

ai = temp_id - i - 1;    //从图示中即可计算出ai的值

如果这道题n的范围比较小的话,那么可以双重for循环直接暴力遍历即可,时间复杂度为O(n^2),但是我们仅仅就满足于这样的代码吗?有没有更好的办法能够解决这个问题?首先我们从转化后的题设入手,每个元素都要找到右边第一个大于该元素的数

那么还记得单调栈的性质3吗?单调栈的性质3:

使用单调栈可以找到元素向左遍历第一个比他小的元素,也可以找到元素向左遍历第一个比他大的元素。

对于这道题呢(考虑给定的测试用例),首先我们考虑从左到右依次将数组中的数据元素压入栈中,当遍历到第一个元素时,此时栈为空,所以第一个元素先要压入栈中,然后到调整部分,遍历到第二个元素,将第二个元素与栈顶相比较,如果当前遍历到的元素大于或者等于栈顶元素,栈顶元素找到了其右边第一个大于他的元素,那么这样的话,其实就已经解决了题设的问题,然后将ans(最后的结果)加上当前元素的下标 - 栈顶元素的下标 - 1,随后栈顶元素出栈(表示该栈顶元素已经处理完毕),然后重复上述过程,继续向左遍历,直到找到第一个比他大的元素,将其压入栈中,成为新的栈顶,停止遍历,处理下一个元素,而如果当前遍历到的元素小于栈顶元素,说明当前遍历到的元素无法再向左边延伸,因此将当前遍历到的元素压入栈中

然后继续向后遍历数组中的数据元素,并按照上述方式处理当前遍历到的元素以及栈顶元素。

每次操作结束之后,仔细观察已经压入栈中的元素,你会发现,从栈底到栈顶,木板的高度始终是单调递减的。

其实这就转化成了一道利用单调栈维护的题目了,即每块遍历到的木板寻找属于“自己”的位置(包含最后一块无限长的木板),而单调栈维护的时间复杂度为O(n),所以在数据范围比较大的情况下,也不用担心程序运行时间过长的问题。

图示分析:

图示中总共有n块木板,第n+1块是一块高度为无限大的木板,按照上述思路对a1-a6这六块木板进行处理。

一开始的时候,ans赋初始值为0.

1.遍历到a1时,此时栈为空,所以将a1压入栈中,此时的栈顶元素为a1;

2.然后继续向右遍历到a2时,a2 > a1,所以此时记录第1块木板淹没木板的块数sum1 = 2 - 1 - 1 = 0,然后ans += sum1,此时的ans = 0

然后a1出栈,此时栈为空,因此a2入栈,此时新的栈顶元素为a2

3.随后遍历至a3,a3 < a2,因此a3压入栈中,成为新的栈顶。此时新的栈顶元素为a3

4.随后遍历至a4,a4 > a3,所以此时记录第3块木板淹没木板的块数sum3 = 4 - 3 - 1 = 0,然后ans += sum3,此时的ans = 0

然后a3出栈,继续向左遍历,发现a2,而a2 > a4,找到了第一个比他大的元素,停止遍历。将a4压入栈中,此时新的栈顶元素为a4

5.随后遍历至a5,a5 < a4,因此a5压入栈中,成为新的栈顶。此时新的栈顶元素为a5

6.随后遍历至a6,a6 > a5,所以此时记录第5块木板淹没木板的块数sum5 = 6 - 5 - 1 = 0,然后ans += sum5,此时的ans = 0

然后a5出栈,继续向左遍历,发现a4,而a4 > a6,找到了第一个比他大的元素,停止遍历。将a6压入栈中,此时新的栈顶元素为a6

7.然后再继续向后遍历,发现了那块高度无限大的木板,说明此时遍历结束(当前遍历到了第n+1块木板)

那么,到现在处理完毕了吗?很明显是没有的,题设要求的是 n 次操作后,所有 ai 的和是多少,而当我们遍历结束的时候,只记录了其中三块木板淹没木板的块数,而栈中从栈底到栈顶依次还有a2,a4,a6(说明这三块木板均为处理),所以呢,在for循环遍历的语句之下,还要加上一个循环判定

此时栈是否为空,而如果栈不为空,而从栈底到栈顶,木板的高度始终是单调递减的。说明当前还在栈中的数据元素,右边第一个大于该元素的数都是第n+1个数,并且第n+1个元素的位置是“栈底”,所以我们只需要从栈顶开始一一处理剩下的元素即可,直到栈空,说明全部元素处理完毕。

所以接下来是处理剩下的元素,从栈顶开始,有sum6 = 6+1-6-1 = 0,ans += sum6,此时ans = 0,a6处理完毕,出栈;

然后sum4 = 6+1-4-1 = 2,ans += sum4,此时ans = 2,a4处理完毕,出栈;

最后是a2,sum2 = 6+1-2-1 = 4,ans += sum2,此时ans = 6,a2处理完毕,出栈;

循环判断此时栈空,跳出循环,截止到此时,所有元素处理完毕。

输出结果,ans = 6.

这道单调栈的试题需要注意的是,当遍历完数组中所有的数据元素时,也就是当前遍历到的数据元素到了第n+1个,就像上图图示所示的,第n+1块木板的高度是无限大的,那么这块木板是可以“延伸”到栈底的,所以当遍历结束之后,只需要利用这块木板一一处理剩下的元素即可。

完整代码实现:

#include
#include
using namespace std;
class Node {
public:
    int id, height;
};
template class Stack {
private:
    Type *urls;
    int max_size, top_index;
public:
    Stack(int length_input) {
        urls = new Type[length_input];
        max_size = length_input;
        top_index = -1;
    }
    ~Stack() {
        delete[] urls;
    }
    bool push(const Type &element) {
        if (top_index >= max_size - 1) {
            return false;
        }
        top_index++;
        urls[top_index] = element;
        return true;
    }
    bool pop() {
        if (top_index < 0) {
            return false;
        }
        top_index--;
        return true;
    }
    Type top() {
        assert(top_index >= 0);
        return urls[top_index];
    }
    bool empty() {
        if (top_index < 0) {
            return true;
        } else {
            return false;
        }
    }
};
int main() {
    int n,ans = 0;
    cin >> n;
    Stack  stack(n);
    Node temp;
    for(int i = 1;i <= n;i++){
        cin >> temp.height;
        temp.id = i;
        while(!stack.empty() && stack.top().height <= temp.height){
                ans = ans + i - stack.top().id - 1;
                stack.pop();
            }
            stack.push(temp);
    }
    while(!stack.empty()){
         ans = ans + n + 1 - stack.top().id - 1;
         stack.pop();
    }
    cout << ans << endl;
    return 0;
}

如有错误,还请指正,O(∩_∩)O谢谢


你可能感兴趣的:(数据结构——栈)