算法【单调队列】

2024-08-27 14:28
文章标签 算法 队列 单调

本文主要是介绍算法【单调队列】,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

注意,本文需要:

1.掌握用数组方式实现队列(常数时间比语言自己提供的好)。

2.掌握滑动窗口。

单调队列最经典的用法是解决如下问题:

滑动窗口在滑动时,r++代表右侧数字进窗口,l++代表左侧数字出窗口。这个过程中,想随时得到当前滑动窗口的最大值或者最小值。窗口滑动的过程中,单调队列所有调整的总代价为O(n),单次操作的均摊代价为O(1)。

注意:这是单调队列最经典的用法,可以解决很多题目。后面将继续介绍其他的用法。

下面通过几个题目加深对经典用法的理解。

题目一

测试链接:https://leetcode.cn/problems/sliding-window-maximum/

分析:这道题就是一个单调队列的模板。代码如下。

class Solution {
public:vector<int> maxSlidingWindow(vector<int>& nums, int k) {vector<int> ans;int length = nums.size();int number = length - k + 1;ans.assign(number, 0);int left = 0;int right = k;int l = 0;int r = 0;int size = 0;int deque[100005] = {0};for(int i = left;i < right;++i){while (size > 0 && nums[i] >= nums[deque[r-1]]){--r;--size;}deque[r++] = i;++size;}for(int i = 0;i < number;++i){ans[i] = nums[deque[l]];if(i == number-1){break;}++left;if(deque[l] < left){++l;--size;}while (size > 0 && nums[right] >= nums[deque[r-1]]){--r;--size;}deque[r++] = right;++size;++right;}return ans;}
};

其中,left和right是滑动窗口的左右,左开右闭;l和r是单调队列的头和尾,也是左开右闭;size为单调队列中元素个数;deque为单调队列。

题目二

测试链接:https://leetcode.cn/problems/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit/

分析:这道题就是通过单调队列找到窗口的最大值和最小值,然后作差和limit去比较。遍历数组,从而得到最长连续子数组的长度。代码如下。

class Solution {
public:int longestSubarray(vector<int>& nums, int limit) {int length = nums.size();int ans = 0;int deque_max[100003] = {0};int deque_min[100003] = {0};int left = 0;int right = 0;int l_max = 0;int r_max = 0;int size_max = 0;int l_min = 0;int r_min = 0;int size_min = 0;for(;left < length && right < length;){while (nums[deque_max[l_max]] - nums[deque_min[l_min]] <= limit && right < length){while (size_max > 0 && nums[right] >= nums[deque_max[r_max-1]]){--r_max;--size_max;}deque_max[r_max++] = right;++size_max;while (size_min > 0 && nums[right] <= nums[deque_min[r_min-1]]){--r_min;--size_min;}deque_min[r_min++] = right;++size_min;++right;}if(nums[deque_max[l_max]] - nums[deque_min[l_min]] > limit){ans = ans > right - left - 1 ? ans : right - left - 1;}else{ans = ans > right - left ? ans : right - left;}if(right == length){break;}++left;if(deque_max[l_max] < left){++l_max;--size_max;}if(deque_min[l_min] < left){++l_min;--size_min;}}return ans;}
};

其中,主要变量在上一题提及,这里不在赘述。窗口是以left为左边界向右能得到的最长满足条件的子数组。

题目三

测试链接:https://www.luogu.com.cn/problem/P2698

分析:这道题也是使用两个单调队列,得到窗口的最大值和最小值,和第二题类似。主要第三题采用了离散化,是因为给的点的数据不是连续的。代码如下。

#include <iostream>
#include <vector>
#include <algorithm>
#define MAXN 100002
using namespace std;
int N;
int D;
vector<vector<int>> pos;
int deque_max[MAXN] = {0};
int deque_min[MAXN] = {0};
int left = 0;
int right = 0;
int l_max = 0;
int r_max = 0;
int size_max = 0;
int l_min = 0;
int r_min = 0;
int size_min = 0;
class MyCompare
{
public:bool operator()(vector<int> v1, vector<int> v2){return v1[0] < v2[0];}
};
int main(void)
{int ans;int left = 0;int right = 0;scanf("%d%d", &N, &D);vector<int> temp;temp.assign(2, 0);pos.assign(N, temp);for (int i = 0; i < N; ++i){scanf("%d%d", &pos[i][0], &pos[i][1]);}sort(pos.begin(), pos.end(), MyCompare());ans = 1000005;for (; left < N && right < N;){while (pos[deque_max[l_max]][1] - pos[deque_min[l_min]][1] < D && right < N){while (size_max > 0 && pos[right][1] >= pos[deque_max[r_max - 1]][1]){--r_max;--size_max;}deque_max[r_max++] = right;++size_max;while (size_min > 0 && pos[right][1] <= pos[deque_min[r_min - 1]][1]){--r_min;--size_min;}deque_min[r_min++] = right;++size_min;++right;}if (pos[deque_max[l_max]][1] - pos[deque_min[l_min]][1] >= D){ans = ans < pos[right-1][0] - pos[left][0] ? ans : pos[right-1][0] - pos[left][0];}if (right == N){break;}++left;if (deque_max[l_max] < left){++l_max;--size_max;}if (deque_min[l_min] < left){++l_min;--size_min;}}printf("%d", ans == 1000005 ? -1 : ans);
}

