单调栈解木板倒水问题(单调栈的简单应用)
2016-02-26 23:45
316 查看
题目描述:
地上从左到右竖立着 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,随后栈顶元素出栈(表示该栈顶元素已经处理完毕),然后重复上述过程,继续向左遍历,直到找到第一个比他大的元素,将其压入栈中,成为新的栈顶,停止遍历,处理下一个元素,而如果当前遍历到的元素小于栈顶元素,说明当前遍历到的元素无法再向左边延伸,因此将当前遍历到的元素压入栈中。
[b][b]然后继续向后遍历数组中的数据元素,并按照上述方式处理当前遍历到的元素以及栈顶元素。[/b][/b]
[b]每次操作结束之后,仔细观察已经压入栈中的元素,你会发现,从栈底到栈顶,木板的高度始终是单调递减的。[/b]
其实这就转化成了一道利用单调栈维护的题目了,即每块遍历到的木板寻找属于“自己”的位置(包含最后一块无限长的木板),而单调栈维护的时间复杂度为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,找到了第一个比他大的元素,停止遍历。[b]将a4压入栈中,[/b]此时新的栈顶元素为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个元素的位置是“栈底”,所以我们只需要从栈顶开始一一处理剩下的元素即可,直到栈空,说明全部元素处理完毕。
[b]所以接下来是处理剩下的元素,从栈顶开始,有sum6
= 6+1-6-1 = 0,ans += sum6,此时ans = 0,a6处理完毕,出栈;[/b]
然后[b]sum4 = 6+1-4-1 = 2,ans += sum4,此时ans = 2,[b]a4处理完毕,出栈;[/b][/b]
[b]最后是a2,sum2
= 6+1-2-1 = 4,ans += sum2,此时ans = 6,a2处理完毕,出栈;[/b]
循环判断此时栈空,跳出循环,截止到此时,所有元素处理完毕。
输出结果,ans = 6.
这道单调栈的试题需要注意的是,当遍历完数组中所有的数据元素时,也就是当前遍历到的数据元素到了第n+1个,就像上图图示所示的,第n+1块木板的高度是无限大的,那么这块木板是可以“延伸”到栈底的,所以当遍历结束之后,只需要利用这块木板一一处理剩下的元素即可。
完整代码实现:
如有错误,还请指正,O(∩_∩)O谢谢
地上从左到右竖立着 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,随后栈顶元素出栈(表示该栈顶元素已经处理完毕),然后重复上述过程,继续向左遍历,直到找到第一个比他大的元素,将其压入栈中,成为新的栈顶,停止遍历,处理下一个元素,而如果当前遍历到的元素小于栈顶元素,说明当前遍历到的元素无法再向左边延伸,因此将当前遍历到的元素压入栈中。
[b][b]然后继续向后遍历数组中的数据元素,并按照上述方式处理当前遍历到的元素以及栈顶元素。[/b][/b]
[b]每次操作结束之后,仔细观察已经压入栈中的元素,你会发现,从栈底到栈顶,木板的高度始终是单调递减的。[/b]
其实这就转化成了一道利用单调栈维护的题目了,即每块遍历到的木板寻找属于“自己”的位置(包含最后一块无限长的木板),而单调栈维护的时间复杂度为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,找到了第一个比他大的元素,停止遍历。[b]将a4压入栈中,[/b]此时新的栈顶元素为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个元素的位置是“栈底”,所以我们只需要从栈顶开始一一处理剩下的元素即可,直到栈空,说明全部元素处理完毕。
[b]所以接下来是处理剩下的元素,从栈顶开始,有sum6
= 6+1-6-1 = 0,ans += sum6,此时ans = 0,a6处理完毕,出栈;[/b]
然后[b]sum4 = 6+1-4-1 = 2,ans += sum4,此时ans = 2,[b]a4处理完毕,出栈;[/b][/b]
[b]最后是a2,sum2
= 6+1-2-1 = 4,ans += sum2,此时ans = 6,a2处理完毕,出栈;[/b]
循环判断此时栈空,跳出循环,截止到此时,所有元素处理完毕。
输出结果,ans = 6.
这道单调栈的试题需要注意的是,当遍历完数组中所有的数据元素时,也就是当前遍历到的数据元素到了第n+1个,就像上图图示所示的,第n+1块木板的高度是无限大的,那么这块木板是可以“延伸”到栈底的,所以当遍历结束之后,只需要利用这块木板一一处理剩下的元素即可。
完整代码实现:
#include<iostream> #include<cassert> using namespace std; class Node { public: int id, height; }; template<class Type> 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 <Node> 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谢谢
相关文章推荐
- Atitit.为什么小公司也要做高大上开源项目
- erb去掉末尾换行 -%>使用方法
- Atitit .c#的未来新特性计划草案
- 学习IT中的一些编程课程推荐
- Atitit .c#的未来新特性计划草案
- Atitit .c#的未来新特性计划草案
- 笨办法学python_学习笔记2
- 获得元素相对于当前窗口的上下左右位置
- srand,rand函数的使用注意事项
- 终于上传了一篇文章
- Introduction to the POM
- memcpy和memmove() 实现和区别
- memory order
- atitit.groovy 语法特性
- 2016/2/26 <marquee></marquee>实现多种滚动效果
- atitit.groovy 语法特性
- atitit.groovy 语法特性
- 一个很详细的web.xml配置文件详解
- 字符串处理之gets与scanf("%s")的区别
- Android 第一天重置版的一些技巧