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

这篇具有很好参考价值的文章主要介绍了【学习笔记】单调队列和单调栈。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

单调栈

以这道题为例: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)\)文章来源地址https://www.toymoban.com/news/detail-612719.html

到了这里,关于【学习笔记】单调队列和单调栈的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 算法模板之单调栈和单调队列图文详解

    🌈个人主页: 聆风吟 🔥系列专栏: 算法模板、数据结构 🔖少年有梦不应止于心动,更要付诸行动。      💬 hello! 各位铁子们大家好哇,今天作者给大家带来了单调栈和单调队列的算法模板讲解,让我们一起加油进步。      📚 系列专栏:本期文章收录在《算法

    2024年02月02日
    浏览(46)
  • 蓝桥杯每日一题----单调栈和单调队列

    单调栈即栈内的元素是单调递减或者单调递增的,我们通过一个题目来理解。 单调栈模板题 题目描述 给出项数为 n 的整数数列 a 1 … a n a_1…a_n a 1 ​ … a n ​ 。 定义函数 f ( i ) f(i) f ( i ) 代表数列中第 i 个元素之后第一个大于 a i a_i a i ​ 的元素的下标,即 f ( i ) = m i n i

    2024年02月19日
    浏览(42)
  • 辅助栈、单调栈与单调队列在lc中的应用

    三者都有何区别? 个人建议 单调栈/队列 中存放的元素最好是下标而不是值,因为有的题目需要根据下标计算,这样泛化性更好。参考lc239和lc496 大概的思路是先把不满足单调性的元素poll掉,然后offer一个当前符合条件的元素。参考lc239和lc496 // 说明:这里使用两个堆来维护

    2024年02月14日
    浏览(37)
  • leetcode分类刷题:队列(Queue)(一、单调队列)

    单调队列,看起来是与单调栈对应起来的一样;但是做题的时候感觉单调队列不像单调栈一样,能根据题意自然形成 单调队列的基本实现 ,感觉单调队列更像是和某个队列对应起来的一样 1、 单调队列的经典题型 :使用双向队列维护窗口,窗口移动的元素增删与队列的先进

    2024年02月09日
    浏览(41)
  • 多重背包问题——单调队列优化

    我们在之前的文章中曾经讲解过多重背包问题,当时我们讲解了两种方法,一种方法就是三重循环,这种方法最为朴素好想。但是这种方法的时间复杂度非常高,后来我们想到了二进制优化的方式。那么今天我们将再介绍一种更好的优化方式——单调队列优化。 在讲解这种优

    2024年02月15日
    浏览(42)
  • 算法设计-单调队列

    这个东西我弄得还不是很明白,所以先放一篇博客:https://www.cnblogs.com/I-Love-You-520/p/13454305.html ​ 单调队列说的就是博客中提到的场景,可以说解决的是 固定长度,多次查询一段的最值 (其实就是slice window的意思),这种如果是朴素算法的话,因为查一段序列的最值的时间复

    2023年04月08日
    浏览(33)
  • 单调队列

    2024年02月19日
    浏览(29)
  • C - 滑动窗口 /【模板】单调队列

    Description 有一个长为 n 的序列 a,以及一个大小为 k 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。 例如: The array is [1,3,−1,−3,5,3,6,7] and k=3。 Input 输入一共有两行,第一行有两个正整数 n,k。 第二行 n 个整数

    2024年02月10日
    浏览(36)
  • 单调队列-滑动窗口最大值

    Problem: 239. 滑动窗口最大值 输入一个数组nums,滑动窗口k遍历该数组,输出得到的最大值数组; 示例1: 输入:nums = [1,3,-1,-3,5,3,6,7], k = 3 输出:[3,3,5,5,6,7] 构造一个单调队列表示当前窗口中单调递减的队列,队列的头就是最大值,为保证这个队列是窗口数据的表示,每次判断队

    2024年02月22日
    浏览(49)
  • 【二】一起算法---队列:STL queue、手写循环队列、双端队列和单调队列、优先队列

    纸上得来终觉浅,绝知此事要躬行。大家好!我是霜淮子,欢迎订阅我的专栏《算法系列》。 学习经典算法和经典代码,建立算法思维;大量编码让代码成为我们大脑的一部分。 ⭐️已更系列  1、基础数据结构        1.1、链表➡传送门        1.2、队列➡本章 专栏直达

    2023年04月08日
    浏览(54)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包