当前位置: 首页 > news >正文

【数据结构与算法】单调队列的定义和运用

概述

单调队列算法基于双端队列

关于 队列和双端队列 的知识,可以参考这里:

【C++】STL数据结构算法3—队列

下面重点看一道例题:

定义引入- - -例题:P1886 滑动窗口 /【模板】单调队列

传送门:P1886 滑动窗口 /【模板】单调队列 - 洛谷

题目大意

给定长度为 nnn 的数列 aaa 以及正整数 kkk

现有一个长度为 kkk 的区间正在滑动

(初始为 [1,k][1,k][1,k],最后滑动至 [n−k+1,n][n-k+1,n][nk+1,n]

求每次滑动过后区间内数的最大值和最小值

(可以看原题的图,很容易理解)

数据范围

对于 100% 的数据,1≤k≤n≤106,ai∈[−231,231)1\leq k \leq n \leq 10^6, a_i \in [-2^{31}, 2^{31})1kn106,ai[231,231)

思路详解

暴力 O(n2)O(n^2)O(n2)

首先很容易想到暴力:直接枚举左端点 lll, 再遍历计算区间 [l,l+k−1]内的最值[l,l+k-1]内的最值[l,l+k1]内的最值

由于代码太过基础,这里就暂且不贴出了
绝对不是因为懒得写

其它数据结构做法 O(nlogn)O(nlogn)O(nlogn)

看到这种区间查询的题目,学过线段树/分块算法的朋友们可能就跃跃欲试了(

的确,我们可以建一颗线段树,写一个函数 query⁡(l,r)\operatorname {query}(l,r)query(l,r) 计算区间最小值和最大值

确实可能能过,但代码量实在是太大了啊……(常数也很大)实在是不推荐

单调队列 O(n)O(n)O(n)

我们可以先只考虑求一个最值(例如求最大值)的做法:

由于只要求求最大值,显然易得:其它的非最大值的数都是无用的

由于滑动窗口有一定的范围,所以超出区间长度的数也是无用的

形象化地说,如果一个数比你的时效长(在后面的数能更长久地产生贡献),数值还比你大,那你就可以光荣“退休”了

那么我们可以考虑正序枚举数列,建一个双端队列,每次弹出队头超出使用时限的元素队尾比当前元素小的元素。可以证明,当前队头的元素所对应的数组值一定是最大值!

(注意,为了判断时效,队列里要存数组元素的下标!!

例如,对于数组 [3,6,7,5,5,5][3,6,7,5,5,5][3,6,7,5,5,5]k=3k = 3k=3

首先队列依次进入 111,相安无事,不输出答案(因为1<31<31<3

接着队中欲新进入一个元素 222。检验时发现:a2>a1a_2 > a_1a2>a1,也就是说,在 111 这个位置的数不仅比它早进入(更容易失效),还比它的值更小!于是我们弹出原先队尾的1,队列变为[2][2][2],同理不输出答案。

而后插入 3,43,43,4 ,相安无事(虽然值比较小,但时效长,可能被用上)队列变为[2,3,4][2,3,4][2,3,4],答案为a2=6a_2=6a2=6

当插入 555 时,队尾自然是相安无事。但我们发现,当前的区间是[3,5][3,5][3,5],队头的 222 已经失效,不能参与计算了!于是,我们弹出222,队列变为[3,4,5][3,4,5][3,4,5],答案为a3=5a_3=5a3=5

以此类推,我们便能证明这个方法的正确性和时间复杂度的合理性。

至于最小值,只需要将弹出队尾的条件稍微修改即可(见代码)

AC代码

#include<iostream>
#include<queue>
using namespace std;
#define MaxN 1000005
#define For(i, j, k) for(int i = j; i <= k; i++)int n, k;
int a[MaxN];int main()
{cin >> n >> k;For(i, 1, n) cin >> a[i];deque<int> q;For(i, 1, n){while(!q.empty() && i-q.front()+1 > k) q.pop_front();while(!q.empty() && a[q.back()] > a[i]) q.pop_back();q.push_back(i);if(i >= k) cout << a[q.front()] << ' ';}cout << endl;q.clear();For(i, 1, n){while(!q.empty() && i-q.front()+1 > k) q.pop_front();while(!q.empty() && a[q.back()] < a[i]) q.pop_back();q.push_back(i);if(i >= k) cout << a[q.front()] << ' ';}cout << endl;return 0;
}

进阶运用- - P2698 Flowerpot S

传送门:P2698 [USACO12MAR] Flowerpot S - 洛谷

【题目描述】

老板需要你帮忙浇花。给出 NNN 滴水的坐标,(x,y)(x,y)(x,y) 表示水滴最初的坐标。

每滴水均以每秒 111 个单位长度的速度下落。你需要把花盆放在 xxx 轴上的某个位置,使得花盆接到第 111 滴水与最后 111 滴水之间的时间差至少为 DDD

如果水滴落在 xxx 轴上的位置与花盆的边沿对齐,也认为被接住。

给出 NNN 滴水的坐标和时间差 DDD ,请算出最小的花盆宽度 WWW

如果无法构造出满足题意的花盆,则输出 −1-11

【数据范围】

40%40\%40% 的数据:1≤N≤10001 \le N \le 10001N10001≤D≤20001 \le D \le 20001D2000

100%100\%100% 的数据:1≤N≤1051 \le N \le 10 ^ 51N1051≤D≤1061 \le D \le 10 ^ 61D1060≤x,y≤1060\le x,y\le10^60x,y106

思路详解

我们发现,若花盆的宽度已经确定

这就变成了一个经典的滑动窗口问题,我们只需要求出每个窗口的最大值与最小值,再做差即可。

而对于花盆的宽度,我们完全可以用 O⁡(logxmax)\operatorname O(logx_{max})O(logxmax) 的时间来二分

这样,总时间复杂度O⁡(nlogn)\operatorname O(nlogn)O(nlogn),求得正解

注意:首先要对数组按 xxx 值进行排序!

AC代码

#include<bits/stdc++.h>
using namespace std;
#define For(i, j, k) for(int i = j; i <= k; i++)
#define dFor(i, j, k)for(int i = j; i >= k; i--)
#define MaxN 100086
#define int long longstruct Node{int x, y;bool operator < (const Node a){		//按x值排序return x < a.x;}
}a[MaxN];
int n, D;bool check(int k){deque<int> q1, q2;		//q1存最大值,q2存最小值For(i, 1, n){while(!q1.empty() && a[i].x - a[q1.front()].x > k) q1.pop_front();while(!q1.empty() && a[i].y <= a[q1.back()].y) q1.pop_back();q1.push_back(i);while(!q2.empty() && a[i].x - a[q2.front()].x > k) q2.pop_front();while(!q2.empty() && a[i].y >= a[q2.back()].y) q2.pop_back();q2.push_back(i);//		cout << q1.front() << ' ' << q2.front() << endl;if(a[q2.front()].y - a[q1.front()].y >= D) return 1;}return 0;
}signed main()
{cin >> n >> D;For(i, 1, n){cin >> a[i].x >> a[i].y;}sort(a+1, a+1+n);int l = 1, r = 10000086;while(l <= r){int mid = (l + r) / 2;if(check(mid)) r = mid - 1;else l = mid + 1;}if(l >= 10000000) cout << -1;else cout << l << endl;return 0;
}

结语

综上,单调队列是双端队列的扩充,应用也十分广泛

在遇到左右端点一同移动的问题时,都可以考虑单调队列

最后,制作不易,如果这篇文章对你有帮助,别忘了点个 收藏 点个

我的主页还有更多好用的数据结构,并且持续更新!感谢支持!

http://www.dtcms.com/a/335795.html

相关文章:

  • 整体设计 之“凝聚式中心点”原型 --整除:智能合约和DBMS的深层联合 之1
  • Android Jetpack | Livedata
  • Gradle快速入门学习
  • 【算法】模拟专题
  • riscv中断处理软硬件流程总结
  • C++算法题目分享:二叉搜索树相关的习题
  • 原子指标、派生指标和复合指标
  • nodejs 中间件
  • 【Jenkins】01 - Jenkins安装
  • C语言网络编程TCP通信实战:客户端↔服务器双向键盘互动全流程解析
  • [GWCTF 2019]枯燥的抽奖
  • Java线程的6种状态和JVM状态打印
  • [TryHackMe]Brainpan1( 内存溢出)
  • PERCEIVER IO:一种用于结构化输入与输出的通用架构
  • 脉冲计数实现
  • 深入剖析ROS参数服务器通信机制 ——共享全局数据的“云端仓库”实现原理
  • Node.js安装教程
  • MySQL的事务日志:
  • java之 junit4单元测试Mockito的使用
  • 26. Object.defineProperty 和 Proxy 用法
  • 中级统计师-会计学基础知识-第五章 财务报告
  • 计算机算术6-SRT2除法
  • Linux817 shell:until,nfs,random
  • TypeScript中的import语法详解
  • 6.Ansible自动化之-管理变量和事实
  • 关于第一次接触Linux TCP/IP网络相关项目
  • 牛客周赛 Round 105
  • Java -- 坐标体系--绘图方法
  • 《详解 C++ Date 类的设计与实现:从运算符重载到功能测试》
  • Originality AI:原创度和AI内容检测工具