单调栈&单调队列学习笔记 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:P1886、P2032

单调队列模板题。

系列难度:P2032 < < < P1886

放个P1886的代码

cpp 复制代码
#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:P1638、P1440

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

系列难度:P1638 > > > P1440

Third:P1714

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

Fourth:P2698、P2564

难度上升了不止一点。

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 函数

cpp 复制代码
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相关内容的同学做,难度思维码量都有的。

模板代码

cpp 复制代码
#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;
}
相关推荐
piikee2 分钟前
粤语初学入门:从粤拼、九声六调到开口,一个普通话母语者的练习笔记
笔记·粤语入门·怎么学习粤语
sunshine22 girl8 分钟前
Java学习七 Java 项目实战1-项目创建和主页面搭建
java·开发语言·学习
传奇开心果编程1 小时前
【SwiftUI提高练中学】第8课 网络层架构:拦截器、重试、缓存与离线优先
学习·macos·ui·ios·swiftui
一条破秋裤1 小时前
内核驱动的意义与课程学习规划
学习
念何架构之路2 小时前
zap采样器与性能优化内幕
算法·性能优化·哈希算法
Bug收容所2 小时前
学习LangChain day1
学习·langchain·llm·agent
传奇开心果编程2 小时前
【现代声明式UI学与练】第7课 组件通信——父传子、子传父、跨层级通信、状态提升与 Context
学习·flutter·react native·ui·swiftui·android jetpack
晒太羊的猫2 小时前
hot100动态规划刷题
算法·动态规划
Seraphina362 小时前
SQL注入(二)盲注
网络·数据库·经验分享·笔记·sql
白山编程大哥3 小时前
Carsim 2024.1 专业安装介绍:功能特性与应用场景详解
笔记