首页 > 其他分享 >【学习笔记】单调队列和单调栈

【学习笔记】单调队列和单调栈

时间:2023-07-25 22:24:40浏览次数:40  
标签:队列 最大值 元素 笔记 int 单调 dp

单调栈

以这道题为例:P5788。我们考虑维护一个单调栈,里面存的是下标,使里面的下标对应的元素从栈顶到栈底是单调上升的。

  • 我们从 \(n\rightarrow 1\) 枚举 \(a_i\)
  • 对于每个 \(i\),如果栈非空,令栈顶的下标为 \(j\),若 \(a_j\) 不比 \(a_i\) 大,那么这个栈顶元素由于值又小,位置又靠后,如果 \(j\) 能满足的条件,\(i\) 也一定能满足,而且 \(i\) 适用范围更广,所以 \(j\) 不可能成为之后的的 \(f_i\),所以就将这个元素弹出。
  • 重复以上操作直至 \(a_j > a_i\),此时的栈顶就是 \(f_i\)。其实这也能解释为什么不符合元素的栈顶要出栈:如果不出的话它作为 \(f_i\) 就不满足条件了。这时再把 \(i\) 压入栈中。

其实这个的原理在生活中就已经有了:某个人比你小,又比你厉害,你就永远也找不过他了(一般情况下)。这便是单调栈的基本做法了。

考虑时间复杂度:由于每个元素最多进栈出栈一次,所以时间复杂度应该是 \(O(n)\)。

这道题的代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 3000005;
int a[N], f[N];
stack<int>p;
int main()
{	
    int n;
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) 
		scanf("%d", &a[i]);
	for (int i = n; i >= 1; i--)
	{
		while (!p.empty() && a[p.top()] <= a[i]) p.pop();
		if (!p.empty()) f[i] = p.top();
		else f[i] = 0;
		p.push(i); 
	}
	for (int i = 1; i <= n; i++)
	    printf("%d ", f[i]);
	return 0;
}

写代码要注意的几点:

  • 枚举栈顶时得用 while 而不是 if
  • 如果 \(f_i\) 不存在时栈为空,得特判并将 \(f_i\) 赋值为 \(0\)

其实,单调栈的运用不止如此,我们知道,\(f_i\) 为在第 \(i\) 个元素之后第一个大于 \(a_i\) 的元素下标,也就是说,在 \([i,f_i-1]\) 这个区间内的最大值为 \(a_i\),同理,我们令 \(g_i\) 为在 \(i\) 之前最后大于 \(a_i\) 的下标,则 \([g_i+1,i]\) 区间的最大值也为 \(i\),这样可得:\([g_i+1,f_i-1]\) 是以 \(a_i\) 为最大值的最长区间。所以,单调栈解决的问题是:求以 \(x\) 为最值的最长区间。

单调队列

接下来来看单调队列。同样有一道例题:P1886。先考虑维护一个队列(若求最大值则从大到小,若求最小值则从小到大),队尾元素同单调栈类似处理,而最值是在队首。那么特定长度又怎么维护呢?

