数据结构专题——单调队列

单调队列是什么?

单调队列用途:序列长为 n n n,求每个 m m m 长的连续子序列的区间最值,常用于滑窗最值问题,也常用于 d p dp dp 优化

单调队列的核心

  1. 一个神奇的单调队列的比喻:比你小的人还比起强,你就可以被淘汰了 (来自知乎某dalao)
  2. 单调队列不一定要严格单调,就那上面的比喻,当你和比你小的人旗鼓相当时,你不一定被淘汰
  3. 维护最大值,则维护单减队列,因为队头总是最值
  4. 队列中存的是数组下标!!!!!!

单调队列八股文模板,熟练掌握!!!!!!!(我们这里就不要求严格了,以最大值为例)

设序列长度为 n,滑窗的大小为 m(n>=m)
注意数组模拟队列的用法,hh,tt均准确指向队头,队尾元素

第一步,预处理 m-1 的初始队列
for 1---->m-1
	while(队列非空(tt>=hh) 且 队尾元素小于当前元素)	弹出队尾元素(--tt)
	当前元素入队

第二步,开始滑动
for m---->n
	while(队列非空(tt>=hh) 且 队尾元素小于当前元素)	弹出队尾元素(--tt)
	当前元素入队
	取队头元素,即为最大值(必须在入队后执行,因为新入队的元素可能是最值)
	if 队头的元素下标刚好等于对应下标,即(i-m+1,(毕业)弹出队列

例1 [Leetcode 剑指 Offer 59 - I. 滑动窗口的最大值]

在这里插入图片描述
单调队列模板,直接套

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) 
    {
        int siz=nums.length,hh=0,tt=-1,cnt=0;
        if(siz==0)
        {
            int[] ans=new int[0];
            return ans;
        }
        int[] q=new int[siz+5];
        int[] ans=new int[siz-k+1];

        for(int i=0;i<k-1;++i)
        {
            while(hh<=tt&&nums[q[tt]]<nums[i]) --tt;
            q[++tt]=i;
        }

        for(int i=k-1;i<siz;++i)
        {
            while(hh<=tt&&nums[q[tt]]<nums[i]) --tt;
            q[++tt]=i;
            ans[cnt++]=nums[q[hh]];
            if(q[hh]==i-k+1)    ++hh;
        }

        return ans;
    }
}

例2 [AcWing 6. 多重背包问题 III——单调队列优化的dp]

在这里插入图片描述
根据数据范围,我们的时间复杂度必须保持在 O ( V N ) O(VN) O(VN),所以必须要用优化。

首先思考,这个题和单调队列有啥关系——优化 d p dp dp 的状态转移

首先来看完全背包的初始版本的状态转移方程:
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v ] + w , . . . . . . , d p [ i − 1 ] [ j − k v ] + k w ) dp[i][j]=max(dp[i-1][j],dp[i-1][j-v]+w,......,dp[i-1][j-kv]+kw) dp[i][j]=max(dp[i1][j],dp[i1][jv]+w,......,dp[i1][jkv]+kw)
在完全背包中,上述的 k k k 值只由容量 j j j 所限制,而在本题的多重背包中,还受到物品数量 s i s_i si 的限制,即: 0 ≤ k ≤ m i n ( s i , ⌊ j v ⌋ ) 0\le k \le min(s_i,\lfloor \frac{j}{v} \rfloor) 0kmin(si,vj)
接下来就是楼大佬发明此思路的绝妙之处了:
我们可以把所有的容量值 v ∈ [ 0 , V ] v\in[0,V] v[0,V] 进行分类,按照 v v v c o s t i cost_i costi分类 ,模相同的 v v v 可以在一个滑动窗口中进行滑动,在 O ( N ) O(N) O(N) 的复杂度内求出最值,滑窗的大小由物品的数量决定,(默认物品大小不会超过总容量)

怎末做呢?
在这里插入图片描述

下面由伪代码解释

设当前 i 物品的容量为 siz,则我们枚举余数 0 ~ siz-1

对于余数 j,我们按照 j+0*siz, j+1*siz, j+2*siz, .... 枚举,直至到达整个背包容量的极限

这里的队列,存的是几个物品,dp[i-1]相当于普通单调队列中的 a 数组