其中,先对点的位置按x的值进行了排序,代码基本思路和第二题一致。

除了单调队列最经典的用法之外,在很多问题里单调队列还可以维持求解答案的可能性。

1.单调队列里的所有对象按照规定好的单调性来组织。

2.当某个对象从队尾进入单调队列时,会从队头或者队尾依次淘汰单调队列里,对后续求解答案没有帮助的对象。

3.每个对象一旦从单调队列弹出,可以结算此时这个对象参与的答案,随后这个对象不再参与后续求解答案的过程。

4.其实是先有对题目的分析!进而发现单调性,然后利用单调队列的特征去实现。

题目四

测试链接:https://leetcode.cn/problems/shortest-subarray-with-sum-at-least-k/

分析:看到此题,一个简单的思路是使用前缀和,利用双重for循环求解,不过从数据量可以看出这种思路会超时。所以不能单纯使用前缀和,要把前缀和和单调队列结合起来。首先得到前缀和数组,arr[i]表示前i和数的和,故arr[0] = 0 且arr的长度比nums的长度多1。单调队列的单调性是根据前缀和大压小,可以从两个方面考虑。一,根据求的前缀和数组中元素的单调顺序是从小到大。二,如果是小压大,假设单调队列中已经存在i,j,此时来了一个k,需要求以k为结尾满足条件的最长子数组,如果i和k满足条件,那么j和k也一定满足条件,因为arr[k]-arr[i] < arr[k]-arr[j],并且因为i小于j,所以i没有存在的意义,则小压大不行。主题思路是,每来到一个元素,先从头开始判断是否满足条件,满足则更新答案然后将满足的元素从头弹出,直到不满足为止,接着按照大压小进队列。遍历数组即可得到答案。代码如下。

class Solution {
public:int shortestSubarray(vector<int>& nums, int k) {int length = nums.size();int ans = length + 1;long long arr[100005] = {0};int deque[100005] = {0};int head = 0;int tail = 0;arr[0] = 0;for(int i = 1;i <= length;++i){arr[i] = arr[i-1] + nums[i-1];}for(int i = 0;i <= length;++i){while (head < tail && arr[i] - arr[deque[head]] >= k){ans = ans < i - deque[head] ? ans : i - deque[head];++head;}while (head < tail && arr[i] <= arr[deque[tail-1]]){--tail;}deque[tail++] = i;}return ans == length+1 ? -1 : ans;}
};

题目五

测试链接:https://leetcode.cn/problems/max-value-of-equation/

分析:将题中的yi + yj + |xi - xj|拆解开就是yi - xi + yj + xj,则对于来到的元素作结尾元素,想往前得到满足条件的最大值,由此可以看出单调队列的单调性是根据y-x的值小压大。则主体思路是,对于来到的元素,更新单调队列头部有可能过期的元素,然后和头元素计算值并更新答案,最后按照要求进队列。遍历数组即可得到答案。代码如下。

class Solution {
public:int findMaxValueOfEquation(vector<vector<int>>& points, int k) {int deque[100005] = {0};int ans = (1 << 31);int length = points.size();int head = 0;int tail = 0;for(int i = 0;i < length;++i){while (head < tail && points[i][0] - points[deque[head]][0] > k){++head;}if(head < tail){ans = ans > points[i][0] + points[i][1] + points[deque[head]][1] - points[deque[head]][0] ?ans : points[i][0] + points[i][1] + points[deque[head]][1] - points[deque[head]][0];}while (head < tail && points[i][1] - points[i][0] >= points[deque[tail-1]][1] - points[deque[tail-1]][0]){--tail;}deque[tail++] = i;}return ans;}
};

其中,过期条件就是两个x间隔超过k,由题中条件推出。

题目六

测试链接:https://leetcode.cn/problems/maximum-number-of-tasks-you-can-assign/

分析:看到此题,在可以得出答案范围的情况下,考虑二分答案法(之前的文章有详细讲解),故主题思路通过二分答案法尝试答案,通过f方法判断。f方法返回是否能完成nums个任务,这里有一个贪心,我们只看能不能完成nums个任务,故可以用前nums个力量大的工人去完成前nums个力量小的任务,由此也可以看出,单调队列的单调性是根据所需力量值大压小。f方法的主体思路是,对于每一个来到的工人,先更新其不吃药时能够解锁的任务,然后判断队列中是否有任务,如果有则做头任务(所需力量最小),如果队列中没有任务,则在吃药的情况下,更新其能够解锁的任务,然后做尾任务(所需力量最大),这是因为不能让药浪费。代码如下。

