算法通关(2)--单调队列

devtools/2024/10/25 6:33:32/

特点:

  1. 队列中的元素保持单调递增或者单调递减的顺序
  2. 可以在头部和尾部进行元素的插入和删除操作
  3. 大小是动态变化的,由元素的入队和出队的操作决定

单调队列的经典用法

1.维持窗口滑动中的最大/最小值

维持了一个依次称为最大值的可能性!

增加数字的逻辑:维持滑动窗口的最大值,构建递减的队列例如:[5 2 4 6 7 1]:当0位置的5从尾部进队列(max=5),1位置的2进入队列(max=5),2位置的4进入队列前,弹出1位置的2,(为什么弹出?因为他再也没有机会称为最大值了!!!)2位置的4进入,(max=5),3位置的6进入前,弹出2位置的4,0位置的5,(max=6),4位置的7进入前,弹出4位置的6,(max=7),5位置的1直接进。

减少数字的逻辑:从头部进行弹出,相当于从左边突出数字,用L++进行维护,可以说这个下标过期了,那么这就可以解释形如[5,3,5,1]当2位置的5进行加入队列时,0位置的5进行弹出。(下标比0大,就是晚过期,比下标为0的位置晚弹出,因此为头部

例1:板子题

. - 力扣(LeetCode)

维持一个长度为k-1的队列,当算最大值时,R++,算完后,L++,还是k-1长度。由于最大值在头部,直接使用头部的最大值,当头部使用过后,进行释放

int MAXN = 10001;
deque<int> d(MAXN);
int head = 0;
int tail = 0;
// 单调队列里存储的是下标!!!
vector<int> maxSlidingWindow(vector<int>& nums, int k) {int n = nums.size();// 形成长度为 k - 1 的窗口for (int i = 0; i < k - 1; i++) {// 弹出 -- 滑动窗口有长度(尾巴 > 头),进的数大于队列的数while (head < tail && nums[d[tail - 1]] <= nums[i]) {tail--;}d[tail++] = i;}int m = n - k + 1;	// 结果数组的长度vector<int>ans(m, 0);// 当前窗口为k - 1// 当前窗口创建左右指针,为了找最大值for (int l = 0, r = k - 1; l < m; l++, r++) {// 进来while (head < tail && nums[d[tail - 1]] <= nums[r]) {tail--;}// 吸收一个数d[tail++] = r;// 最大值在头部ans[l] = nums[d[head]];// 如果是过期下标,最大值在滑动窗口的左边界,释放头部if (d[head] == l) {head++;}}return ans;
}

例题2:

. - 力扣(LeetCode)

思路:维持一个头部是最大的单调递减队列,维持一个头部是最小的单调递增队列,以0作为起始下标出发,分别记录最大最小值,直到走到末尾,记录下满足条件(最大值-最小值的绝对值小于等于limit)的距离(当前位置的下标-起始下标)。ok后,以1作为起始下标出发,按照上面的规则,直到走大末尾,之后是以2位置。。。
代码1:C++中的STL库

deque<int> maxDeque;	// 队列头部是进队的最大值
deque<int> minDeque;	// 队列头部是进队的最小值
vector<int> num;bool ok(int limit, int number) {// 获取单调减和单调增队列的队头元素--最大最小值int max_1 = (!maxDeque.empty()) ? max(num[maxDeque.front()], number) : number;int min_1 = (!minDeque.empty()) ? min(num[minDeque.front()], number) : number;return max_1 - min_1 <= limit;
}void push(int r) {// 入队条件:空 or 队尾元素 大于 当前元素,因为是一个单调减队列,队头元素最大while (!maxDeque.empty() && num[maxDeque.back()] <= num[r]) {maxDeque.pop_back();}maxDeque.push_back(r);// 入队条件:空 or 队尾元素 小于 当前元素,因为是一个单调增队列,队头元素最小while (!minDeque.empty() && num[minDeque.back()] >= num[r]) {minDeque.pop_back();}minDeque.push_back(r);
}void pop(int l) {// 队列不空 + 队头是要弹出的数if (!maxDeque.empty() && maxDeque.front() == l) {maxDeque.pop_front();}if (!minDeque.empty() && minDeque.front() == l) {minDeque.pop_front();}
}int longestSubarray(vector<int>& nums, int limit) {maxDeque.clear();minDeque.clear();	// 清空队列num = nums;			// 为了少传参int n = nums.size();int ans = 0;for (int l = 0, r = 0; l < n; l++) {// 不越界 + 队列算上推进去最大绝对值差<=limit才能pushwhile (r < n && ok(limit, nums[r])) {push(r++);}// 越界 或者 最大绝对值差>limitans = max(ans, r - l);pop(l);	// 此位置算完了,弹出算下一位置}return ans;
}int main() {vector<int> nums = { 10,1,2,4,7,2 };int limit = 5;int length = longestSubarray(nums, limit);cout << length;return 0;
}

代码2:数组的模拟实现

const int MAXN = 10001;
int maxQueue[MAXN];		// 队头元素最大 -- 递减
int minQueue[MAXN];		// 队头元素最小 -- 递增
int maxHead, minHead, maxTail, minTail;	// 两个队列的头和尾
vector<int>num;	// 将nums变成全局变量bool reach(int limit, int value) {// 加入右边的数字,看是否<=limit// 如果此时窗为空,那么最大值就是进来的数int maxValue = (maxHead < maxTail) ? max(num[maxQueue[maxHead]], value) : value;int minValue = (minHead < minTail) ? min(num[minQueue[minHead]], value) : value;return maxValue - minValue <= limit;
}void pushValue(int r) {// 加入+调整// 不为空+进的大于队尾元素,弹出while (maxHead < maxTail && num[maxQueue[maxTail - 1]] <= num[r]) {maxTail--;}maxQueue[maxTail++] = r;// 不为空+进的小于队尾元素,弹出while (minHead < minTail && num[minQueue[minTail - 1]] >= num[r]) {minTail--;}minQueue[minTail++] = r;
}void popValue(int l) {// 不为空+是要弹出的元素if (maxHead < maxTail && maxQueue[maxHead] == l) {maxHead++;}if (minHead < minTail && minQueue[minHead] == l) {minHead++;}
}int longestSubarray(vector<int>& nums, int limit) {maxHead = minHead = maxTail = minTail = 0;	// 清空队列num = nums;									// 减少传参int n = nums.size();int len = 0;for (int l = 0, r = l; l < n; l++) {// 创建窗口[l,r),r是没有进入窗口的,下一个位置// 不能越界,r位置的数进来是否达标,即<=limitwhile (r < n && reach(limit, num[r])) {		pushValue(r++);}// [l,r) 以l开头子数组向右延伸的最大范围len = max(len, r - l);// 这个l弄完了,弹出popValue(l);}return len;
}int main() {int n, x, limit;scanf("%d", &n);vector<int> nums;for (int i = 0; i < n; i++) {scanf("%d", &x);nums.push_back(x);}scanf("%d", &limit);int res = longestSubarray(nums, limit);printf("%d", res);return 0;
}

