算法【单调队列】

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

相关文章

openCV中KNN算法的实现

《openCV中KNN算法的实现》KNN算法是一种简单且常用的分类算法,本文主要介绍了openCV中KNN算法的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的... 目录KNN算法流程使用OpenCV实现KNNOpenCV 是一个开源的跨平台计算机视觉库,它提供了各

SpringKafka错误处理(重试机制与死信队列)

《SpringKafka错误处理(重试机制与死信队列)》SpringKafka提供了全面的错误处理机制,通过灵活的重试策略和死信队列处理,下面就来介绍一下,具有一定的参考价值,感兴趣的可以了解一下... 目录引言一、Spring Kafka错误处理基础二、配置重试机制三、死信队列实现四、特定异常的处理策略五

springboot+dubbo实现时间轮算法

《springboot+dubbo实现时间轮算法》时间轮是一种高效利用线程资源进行批量化调度的算法,本文主要介绍了springboot+dubbo实现时间轮算法,文中通过示例代码介绍的非常详细,对大家... 目录前言一、参数说明二、具体实现1、HashedwheelTimer2、createWheel3、n

SpringBoot实现MD5加盐算法的示例代码

《SpringBoot实现MD5加盐算法的示例代码》加盐算法是一种用于增强密码安全性的技术,本文主要介绍了SpringBoot实现MD5加盐算法的示例代码,文中通过示例代码介绍的非常详细,对大家的学习... 目录一、什么是加盐算法二、如何实现加盐算法2.1 加盐算法代码实现2.2 注册页面中进行密码加盐2.

Java时间轮调度算法的代码实现

《Java时间轮调度算法的代码实现》时间轮是一种高效的定时调度算法,主要用于管理延时任务或周期性任务,它通过一个环形数组(时间轮)和指针来实现,将大量定时任务分摊到固定的时间槽中,极大地降低了时间复杂... 目录1、简述2、时间轮的原理3. 时间轮的实现步骤3.1 定义时间槽3.2 定义时间轮3.3 使用时

如何通过Golang的container/list实现LRU缓存算法

《如何通过Golang的container/list实现LRU缓存算法》文章介绍了Go语言中container/list包实现的双向链表,并探讨了如何使用链表实现LRU缓存,LRU缓存通过维护一个双向... 目录力扣:146. LRU 缓存主要结构 List 和 Element常用方法1. 初始化链表2.

Spring Boot整合消息队列RabbitMQ的实现示例

《SpringBoot整合消息队列RabbitMQ的实现示例》本文主要介绍了SpringBoot整合消息队列RabbitMQ的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的... 目录RabbitMQ 简介与安装1. RabbitMQ 简介2. RabbitMQ 安装Spring

golang字符串匹配算法解读

《golang字符串匹配算法解读》文章介绍了字符串匹配算法的原理,特别是Knuth-Morris-Pratt(KMP)算法,该算法通过构建模式串的前缀表来减少匹配时的不必要的字符比较,从而提高效率,在... 目录简介KMP实现代码总结简介字符串匹配算法主要用于在一个较长的文本串中查找一个较短的字符串(称为

通俗易懂的Java常见限流算法具体实现

《通俗易懂的Java常见限流算法具体实现》:本文主要介绍Java常见限流算法具体实现的相关资料,包括漏桶算法、令牌桶算法、Nginx限流和Redis+Lua限流的实现原理和具体步骤,并比较了它们的... 目录一、漏桶算法1.漏桶算法的思想和原理2.具体实现二、令牌桶算法1.令牌桶算法流程:2.具体实现2.1

如何通过Python实现一个消息队列

《如何通过Python实现一个消息队列》这篇文章主要为大家详细介绍了如何通过Python实现一个简单的消息队列,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录如何通过 python 实现消息队列如何把 http 请求放在队列中执行1. 使用 queue.Queue 和 reque