可以直到,对于一个队首,如果它是在区间外的,则不能作为最值,得弹出,这种操作是可以用双端队列 deque 来实现的。但注意,deque 的常数大,尤其是在未开 O2 的情况下,所以对于时间要求较紧的题目慎用。具体步骤如下:

  • 从 \(1\rightarrow n\) 枚举 \(i\)
  • 对于每个 \(i\),在栈非空的情况下重复进行以下操作直到不满足条件(以最大值为例,最小值同理):
  1. 令队尾元素为 \(j\),若 \(a_j \le a_i\) 则弹出 \(j\)
  2. 令队首元素为 \(j'\),若 \(j'<i-k+1\)(超出范围)则弹出 \(j'\)
  • 将 \(i\) 从末尾进队,令队首元素为 \(s\),此时 \([i-k+1,i]\) 区间的最大值就是 \(a_{s}\)。

代码如下(deque 实现):

#include <bits/stdc++.h>
using namespace std;
namespace {
	const int N = 1000005;
	int a[N];
	deque <int> p, q;
	int f1[N], f2[N];
	void main()
	{
		int n, k;
		cin >> n >> k;
		for (int i = 1; i <= n; i++)
			cin >> a[i];
		for (int i = 1; i <= n; i++)
		{
			while (!p.empty() && p.front() < i - k + 1) p.pop_front();
			while (!p.empty() && a[p.back()] <= a[i]) p.pop_back();
			p.push_back(i);
			f1[i] = a[p.front()];
			while (!q.empty() && q.front() < i - k + 1) q.pop_front();
			while (!q.empty() && a[q.back()] >= a[i]) q.pop_back();
			q.push_back(i);
			f2[i] = a[q.front()];
		}
		for (int i = k; i <= n; i++) cout << f2[i] << " ";
		cout << endl;
		for (int i = k; i <= n; i++) cout << f1[i] << " ";
	}
}

int main()
{
	return 0;
}

注意事项:

  • 同样得用 while 而不是 if
  • 在记录 \(f_i\) 时得先将 \(i\) 入队

单调队列优化dp

有的时候,dp 的复杂度太高了,在一些特殊的 dp 时,我们就可以用单调队列来优化它。

对于这样的 dp(或者是取 min):

\[dp_i = max\{dp_j\}(max\{1,i-k\}\le j < i)+a_i \]

便可以用单调队列优化。

正常情况下,我们需要用两重循环,一重枚举 \(i\),一重枚举 \(j\) 来进行转移,复杂度是 \(O(n^2)\)。但观察到,对于每个 \(i\),我们发现,它的转移范围是一个长度为 \(k\) 的区间。这是不是很眼熟?没错,这不就和单调队列的模板题几乎一样吗?

我们在转移 \(dp_i\) 的时候,可以维护一个 \([i-k,i-1]\) 的单调队列记录 \([i-k,i-1]\) 区间内的最大值(最小值),直接进行转移即可。这样的时间复杂度就会大大减小,是 \(O(n)\)。

标签:队列,最大值,元素,笔记,int,单调,dp
From: https://www.cnblogs.com/zhuoyuxuan/p/17581197.html

相关文章

  • 【学习笔记】并查集
    先来看百度百科上的定义:并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。并查集是一种树型的数据结构,用于处理一些不相交集合(disjointsets)的合......
  • 7.24 树上问题2笔记
    题单T1题目•有一棵点数为\(n\)的树。•有\(q\)次询问,每次询问有多少个点到\(a,b\)距离相等。•\(1≤n\),\(q≤500000\)。Solution•设询问\(a,b\)两点直接的路长度为\(d\)。•如果\(d\)为奇数,那么无解,\(d\)为偶数有解。•考虑以下几种情况:\(......
  • 7.23 树上问题笔记
    题单由于题目过多,只放几道重要的。。。T1题目•A国有\(n\)座城市,编号从\(1\)到\(n\),城市之间有\(m\)条双向道路。每一条道路对车辆都有重量限制\(z\),简称限重。•现在有\(q\)辆货车在运输货物,司机们想知道每辆车在不超过车辆限重的情况下,最多能运多重的货物。......
  • 【学习笔记】无向图的连通性
    #割点**定义:**在无向图连通图中,若把点$x$删去后整个图就不连通了,则$x$为割点(割顶)。**朴素方法:**每次删去一个点,然后判断图是否连通,时间复杂度为$O(n(n+m))$。**Tarjan算法:**$dfn_x$:$x$被`dfs`到的时间戳$low_x$:在$x$及以后被搜索的所有节点的$low$值和这些节......
  • 笔记-交易圣经
    笔记-交易圣经通用原则一:思想准备最大逆境:没有最坏,只有更坏情绪指向:目标与期望失利:失败是必然随机市场:不确定性,不可预测性输得起才会赢:生存是第一要义风险管理:如上交易伙伴:防止自欺欺人财务边界:闲钱通用原则二:启蒙避免爆仓风险:有可能即是迟早会发生。没有杠杆减......
  • Numpy学习笔记之Numpy练习
    练习1:分别按照要求,生成一个一维数组、二维数组,并且查看其shapea1=np.array([1,2,'a','hello',[1,2,3],{'one':100,'two':200}])a2=np.array([list(range(6)),list('abcdef'),[True,False,True,False,True,True]])print(a1,'......
  • 2023长郡集训 动态规划笔记
    动态规划原理何为动态规划?动态规划(\(\text{Dynamicprogramming}\)),简称DP。DP并不是一种算法,与模拟、贪心一样,而是一种解决问题的方式。DP的基本思想为「将给定的问题拆分为一个个规模更小的子问题,直到子问题可以直接解决,返回/保存这个值,再根据方程一步步推出原本问题的答......
  • python教程 入门学习笔记 第1天
    初识python一、python语言简介:1、起源:1989年由荷兰的前谷歌程序员吉多.范罗苏姆(龟叔)创造,python的命名来源于英国电视喜剧MontyPython’sFlyingCircus飞行马戏团2、优势:python、Java、c这几种是世界最流行语言;用途广泛,被称为万能语言;语法简洁,上手简单;例如:print("hellowor......
  • typescripts学习笔记(三)
    typescripts学习笔记(三)-实现过程引言Typescript是一种由微软开发的开源编程语言,它是JavaScript的超集,可以在任何支持JavaScript的环境中运行。本篇文章将教你如何使用Typescript来创建一个简单的学习笔记应用。整体流程下面是整个实现过程的流程图:步骤描述步骤1......
  • [c/c++][考研复习笔记]内部排序篇学习笔记
    考研排序复习笔记插入排序#include<stdio.h>#include<stdlib.h>#defineMaxSize9//折半插入排序voidZBInsertSort(intA[],intn){ inti,j,high,low,mid; for(i=2;i<=n;i++){ A[0]=A[i]; low=1;high=i-1; while(low<=high){ mid=(low+high)/2......