单调栈&单调队列学习笔记 write by taoyiwei17_cfynry days:2024.1.7

单调队列

定义

是指队列维护的元素单调、下标也单调的数据结构。

单调队列不像优先队列,是一种C++自带的STL,单调队列是用普通的队列进行维护的。

使用场景

滑动窗口。

在一个固定大小的窗口内寻找最值,且窗口从左到右移动。

滑动窗口实现方法

  1. 暴力: O ( n 2 ) O(n^2) O(n2)

  2. 线段树: O ( n × log ⁡ 2 n ) O(n \times \log_2{n}) O(n×log2n)

  3. RMQ: O ( n × log ⁡ 2 n ) O(n \times \log_2{n}) O(n×log2n)

  4. 单调队列: O ( n ) O(n) O(n)

显然,用单调队列维护时间复杂度最优。

单调队列实现步骤

  1. 循环枚举下标 i i i,从 1 1 1 n n n

  2. a i a_i ai 循环与队尾元素 a q t a i l a_{q_{tail}} aqtail 比较,并删除 ≤ a i \le a_i ai 的队尾, i i i 进队尾。

  3. 检查队头是否过期,并从队头删除过期下标。

  4. 输出队头元素,即为当前窗口最大值(具体看题目)。

时间复杂度分析:每个元素只出队入队一次,总时间复杂度为 O ( n ) O(n) O(n)

例题

First:P1886P2032

单调队列模板题。

系列难度:P2032 < < < P1886

放个P1886的代码

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5;
int a[N], q[N], n, k, head=1, tail;
void getmin()
{
	head=1, tail=0;
	for(int i=1;i<=n;i++)
	{
		while(head<=tail&&a[q[tail]]>=a[i])
		{
			tail--;
		}
		q[++tail]=i;
		while(head<=tail&&q[tail]-q[head]+1>k)
		{
			head++;
		}
		if(i>=k)cout<<a[q[head]]<<" ";
	}
	cout<<"\n";
}
void getmax()
{
	memset(q,0,sizeof(q));
	head=1, tail=0;
	for(int i=1;i<=n;i++)
	{
		while(head<=tail&&a[q[tail]]<=a[i])
		{
			tail--;
		}
		q[++tail]=i;
		while(head<=tail&&q[tail]-q[head]+1>k)
		{
			head++;
		}
		if(i>=k)cout<<a[q[head]]<<" ";
	}
	cout<<"\n";
}
signed main()
{
	cin>>n>>k;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
	}
	getmin();
	getmax();
}

Second:P1638P1440

稍微包装了一下的模板题。

系列难度:P1638 > > > P1440

Third:P1714

单调队列中套了一个简单算法,这次套的前缀和。

Fourth:P2698P2564

难度上升了不止一点。

P2698在二次单调队列中套了个二分。

难度都差不多,P2564有一些坑点,P2698难写一些。

P2698也有坑点,和单调队列窗口大小有关。

总结

单调队列虽然是线段树等数据结构中难度中下级的数据结构,但与其他算法结合起来难度还是不小的。

单调队列例题

讲个难点的,P2698

分析

  1. 由于每滴水以 1 1 1 单位时间下降,所以时间差等于高度差。

  2. 假设花盆宽度 w w w 已经确定,那么花盆可以从左到右滑动,转化为滑动窗口。

  3. 维护 2 2 2 个单调队列,维护最小和最大 y y y 值。

  4. 枚举花盆宽度可行但会TLE,宽度有单调性,一眼二分。

注意事项:

  1. 枚举将水滴按 x x x s o r t sort sort。(水滴比坐标小,枚举水滴常数低)

  2. 花盆宽度为 n n n 时,可接到 [ i , i + w ] [i,i+w] [i,i+w] 的水滴。

贴个 check 函数

bool check(int x)
{
	head=1,tail=0,head1=1,tail1=0;
	for(int i=1;i<=n;i++)
	{
		while(head<=tail&&a[i].y>=a[q[tail]].y)
		{
		    tail--;
		}
		q[++tail]=i;
		while(tail-head>=0&&a[q[tail]].x-a[q[head]].x+1>x)
		{
		    head++;
		}
		while(head1<=tail1&&a[i].y<=a[qq[tail1]].y)
		{
		    tail1--;
		}
		qq[++tail1]=i;
		while(tail1-head1>=0&&a[qq[tail1]].x-a[qq[head1]].x+1>x)
		{
		    head1++;
		}
		if(abs(a[q[head]].y-a[qq[head1]].y)>=m)
		{
			return 1;
		}
	}
	return 0;
}

单调栈

定义

一种下标单调、元素也单调的栈。

单调栈同单调队列不是一种C++自带的STL,单调栈是用普通的栈进行维护的。

使用场景

在若干区间内找最值,转化为枚举每个最值找区间。

寻找每个元素 a i a_i ai 向右(左)第一个比 a i a_i ai 大(小)的元素位置。

如何寻找 a i a_i ai 右边第一个大于 a i a_i ai 的位置?

  1. 枚举 i i i a i a_i ai s t k . t o p ( ) stk.top() stk.top() 循环比较,若 a i a_i ai 大于当前栈顶元素,弹出栈顶。

  2. i i i 入栈。

  3. 循环结束后,剩余栈中元素下标说明其右侧没有更大的元素。

如何寻找 a i a_i ai 左边第一个大于 a i a_i ai 的位置?

同上,倒序枚举 i i i 即可。

训练题单

  1. P5788 模板题

  2. P2947 跟模板题差不多,套了个背景。

  3. P2866 初学者建议去做,码量短,有少许思维难度。

  4. CF547B 做法多样,难度中等偏上少许,有一定思维难度,记得看讨论区的翻译。

  5. CF1299C 这题建议已开始学CSP-S相关内容的同学做,难度思维码量都有的。

模板代码

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e6+5;
int n, a[N], ans[N];

stack<int> stk;
signed main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		a[n+1]=1e9;
	}
	for(int i=1;i<=n;i++)
	{
		while(!stk.empty()&&a[i]>a[stk.top()])
		{
			ans[stk.top()]=i;
			stk.pop();
		}
		stk.push(i);
	}
	for(int i=1;i<=n;i++)
	{
		cout<<ans[i]<<" ";
	}
	return 0;
}
  • 51
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值