相似题目:[USACO12MAR] Flowerpot S - 洛谷

例3:

862. 和至少为 K 的最短子数组 - 力扣(LeetCode)

思路:满足条件的子数组可以以任意下标结尾,可能以5下标结尾的子数组满足条件的有两个,那么我们就取最短的。这样子的大体思路出来了。以每个位置下标的数字结尾,向前数最短的满足条件的长度,总的最短长度就是题目的要求。
但是求最短长度需要求得一个区间的和,如何快速求得区间的和?对的,利用前缀和,求得数组每个位置的前缀和,当求得每一段数组的和时,就可以使对应位置的和相减即为相对应区间长度的和。由此可以得出这样的两段代码:

// 求前缀和
for (int i = 0; i < n; i++) {preSum[i+1] = preSum[i]+nums[i];
}// 当前的前缀和 - 头部前缀和 大于等于 k,达标
while (head != tail && preSum[i] - preSum[Queue[head]] >= k) {ans = min(ans, i - Queue[head++]);
}

添加前缀和的注意情况:构建一个头为最小值的单调队列,单调队列中放前缀和。为什么这样子的构造。举个例子:数组[6,5,4,-3,4...]要满足的和为20,数组此时构成的前缀和为[6,11,15,12,16....]当后面的前缀和减去12时得到的值一定大于减去15的值,更加的接近20,而且减去12得到的数组长度还短,那为什么不选择减去12呢~~

淘汰的情况:[1,2,1,6,7,2,1,5,3],前缀和:[1,2,4,10,17,19,20,25,28],假设要满足子序列和12,前缀和1,2,4,10进入,当17进入是满足条件,而且淘汰掉1和2得到的和为14,也满足条件,长度为3,比原先的长度为5要短,加入前缀和17... 算最短的长度

