1.引入:
先看下面这道题: 单调队列模板
题目描述
有一个长为 n 的序列 a,以及一个大小为 k 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。
输入格式
输入一共有两行,第一行有两个正整数 n,k。 第二行 n个整数,表示序列 a
输出格式
输出共两行,第一行为每次窗口滑动的最小值
第二行为每次窗口滑动的最大值
输入输出样例
输入 #1 输出 #1
8 3 -1 -3 -3 -3 3 3 3 3 5 5 6 7 1 3 -1 -3 5 3 6 7
说明/提示
【数据范围】
对于 50% 的数据,1≤n≤10^5;
对于 100%的数据,1≤k≤n≤10^6,ai∈[−2^31,2^31)。
2.解题思路
2.1暴力
对于这道题,很容易就想到暴力方法。对于每一个区间都进行一次遍历,求出最大值和最小值。编码也很容易。
但是时间复杂度O(nk),太高肯定只能拿部分分。
2.2 暴力的些许优化
有人可能会想到,新加入窗口的值可以拿他跟原来的最值比较,丢弃的值也拿他跟原来的最值比较,就能更新最值了。
但是如果被丢弃的是最值,那么你还得找到最值,优化的并不多
2.3 单调队列登场
单调队列是一种特殊的队列数据结构,它具有以下特点:
- 队列中的元素是有序的,按照某种规则进行排序,例如从小到大或从大到小。
- 队列中的元素在入队和出队操作时,保持有序性。
- 入队操作时,新元素会在队列中找到合适的位置插入,并确保队列中的元素继续保持有序。
- 在出队操作时,队列中的元素按照一定的规则进行出队,保证队列中最前面的元素是最大或最小的。
- 单调队列可以高效地解决一些特定的问题,例如滑动窗口最大值、最小值等问题。
在实现单调队列时,一般会使用双端队列作为底层数据结构,在入队和出队操作时,可以通过比较队列末尾元素和新元素的大小,来确定入队和出队的位置,从而保持队列的有序性。
2.4 举个例子
下面是一个简单的例子来说明单调队列的使用:
假设数组 nums 为 [1, 3, -1, -3, 5, 3, 6, 7],滑动窗口的大小 k 为 3。
- 初始化一个空的单调队列和一个空的结果数组 result。
- 对于第一个元素 1,单调队列为空,直接将其入队。
- 对于第二个元素 3,单调队列中的队尾元素 1 小于等于 3,将其出队,将 3 入队。
- 对于第三个元素 -1,单调队列中的队尾元素 3 大于 -1,不需要出队,将 -1 入队。
- 对于第四个元素 -3,单调队列中的队尾元素 -1 大于 -3,不需要出队,将 -3 入队。
- 对于第五个元素 5,单调队列中的队尾元素 -3 小于等于 5,将其出队,将 5 入队。滑动窗口的大小已经达到 3,将单调队列的队首元素 3 添加到结果数组中。
- 对于第六个元素 3,单调队列中的队尾元素 5 大于 3,不需要出队,将 3 入队。将单调队列的队首元素 5 添加到结果数组中。
- 对于第七个元素 6,单调队列中的队尾元素 3 小于等于 6,将其出队,将 6 入队。将单调队列的队首元素 6 添加到结果数组中。
- 对于第八个元素 7,单调队列中的队尾元素 6 小于等于 7,将其出队,将 7 入队。将单调队列的队首元素 7 添加到结果数组中。
所以,最终的结果数组为 [3, 3, 5, 5, 6, 7],即滑动窗口内的最大值。
最小值也是一样的
3.代码如下
#include<bits/stdc++.h>
using namespace std;
int a[1000002];
deque<int>q;
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++){//最小值
while(!q.empty() and a[q.back()]>a[i]) q.pop_back();//去尾
q.push_back(i);
if(i>=m){
while(!q.empty() and q.front()<=i-m)q.pop_front();//删头
printf("%d ",a[q.front()]);
}
}
cout<<endl;
while(!q.empty())//清空
q.pop_front();
for(int i=1;i<=n;i++){//最大值
while(!q.empty() and a[q.back()]<a[i]) q.pop_back();//去尾
q.push_back(i);
if(i>=m){
while(!q.empty() and q.front()<=i-m)q.pop_front();//删头
printf("%d ",a[q.front()]);
}
}
return 0;
}
4.单调栈的引入
先看下面这道题: 单调栈模板
题目描述
农夫约翰有N头奶牛正在过乱头发节。
每一头牛都站在同一排面朝右,它们被从左到右依次编号为 1,2,⋯ ,N。编号为 i 的牛身高为 hi。第 N 头牛在最前面,而第 1 头牛在最后面。
对于第 i 头牛前面的第 j 头牛,如果 hi>hi+1,hi>hi+2,⋯ ,hi>hj,那么认为第 i 头牛可以看到第 i+1 到第 j 头牛。
定义 Ci 为第 i 头牛所能看到的牛的数量。请帮助农夫约翰求出 C1+C2+⋯+CN
输入格式
输入共 N+1行。
第一行为一个整数 N,代表牛的个数。
接下来 N 行,每行一个整数 ai,分别代表第 1,2,⋯ ,N 头牛的身高。
输出格式
输出共一行一个整数,代表 C1+C2+⋯+CN。
输入输出样例
输入 #1 输出 #1
6 5 10 3 7 4 12 2
说明/提示
数据规模与约定
对于 100% 的数据,保证 1≤N≤8×10^4,1≤hi≤10^9。
5.解题思路
5.1暴力
我还是那句话。
如果你只想拿部分分,可以开暴力。
如果你想AC,不能开暴力
5.2单调栈
单调栈是一种数据结构,它可以在O(n)的时间复杂度内解决一类特殊的问题。 单调栈是一个栈,但是它只能从栈顶部进行数据的插入和删除操作,并且要保持栈中元素的单调性。
单调栈有两种类型:单调递增栈和单调递减栈。
单调递增栈是指栈中的元素从栈底到栈顶是递增的,也就是说栈底的元素是最小的。
单调递减栈是指栈中的元素从栈底到栈顶是递减的,也就是说栈底的元素是最大的。
5.3 举个例子
假设我们有一个数组arr=[3, 4, 2, 1, 5, 6],我们想找到每个元素右边第一个比它大的元素的索引。
我们可以使用单调栈来解决这个问题。单调栈是指栈中的元素按照某种顺序排列,例如从小到大或从大到小。在本例中,我们将使用一个单调递减的栈。
我们首先创建一个空栈和一个结果数组。
第一步,我们将第一个元素的索引0压入栈中。
接下来,我们依次遍历数组中的每个元素。
对于每个元素,我们将其索引压入栈中,并与栈顶索引对应的元素进行比较。
如果当前元素大于栈顶索引对应的元素,说明栈顶索引对应的元素找到了右边第一个比它大的元素。我们可以把栈顶索引弹出栈,并在结果数组中记录该元素的索引为栈顶索引对应的元素的值。
如果当前元素小于等于栈顶索引对应的元素,我们继续将当前元素的索引压入栈。
重复以上步骤,直到遍历完整个数组。
最后,栈中剩余的索引对应的元素都没有右边比它们大的元素。我们可以将它们在结果数组中的值设为-1。
根据上述步骤,我们可以得到结果数组为[-1, 2, -1, -1, 5, -1]。
6.代码如下
#include<bits/stdc++.h>
using namespace std;
int a[80005],top, n;
long long ans;//十年OI一场空,不开long long见祖宗
int main(){
scanf("%d",&n);
for (int i=1;i<=n;i++){
int x;
scanf("%d",&x);
while (top && x>=a[top])top--;
ans+=top;
a[++top]=x;
}
cout<<ans;
return 0;
}
再见