单调队列是什么?
单调队列用途:序列长为 n n n,求每个 m m m 长的连续子序列的区间最值,常用于滑窗最值问题,也常用于 d p dp dp 优化
单调队列的核心
- 一个神奇的单调队列的比喻:比你小的人还比起强,你就可以被淘汰了 (来自知乎某dalao)
- 单调队列不一定要严格单调,就那上面的比喻,当你和比你小的人旗鼓相当时,你不一定被淘汰
- 维护最大值,则维护单减队列,因为队头总是最值
- 队列中存的是数组下标!!!!!!
单调队列八股文模板,熟练掌握!!!!!!!(我们这里就不要求严格了,以最大值为例)
设序列长度为 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[i−1][j],dp[i−1][j−v]+w,......,dp[i−1][j−kv]+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)
0≤k≤min(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[i−1],pre[i−2],...pre[max(0,i−m)])
所以很明显要用单调队列优化。。。,但要注意这里子序列的长度必须非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;
}