问题描述 ------最大矩形
给一个直方图,求直方图中的最大矩形的面积。例如,下面这个图片中直方图的高度从左到右分别是2, 1, 4, 5, 1, 3, 3, 他们的宽都是1,其中最大的矩形是阴影部分。
Input
输入包含多组数据。每组数据用一个整数n来表示直方图中小矩形的个数,你可以假定1 <= n <= 100000. 然后接下来n个整数h1, …, hn, 满足 0 <= hi <= 1000000000. 这些数字表示直方图中从左到右每个小矩形的高度,每个小矩形的宽度为1。 测试数据以0结尾。
Output
对于每组测试数据输出一行一个整数表示答案。
Sample Input
7 2 1 4 5 1 3 3
4 1000 1000 1000 1000
0
Sample Output
8
4000
解题思路以及关键代码
初始化
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]);//数据很多 用scanf速度快
memset(a_l+1,0,n); //将左侧第一个比它小的位置初始化为0
memset(a_r+1,n+1,n);//将右侧第一个比它小的位置初始化为n+1
a[0] = -1 , a[n+1] = -1; //为了将栈中元素全部弹出
维护一个单调栈,当 栈非空 并且 当前元素小于栈顶元素时,将栈顶元素依次弹出,并且对其右侧第一个比它小的位置 a_r[st.top()]=i;
for(int i=1;i<=n+1;i++)
{
while(!st.empty()&&a[st.top()]>a[i])
{
a_r[st.top()]=i;
st.pop();
}
if(st.empty()||a[st.top()]<=a[i])
//if 其实可以省略,因为while后一定满足该条件,此处为了方便理解
{
st.push(i);
}
}
再倒叙来一遍,相当于找左边能到达的最远区域,本质是一样的。
全部代码
#include<iostream>
#include<cstdio>
#include<stack>
#include<cstring>
using namespace std;
long long a[100005];
long long a_r[100005];
long long a_l[100005];
long long ans;
int main()
{
int n;
while(cin>>n&&n)
{
ans = 0;
stack<int> st;
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]);
memset(a_l+1,0,n);
memset(a_r+1,n+1,n);
a[0] = -1 , a[n+1] = -1; //为了将栈中元素全部弹出
for(int i=1;i<=n+1;i++)
{
while(!st.empty()&&a[st.top()]>a[i])
{
a_r[st.top()]=i;
st.pop();
}
if(st.empty()||a[st.top()]<=a[i])
{
st.push(i);
}
}
st.pop(); // a[n+1] = -1; //为了将栈中元素全部弹出 最后还在stack里面
for(int i=n;i>=0;i--)
{
while(!st.empty()&&a[st.top()]>a[i])
{
a_l[st.top()]=i;
st.pop();
}
if(st.empty()||a[st.top()]<=a[i])
{
st.push(i);
}
}
for(int i=1;i<=n;i++)
{
if(a[i]*(a_r[i]-a_l[i]-1)>ans)
//因为统计的是两侧第一个比它小的位置 所以面积=a[i]*(a_r[i]-a_l[i]-1)
ans=a[i]*(a_r[i]-a_l[i]-1);
}
cout<<ans<<endl;
}
}
问题描述--------滑动窗口
ZJM 有一个长度为 n 的数列和一个大小为 k 的窗口, 窗口可以在数列上来回移动. 现在 ZJM 想知道在窗口从左往右滑的时候,每次窗口内数的最大值和最小值分别是多少. 例如:
数列是 [1 3 -1 -3 5 3 6 7], 其中 k 等于 3.
Input
输入有两行。第一行两个整数n和k分别表示数列的长度和滑动窗口的大小,1<=k<=n<=1000000。第二行有n个整数表示ZJM的数列。
Output
输出有两行。第一行输出滑动窗口在从左到右的每个位置时,滑动窗口中的最小值。第二行是最大值。
Sample Input
8 3
1 3 -1 -3 5 3 6 7
Sample Output
-1 -3 -3 -3 3 3
3 3 5 5 6 7
解题思路以及关键代码
先顺序维护一个单调递增的双向队列(存储下标)。
首先将1~k-1号元素push进队列中并维护单增队列,如果队尾元素大于当前元素,则一直pop队尾元素直到不满足条件。
for(int i=1;i<=k-1;i++) //初始化
{
while(!q.empty()&&a[q.back()]>a[i])
{
q.pop_back();
}
if(q.empty()||a[q.back()]<=a[i])
{
q.push_back(i);
}
}
然后从k到n号元素开始,每次先维护队列的单调性,然后放入i号元素并且维护窗口的大小,具体做法为判断(i - 队首元素存储的下标)是否 > k,(因为队首元素一定是当前单调队列最先进来的元素)超过则pop队首元素 然后将队首元素存到a_min[i]中。
for(int i=k;i<=n;i++)//窗口从k开始向右移动 维护一个单增队列
{
while(!q.empty()&&a[q.back()]>a[i]) //先维护单调性
{
q.pop_back();
}
if(q.empty()||a[q.back()]<=a[i])
{
q.push_back(i);
}
while(!q.empty()&&(i-q.front())>=k) //再维护窗口的大小 保证此时队列中都落在窗口中
{
q.pop_front();
}
a_min[i]=q.front();
}
同理,维护一个单减队列,重复上述操作。(前提把deque清空 clear())
全部代码
#include<iostream>
#include<cstdio>
#include<queue>
using namespace std;
int a[1000005];
int a_min[1000005];
int a_max[1000005];
int main()
{
int n,k;
cin>>n>>k;
for(int i=1;i<=n;i++)
scanf("%d",&a[i]); //数据多 用scanf
deque<int> q; //储存下标
for(int i=1;i<=k-1;i++) //初始化
{
while(!q.empty()&&a[q.back()]>a[i])
{
q.pop_back();
}
if(q.empty()||a[q.back()]<=a[i])
{
q.push_back(i);
}
}
for(int i=k;i<=n;i++)//窗口从k开始向右移动 维护一个单增队列
{
while(!q.empty()&&a[q.back()]>a[i]) //先维护单调性
{
q.pop_back();
}
if(q.empty()||a[q.back()]<=a[i])
{
q.push_back(i);
}
while(!q.empty()&&(i-q.front())>=k) //再维护窗口的大小 保证此时队列中都落在窗口中
{
q.pop_front();
}
a_min[i]=q.front();
}
q.clear(); //deque 可以清空队列
for(int i=1;i<=k-1;i++)
{
while(!q.empty()&&a[q.back()]<a[i])
{
q.pop_back();
}
if(q.empty()||a[q.back()]>=a[i])
{
q.push_back(i);
}
}
for(int i=k;i<=n;i++)//窗口从k开始向右移动 维护一个单增队列
{
while(!q.empty()&&a[q.back()]<a[i]) //先维护单调性
{
q.pop_back();
}
if(q.empty()||a[q.back()]>=a[i])
{
q.push_back(i);
}
while(!q.empty()&&(i-q.front())>=k) //然后维护窗口的大小 保证此时队列中都落在窗口中
{
q.pop_front();
}
a_max[i]=q.front();
}
for(int i=k;i<=n;i++)
cout<<a[a_min[i]]<<" ";
cout<<endl;
for(int i=k;i<=n;i++)
cout<<a[a_max[i]]<<" ";
cout<<endl;
return 0;
}
关于—单调栈以及单调队列的总结
单调队列的维护过程与单调栈相似
•区别在于
• 单调栈只维护一端(栈顶), 而单调队列可以维护两端(队首和队尾)
• 单调栈通常维护 全局 的单调性, 而单调队列通常维护 局部 的单调性
• 单调栈大小没有上限, 而单调队列通常有大小限制
(由于单调队列 可以队首出队 以及 前面的元素一定比后面的元素先入队 的
性质,使得它可以维护局部的单调性,当队首元素不在区间之内则可以出队)
关于复杂度,遍历一遍,每个元素只访问一遍,所以是O(n)