我为什么想写这篇单调队列的总结
一个原因是我在在看背包问题里的 多重背包解题优化 的时候后看到了 一种算是我现在知道最高效的一个优化方法,了解到可以用 单调队列优化 去大大的减小运算复杂度,然后就到百度上搜索学习,看到了一些 较好的博文、讲解视频,所以有我写的这篇一些东内容是从中借用的,但是大部分文章还讲的 有难以理解。所我想先总结一下这个单调队列的用法,并以我认为简单的方式总结好,并希望能帮助其它的人
单调队列是什么、怎么理解
从字面单调我们可以知道它就是 从大到小或者从小到大 排列的一组连续的数.我认为他最重要的核心是:它所确定的 单调性 一定是在一定是 在当前的某个限制个范围上的(他最重要的就是这个 范围限制,否则的话 直接找就好了,何必这么麻烦呢),在这个范围上 有最大或最小值,这是最重要的它的应用也是建立在这个基础上的。我认为 也可以把单调队列看作是一个解题的思想来看待它。
单调队列有什么用
先举个🌰去感受它的 操作过程
举个例子:有 7 6 8 12 9 10 3 七个数字,现在让你找出范围 [ i-3,i ] (i<=1 && i <=7) 的最小值。
我们先模拟一遍
这里注意我们可以建立 一个 单调递减的数列,这个队列从左往右的是递减的(我们默认队列列的最 左边 是头部,最右边是尾部)所以这个队列的的部元素一定是该队列中的最大值,当我们在插入元素的时候,一定是从队列的尾部插入元素,当插入元素后我们 需要进行 两步判断操作:
操作 1.维持(保持)队列的单调性:这个明词说的好像说的非常高端、难以理解,其实它的意思非常容易理解「例如:在一个递减队列 {5,3,1}中我们插入 一个4,则这个队列就变成了{5,3,1,4},这个时候队列的单调性已经被破坏了,为了使队列仍具有单递增性 这个时候就需要把 在队列中小于等于4的元素全部弹出,这队列就变成了{5,4},队列又恢复了原来的单调递增性,即使经过n次这样的操作 我们仍然可以确定 队头元素一定是最大值(最左边的元素)」,这一个操作存在的意义是不管怎么从尾部插入元素,经过这部操作后队列一定仍是 有序的。
.
操作 2.检查 队列中的元素是否都在所规定的边界范围内:这个直接说还真的不一定能理解「所以还是继续举例子:假如我们有一个 序列 5 、3、1、4 我们 要求3,1,4 这个三个数的最🀄️的大值,我们如果把 5、3、1、4 按照 1操步骤 插入单调队列中去得到单调队列为 {5,4} 这个时候他的最大值是5,但是我们是取不到 5 这个数的,因为我们让求的是3、1、4 这三个数范围内的最大值,显然这个时候 5已经越界了,所以要把越界的头部元素弹出队列 ,这样队列剩余的元素为 {4},4正是我们所求的最大值 」我想这个操作步骤存在的意义 就是把 不在规定所求区域的队列中的头部元素剔除,使头部(最大、最小值)元素一定在所求区域
操作过程模拟
目标序列
7
6
8
12
9
10
3
序列元素下表 i(区间)
1
2
3
4
5
6
7
起始的时候 递增队列为空的
i 值
所求区间
插入元素后的队列
操作1后的队列
操作2后的最终队列
1
(-2,1)
{ 7 }
{ 7 }
{ 7}
2
(-1,2)
{ 7,6}
{ 6 }7大于6舍去
{ 6}
3
(0,3)
{ 6,8 }
{ 6,8 }
{ 6,8 }
4
(1,4)
{6,8,12}
{ 6,8,12}
{6,8,12}
5
(2,5)
{6,8,12,9}
{6,9}8,12大于9舍去
{6,9}
6
(3,6)
{6,9,10}
{6,9,10}
{9,10} 6越界舍去
7
(4,7)
{9,10,7}
{7} 9,10大于7舍去
{7}
所以经过 这样几次循环的操作和最终的序列为{ 7 }
单调队列作用
**单调队列用于维护一个长度固定的区间内,数组的最值。以最大值为例,如果一个数组长度为n,取长度为m的区间,那么单调队列的队首一定是数组在该区间内的最大值。之所以是维护,就是当区间开始整体后移时,最大值可能发生变化,而单调队列可以在O(n)的时间复杂度下得到全部n-m+1个区间的最大值。
**
单调队列实例解析 :最大子序和
输入一个长度为n的整数序列,从中找出一段长度不超过m的连续子序列,使得子序列中所有数的和最大。
注意: 子序列的长度至少是1。
输入格式
第一行输入两个整数n,m。
第二行输入n个数,代表长度为n的整数序列。
同一行数之间用空格隔开。
输出格式
输出一个整数,代表该序列的最大子序和。
数据范围
1≤n,m≤300000
输入样例:
6 4
1 -3 5 1 -2 3
输出样例:
7
思路如下:
我们先把序列的前i项和加起来并存到一个数组sum[ ]上,那么任意连续的子序列和就为sum [ i ] - sum[ j ] (i>j && j>i-m)。
那么我们就可以跟上述例子一样枚举i值 当i值确定则sum[i]也确定,在当前sum[i]值已经确定确定的情况下,每次找出在(i-m,i)范围内的最小sum[j]值(这正照应:在当前的限制范围内,不断更新单调队列)。
题解如下 一:
#include
#include
using namespace std;
const int Len = 300005;
long long int sum[Len];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i = 1;i <= n;i ++)//求前缀和
{
cin>>sum[i];
sum[i] += sum[i - 1];
}
//初始化数据
int left = 1,right = 1;
int index[Len] = {0};
index[1] = 1;
long long int maxx = sum[1];
//(定范围的)单调递增序列
for(int i = 2;i <= n;i ++)
{
//操作1:检查 单调队列 最左边的元素 是否不再(i - m, i)这个范围内
if(index[left] < i - m)
left ++;
//用当前sum[i]减去前 单调队列中 最左边的 最小sum值
maxx = max(maxx,sum[i] - sum[index[left]]);
//操作2:往 队列 最右端添加sum[i]元素(实际上是把 i 坐标添加进单调队列了),为了维护 队列从小到大的单调性,所以要把 队列中元素 比 sum[i] 大的都删去
while(left <= right && sum[index[right]] >= sum[i])
right --;
//添加sum[i]元素的下表
right ++;
index[right] = i;
}
printf("%lld\n",maxx);
return 0;
}
题解如下 二:
#include
#include
#include
using namespace std;
const int L = 300005;
long long int s[L];
int n,m;
int q[L];
int main()
{
cin>>n>>m;
s[0] = 0; //把 首元素设置为边界值,去解决边界问题
for(int i = 1;i <= n;i ++)
{
cin>>s[i];
s[i] += s[i-1];
}
//初始化数据
long long int mx = INT_MIN;
int hd = 0,ed = 0;
//q[0] = sum[
for(int i = 1;i <= n;i ++)
{
if(i - q[hd] > m) hd ++;//操作2
mx = max(mx,s[i] - s[q[hd]]);
while(hd <= ed && s[i] <= s[q[ed]]) ed --;//操作1
q[++ed] = i;
}
cout<
}
题解如下 三:
#include
#include
#include
#include
#include
using namespace std;
const int L = 300005;
long long int sum[L];
int n,m;
deque qu;
int main()
{
cin>>n>>m;
sum[0] = 0;
for(int i = 1;i <= n;i ++)
{
cin>>sum[i];
sum[i] += sum[i-1];
}
long long int mx = INT_MIN;
qu.push_back(0);
for(int i = 1;i <= n;i ++)
{
if(i - qu.front() > m) qu.pop_front();//操作2检查边界
mx = max(mx,sum[i] - sum[qu.front()]);
while(!qu.empty() && sum[i] <= sum[qu.back()]) qu.pop_back();//操作1 维护队列单调性
qu.push_back(i);
}
cout<
return 0;
}