总体:例子:[5,3,2,-4,7,3,10],构建头部最小的单调递增栈。

int n, k;
const int MAXN = 100005;
long preSum[MAXN];	// 前缀和
int head, tail;
int Queue[MAXN];	// 构建递增队列
int shortestSubarray(vector<int>& nums, int k) {int n = nums.size();// 求前缀和for (int i = 0; i < n; i++) {preSum[i+1] = preSum[i]+nums[i];}head = tail = 0;int ans = INT_MAX;for (int i = 0; i <= n; i++) {// 当前的前缀和 - 头部前缀和 大于等于 k,达标while (head != tail && preSum[i] - preSum[Queue[head]] >= k) {ans = min(ans, i - Queue[head++]);}// 构建单调递增队列,放前缀和while (head!=tail && preSum[Queue[tail - 1]] >= preSum[i]) {tail--;}Queue[tail++] = i;}return ans != INT_MAX ? ans : -1;
}

 


http://www.ppmy.cn/devtools/128617.html

相关文章

基于neo4j的学术论文关系管理系统

正在为毕业设计头疼&#xff1f;又或者在学术研究中总是找不到像样的工具来管理浩瀚的文献资料&#xff1f;今天给大家介绍一款超实用的工具——基于Neo4j的学术论文关系管理系统&#xff0c;让你轻松搞定学术文献的管理与展示&#xff01;&#x1f389; 系统的核心是什么呢&a…

word删除空白页 | 亲测有效

想要删掉word里面的末尾空白页&#xff0c;但是按了delete之后也没有用 找了很久找到了以下亲测有效的方法 1. 通过鼠标右键在要删除的空白页面处显示段落标记 2. 在字号输入01&#xff0c;按ENTER&#xff08;回车键&#xff09; 3.成功删除了&#xff01;&#xff01; PS…

进程间通信(一)管道

文章目录 进程间通信进程间通信概述进程间通信的方式管道通信示例--基于管道的父子进程通信示例--使用管道进程兄弟进程通信 管道的读写特性示例--不完整管道&#xff08;读一个写端关闭的管道&#xff09;示例--不完整管道&#xff08;写一个读端关闭的管道&#xff09; 标准库…

Java实现文件上传功能

目录 1、准备工作 2、注意事项 3、jsp页面代码 4、Servlet 5、注册Servlet 1、准备工作 导入依赖&#xff1a;commons-fileupload和commons-io 2、注意事项 ①为保证服务器安全&#xff0c;上传文件应该放在外界无法直接访问的目录下&#xff0c;比如WEB-INF目录下 ②为…

Lua变量

软考鸭微信小程序 过软考,来软考鸭! 提供软考免费软考讲解视频、题库、软考试题、软考模考、软考查分、软考咨询等服务 Lua是一种轻量级的脚本语言&#xff0c;以其简单、高效和易于嵌入的特性而广受欢迎。在Lua中&#xff0c;变量是存储数据的容器&#xff0c;可以存储不同类型…

基于KU115+ZU19EG+C6678 的高性能6U VPX 载板

基于KU115ZU19EGC6678 的高性能6U VPX 载板&#xff0c;板载 2 个 HPC 形式的FMC 连接器&#xff08;用于外部信号扩展&#xff09;。板卡选用了 1 片Xilinx 公司的Kintex UltraScale 系列 FPGA 家族中的XCKU115-2FLVA1517I 和 1 片 Zynq UltraScale MPSoC 家族的XCZU19EG-2FFV…

Spring 相关技术要点整理

以下是对 Bean 的作用域和生命周期的详细说明&#xff1a; 一、Bean 的作用域 singleton&#xff08;单例&#xff09;&#xff1a; 这是默认的作用域。在整个应用中&#xff0c;对于特定的 Bean 类型&#xff0c;只会创建一个实例。无论在应用的哪个地方获取该 Bean&#xff…

nginx中的HTTP 负载均衡

HTTP 负载均衡&#xff1a;如何实现多台服务器的高效分发 为了让流量均匀分配到两台或多台 HTTP 服务器上&#xff0c;我们可以通过 NGINX 的 upstream 代码块实现负载均衡。 方法 在 NGINX 的 HTTP 模块内使用 upstream 代码块对 HTTP 服务器实施负载均衡&#xff1a; upstr…