for(k=0; j+k*siz<=MAX_SIZE; ++k)
	首先 dp[i][j+k*siz]=dp[i-1][j+k*siz](即上述图片的最后一行)
	
	while(tt<=hh 且 队尾的dp值小于当前dp值,
			即 dp[i-1][j+q[tt]*siz]+(k-q[tt])*val < dp[i-1][j+k*siz])	
		弹出队尾
		
	当前元素入队:q[++tt]=k
	
	更新最值,dp[i][j+k*siz]=max(dp[i][j+k*siz],dp[i-1][j+q[hh]*siz]+(k-q[hh])*val)
	
	处理队头,若队头的元素(物品的数量)+ s == k
	if (q[hh]+s==k)	弹出队头

注意:要用滚动数组!!!

完整代码(洛谷P1776验):

#include<cstdio>
#include<cstring>
#include<iostream>
#include<cmath>
#include<algorithm>
#include<vector>
#include<string>
#include<set>
#include<map>
#include<unordered_map>
#include<queue>

#define me(x,y) memset(x,y,sizeof x)
#define rep(i,x,y) for(i=x;i<=y;++i)
#define repf(i,x,y) for(i=x;i>=y;--i)
#define lowbit(x) -x&x
#define inf 0x3f3f3f3f
#define INF 0x7fffffff
#define f first
#define s second

using namespace std;
typedef long long ll;
typedef pair<int,int> PII;

inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
	while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
	return x*f;
}

const int V=4e4+10;
int n,MAX_SIZ;

int dp[2][V];
int q[V];

int main()
{
	int i,j,k,siz,val,s,hh,tt,tag=0;
	
	n=read(),MAX_SIZ=read();
	
	rep(i,1,n)
	{
		val=read(),siz=read(),s=read();
		
		rep(j,0,siz-1)		//枚举余数
		{
			hh=0,tt=-1;
			
			for(k=0;k*siz+j<=MAX_SIZ;++k)
			{
				dp[tag][k*siz+j]=dp[tag^1][k*siz+j];
				while(tt>=hh&&dp[tag^1][q[tt]*siz+j]+(k-q[tt])*val<dp[tag^1][k*siz+j])	--tt;
				q[++tt]=k;
				dp[tag][k*siz+j]=max(dp[tag][k*siz+j],dp[tag^1][q[hh]*siz+j]+(k-q[hh])*val);
				if(q[hh]+s==k)	++hh;
			}
		}
		tag^=1;
	}
	
	printf("%d",dp[tag^1][MAX_SIZ]);
	
	return 0;
}

例3 [AcWing 135. 最大子序和] 单调队列与前缀和

在这里插入图片描述
连续子序列求和首先想到前缀和,设 p r e [ i ] pre[i] pre[i]为前 i i i 个元素的和, d p [ i ] dp[i] dp[i]为以 i i i 结尾的长度不超过 m m m 的连续子序列的最大值,状态转移方程为: d p [ i ] = p r e [ i ] − m i n ( p r e [ i − 1 ] , p r e [ i − 2 ] , . . . p r e [ m a x ( 0 , i − m ) ] ) dp[i]=pre[i]-min(pre[i-1],pre[i-2],...pre[max(0,i-m)]) dp[i]=pre[i]min(pre[i1],pre[i2],...pre[max(0,im)])

所以很明显要用单调队列优化。。。,但要注意这里子序列的长度必须非0!所以要先更新最值!

#include<cstdio>
#include<cstring>
#include<iostream>
#include<cmath>
#include<algorithm>
#include<vector>
#include<string>
#include<set>
#include<map>
#include<unordered_map>
#include<queue>

#define me(x,y) memset(x,y,sizeof x)
#define rep(i,x,y) for(i=x;i<=y;++i)
#define repf(i,x,y) for(i=x;i>=y;--i)
#define lowbit(x) -x&x
#define inf 0x3f3f3f3f
#define INF 0x7fffffff
#define f first
#define s second

using namespace std;
typedef long long ll;
typedef pair<int,int> PII;

inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
	while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
	return x*f;
}

const int N= 3e5+10;
int n,m;

ll pre[N]; 
int q[N];

int main()
{
	int i,j,hh=0,tt=-1;
	ll ans=-1e19;
	
	n=read(),m=read();
	rep(i,1,n)	pre[i]=read(),pre[i]+=pre[i-1];
	
	q[++tt]=0;
	rep(i,1,n)
	{
		ans=max(ans,pre[i]-pre[q[hh]]);
		while(hh<=tt&&pre[q[tt]]>pre[i])	--tt;
		q[++tt]=i;
		if(i-q[hh]>=m)	++hh; 							//带不带等号要想清楚
	}
	printf("%lld",ans);
	
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值