题目描述:
地上从左到右竖立着 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;
}