单调队列和单调栈的基础教程

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 单调队列登场

单调队列是一种特殊的队列数据结构,它具有以下特点:

  1. 队列中的元素是有序的,按照某种规则进行排序,例如从小到大或从大到小。
  2. 队列中的元素在入队和出队操作时,保持有序性。
  3. 入队操作时,新元素会在队列中找到合适的位置插入,并确保队列中的元素继续保持有序。
  4. 在出队操作时,队列中的元素按照一定的规则进行出队,保证队列中最前面的元素是最大或最小的。
  5. 单调队列可以高效地解决一些特定的问题,例如滑动窗口最大值、最小值等问题。

在实现单调队列时,一般会使用双端队列作为底层数据结构,在入队和出队操作时,可以通过比较队列末尾元素和新元素的大小,来确定入队和出队的位置,从而保持队列的有序性。

 2.4 举个例子

下面是一个简单的例子来说明单调队列的使用:

假设数组 nums 为 [1, 3, -1, -3, 5, 3, 6, 7],滑动窗口的大小 k 为 3。

  1. 初始化一个空的单调队列和一个空的结果数组 result。
  2. 对于第一个元素 1,单调队列为空,直接将其入队。
  3. 对于第二个元素 3,单调队列中的队尾元素 1 小于等于 3,将其出队,将 3 入队。
  4. 对于第三个元素 -1,单调队列中的队尾元素 3 大于 -1,不需要出队,将 -1 入队。
  5. 对于第四个元素 -3,单调队列中的队尾元素 -1 大于 -3,不需要出队,将 -3 入队。
  6. 对于第五个元素 5,单调队列中的队尾元素 -3 小于等于 5,将其出队,将 5 入队。滑动窗口的大小已经达到 3,将单调队列的队首元素 3 添加到结果数组中。
  7. 对于第六个元素 3,单调队列中的队尾元素 5 大于 3,不需要出队,将 3 入队。将单调队列的队首元素 5 添加到结果数组中。
  8. 对于第七个元素 6,单调队列中的队尾元素 3 小于等于 6,将其出队,将 6 入队。将单调队列的队首元素 6 添加到结果数组中。
  9. 对于第八个元素 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;
}

再见

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值