class Solution {
public:bool f(vector<int>& tasks, vector<int>& workers, int pills, int strength, int tasks_length, int workers_length, int nums){int deque[50005] = {0};int head = 0;int tail = 0;int index = 0;int did = 0;if(did == nums){return true;}for(int i = workers_length-nums;i < workers_length;++i){while (index < tasks_length && tasks[index] <= workers[i]){deque[tail++] = index;++index;}if(head < tail && workers[i] >= tasks[deque[head]]){++did;if(did == nums){return true;}++head;}else{if(--pills == -1){return false;}while (index < tasks_length && tasks[index] <= workers[i] + strength){deque[tail++] = index;++index;}if(head < tail && workers[i] + strength >= tasks[deque[tail-1]]){++did;if(did == nums){return true;}--tail;}}}return false;}int maxTaskAssign(vector<int>& tasks, vector<int>& workers, int pills, int strength) {int tasks_length = tasks.size();int workers_length = workers.size();sort(tasks.begin(), tasks.end());sort(workers.begin(), workers.end());int left = 0;int right = tasks_length < workers_length ? tasks_length : workers_length;int middle;int ans;while (left <= right){middle = (left + ((right - left) >> 1));if(f(tasks, workers, pills, strength, tasks_length, workers_length, middle)){left = middle + 1;ans = middle;}else{right = middle - 1;}}return ans;}
};

其中,index是待解锁任务的起始下标;did是已经做了多少任务。

这篇关于算法【单调队列】的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1111925

相关文章

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

康拓展开(hash算法中会用到)

康拓展开是一个全排列到一个自然数的双射(也就是某个全排列与某个自然数一一对应) 公式: X=a[n]*(n-1)!+a[n-1]*(n-2)!+...+a[i]*(i-1)!+...+a[1]*0! 其中,a[i]为整数,并且0<=a[i]<i,1<=i<=n。(a[i]在不同应用中的含义不同); 典型应用: 计算当前排列在所有由小到大全排列中的顺序,也就是说求当前排列是第

hdu1180(广搜+优先队列)

此题要求最少到达目标点T的最短时间,所以我选择了广度优先搜索,并且要用到优先队列。 另外此题注意点较多,比如说可以在某个点停留,我wa了好多两次,就是因为忽略了这一点,然后参考了大神的思想,然后经过反复修改才AC的 这是我的代码 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个

综合安防管理平台LntonAIServer视频监控汇聚抖动检测算法优势

LntonAIServer视频质量诊断功能中的抖动检测是一个专门针对视频稳定性进行分析的功能。抖动通常是指视频帧之间的不必要运动,这种运动可能是由于摄像机的移动、传输中的错误或编解码问题导致的。抖动检测对于确保视频内容的平滑性和观看体验至关重要。 优势 1. 提高图像质量 - 清晰度提升:减少抖动,提高图像的清晰度和细节表现力,使得监控画面更加真实可信。 - 细节增强:在低光条件下,抖

【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值 基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。 快速排序实现主框架: //快速排序 void QuickSort(int* arr, int left, int rig

poj 3974 and hdu 3068 最长回文串的O(n)解法(Manacher算法)

求一段字符串中的最长回文串。 因为数据量比较大,用原来的O(n^2)会爆。 小白上的O(n^2)解法代码:TLE啦~ #include<stdio.h>#include<string.h>const int Maxn = 1000000;char s[Maxn];int main(){char e[] = {"END"};while(scanf("%s", s) != EO

秋招最新大模型算法面试,熬夜都要肝完它

💥大家在面试大模型LLM这个板块的时候,不知道面试完会不会复盘、总结,做笔记的习惯,这份大模型算法岗面试八股笔记也帮助不少人拿到过offer ✨对于面试大模型算法工程师会有一定的帮助,都附有完整答案,熬夜也要看完,祝大家一臂之力 这份《大模型算法工程师面试题》已经上传CSDN,还有完整版的大模型 AI 学习资料,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

poj 3190 优先队列+贪心

题意: 有n头牛,分别给他们挤奶的时间。 然后每头牛挤奶的时候都要在一个stall里面,并且每个stall每次只能占用一头牛。 问最少需要多少个stall,并输出每头牛所在的stall。 e.g 样例: INPUT: 51 102 43 65 84 7 OUTPUT: 412324 HINT: Explanation of the s

poj 2431 poj 3253 优先队列的运用

poj 2431: 题意: 一条路起点为0, 终点为l。 卡车初始时在0点,并且有p升油,假设油箱无限大。 给n个加油站,每个加油站距离终点 l 距离为 x[i],可以加的油量为fuel[i]。 问最少加几次油可以到达终点,若不能到达,输出-1。 解析: 《挑战程序设计竞赛》: “在卡车开往终点的途中,只有在加油站才可以加油。但是,如果认为“在到达加油站i时,就获得了一