学习笔记:单调队列优化dp

概念

单调队列优化 d p dp dp,就是指用单调队列来优化的 d p dp dp

方法

一般是针对于求最值的问题。在队列中加入某一个数,如果有些方案显然在后面是不会用到的则直接弹出即可。一般处理再 d p dp dp中有要求一段区间的最小值或最大值等等。还有一种是滑动窗口,指在一个序列的一段区间内求最值的问题,就像是一个窗口框柱一些框内的数,然后在框内找最值。

例题

AcWing 135

因为要求的是某一段区间的和,所以可以用前缀和来得到一段区间的和。首先,我们发现,求一段区间的和 s r − s l − 1 s_r-s_{l-1} srsl1,枚举到 r r r肯定 s r s_r sr是一定的,那么要区间和尽量大就是让 s l − 1 s_{l-1} sl1尽量小,则加入一个数 s i s_i si作为 s l − 1 s_{l-1} sl1时前面比 s i s_i si大的都不可能用到了,就是个单调递增队列。每次取队头作为 s l − 1 s_{l-1} sl1就是以 s i s_i si s r s_r sr的区间的最优方案。注意,如果区间长度大于 m m m了要弹出一个数。注意,如果最大的是从头开始就是 − s 0 -s_0 s0,所以最开始要压入一个 0 0 0(这里直接令 h = t = 0 h=t=0 h=t=0即可)。

#include<bits/stdc++.h>
using namespace std;
const int NN=300004;
int s[NN],q[NN];
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        int x;
        scanf("%d",&x);
        s[i]=s[i-1]+x;
    }
    int ans=-1e9,h=0,t=0;
    for(int i=1;i<=n;i++)
    {
        if(i-q[h]>m)
            h++;
        if(h<=t)
            ans=max(ans,s[i]-s[q[h]]);
        while(h<=t&&s[q[t]]>=s[i])
            t--;
        q[++t]=i;
    }
    printf("%d",ans);
    return 0;
}

AcWing 1087

本题可以想到,设 f i f_i fi为前 i i i头牛合法选择的最大效益。首先可以不选, f i = f i − 1 f_i=f_{i-1} fi=fi1。考虑选择第 i i i头牛,可以枚举一个 j j j表示 i i i开始往前连续的牛的数量,保证 1 ≤ j ≤ k 1\le j\le k 1jk即可。则这一段牛就是 ∑ x = i − j + 1 i \displaystyle\sum_{x=i-j+1}^i x=ij+1i,为了保证不选第 i − j i-j ij头牛,那么就是用 f i − j − 1 f_{i-j-1} fij1。于是有状态转移方程 f i = max ⁡ ( f i − 1 , max ⁡ ( f i − j − 1 + ∑ x = i − j + 1 i , 1 ≤ j ≤ k ) ) f_i=\max(f_{i-1},\max(f_{i-j-1}+\displaystyle\sum_{x=i-j+1}^i,1\le j\le k)) fi=max(fi1,max(fij1+x=ij+1i,1jk))。有了状态转移方程,发现有一个求区间和的过程,用前缀和。发现中间的 j j j求的是一个 max ⁡ \max max,可以用单调队列维护。因为 f f f可以从 f 0 f_0 f0迭代,所以要先压入一个 0 0 0

#include<bits/stdc++.h>
using namespace std;
const int NN=1e5+4;
long long s[NN],f[NN];
int q[NN];
int main()
{
	int n,k;
	scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++)
    {
    	scanf("%lld",&s[i]);
		s[i]+=s[i-1];
	}
	int h=0,t=0;
    for(int i=1;i<=n;i++)
	{
	    if(q[h]<i-k)
			h++;
		if(h<=t)
    	    f[i]=max(f[i-1],f[q[h]-1]+s[i]-s[q[h]]);
	    while(h<=t&&f[q[t]-1]-s[q[t]]<=f[i-1]-s[i])
			t--;
	    q[++t]=i;
	}
    printf("%lld",f[n]);
    return 0;
}

AcWing 1088

可以选择顺时针或者逆时针,那么做两次即可。因为是环,所以再接一段序列。记录从一条路径经过所需要或者能获得的油,如果这样不断累加有一段小于 0 0 0就不可行,因为要计算一段,所以用前缀和。我们要求每一段序列之和都小于 0 0 0,所以只要要求每一段序列之和都大于等于 0 0 0即可。因为计算某个区间的和是 s r − s l − 1 s_r-s_{l-1} srsl1,所以找最大的一个 s l − 1 s_{l-1} sl1看看能不能使 s r − s l − 1 s_r-s_{l-1} srsl1小于 0 0 0即可。因为要找最大值,所以可以用优先级队列维护。因为本题是算的区间长度,可能减去 s 0 s_0 s0,所以要先压入一个 0 0 0

#include<bits/stdc++.h>
using namespace std;
const int NN=1e6+4;
long long s[2*NN];
bool ans[NN];
int p[NN],d[NN],a[2*NN],q[2*NN];
int main()
{
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++) 
	{
		scanf("%d%d",&p[i],&d[i]);
		a[i]=a[i+n]=p[i]-d[i];
	}
	for(int i=1;i<=2*n;i++)
		s[i]=s[i-1]+a[i];
	int h=0,t=0;
	for(int i=1;i<=2*n;i++)
	{
		if(q[h]<i-n+1)
			h++;
		while(s[q[t]]>=s[i]&&h<=t)
			t--;
		q[++t]=i;
		if(h<=t&&i>=n&&s[q[h]]>=s[i-n])
			ans[i-n+1]=true;
	}
	for(int i=1;i<n;i++)
		a[i]=a[i+n]=p[n-i+1]-d[n-i];
	a[n]=p[1]-d[n];
	for(int i=1;i<=2*n;i++)
		s[i]=s[i-1]+a[i];
	t=h=0;
	for(int i=1;i<=2*n;i++)
	{
		if(q[h]<i-n+1)
			h++;
		while(s[q[t]]>=s[i]&&h<=t)
			t--;
		q[++t]=i;
		if(h<=t&&i>=n&&s[q[h]]>=s[i-n])
			ans[2*n-i]=true;
	}
	for(int i=1;i<=n;i++) 
		if(ans[i])
			puts("TAK");
		else
			puts("NIE");
	return 0;
}

AcWing 1089

f i f_i fi为前 i i i个位置,且第 i i i个位置要放烽火台,最少放多少烽火台可行。因为只要中间没有隔 m m m个烽火台就是可行的,则 f i = min ⁡ ( f j , i > j , i − j < = m ) + a i f_{i}=\min(f_{j},i>j,i-j<=m)+a_i fi=min(fj,i>j,ij<=m)+ai。因为有一个求区间的最小值的过程,可以用单调队列维护。考虑计算答案,枚举满足要求的情况下,最后一个烽火台放在哪里,取这些方案的最小值即可。本题 f f f可能从 0 0 0迭代,要先压入一个 0 0 0

#include<bits/stdc++.h>
using namespace std;
const int NN=2*1e5+4;
int a[NN],f[NN],q[NN];
int main()
{
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	int h=0,t=0;
	for(int i=1;i<=n;i++)
	{
		if(q[h]<i-m)
			h++;
		if(h<=t)
		    f[i]=f[q[h]]+a[i];
		while(h<=t&&f[q[t]]>=f[i])
			t--;
		q[++t]=i;
	}
	int res=1e9;
	for(int i=n-m+1;i<=n;i++)
	    res=min(res,f[i]);
    printf("%d",res);
	return 0;
}

AcWing 1090

本题和上一题非常像。考虑如何转换成那一题,发现本题中缺少的就是连续不选择的限制,这也刚好是本题要求的,那么就二分答案。然后有了这个限制,按那一题的做法计算最少用的代价,看看能不能有一种方法在 t t t的时间内完成,这就是二分答案的 c h e c k check check函数。因为 f f f可以从 f 0 f_0 f0迭代,所以要先压入一个 0 0 0

#include<bits/stdc++.h>
using namespace std;
const int NN=5*1e4+4;
int n,t,a[NN],f[NN],q[NN];
bool check(int mid)
{
	int head=0,tail=0;
	for(int i=1;i<=n;i++)
	{
		if(q[head]+mid+1<i)
			head++;
		if(head<=tail)
		    f[i]=f[q[head]]+a[i];
		while(f[q[tail]]>=f[i]&&head<=tail)
			tail--;
		q[++tail]=i;
	}
	for(int i=n-mid;i<=n;i++)
	    if(f[i]<=t)
	        return true;
    return false;
}
int main()
{
	scanf("%d%d",&n,&t);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	int l=1,r=n;
	while(l<r)
	{
		int mid=l+(r-l)/2;
		if(check(mid))
			r=mid;
		else
			l=mid+1;
	}
	printf("%d",r);
	return 0;
}

AcWing 1091

本题是一个二维的滑动窗口。可以先竖着求一遍在 n n n行内的最小值和最大值,这样就相当于把 n n n行压缩成一个数,这样就压缩了一维。然后再横着求一边一维的滑动窗口即可。

#include<bits/stdc++.h>
using namespace std;
const int NN=1004;
int g[NN][NN],maxn[NN][NN],minn[NN][NN],q1[NN],q2[NN];
int main()
{
	int a,b,n,ans=1e9,h1,h2,t1,t2;
	scanf("%d%d%d",&a,&b,&n);
	for(int i=1;i<=a;i++)
		for(int j=1;j<=b;j++)
			scanf("%d",&g[i][j]);
	for(int i=1;i<=a;i++)
	{
		h1=0;
		h2=0;
		t1=-1;
		t2=-1;
		for(int j=1;j<=b;j++)
		{
			if(h1<=t1&&q1[h1]<=j-n)
				h1++;
			if(h2<=t2&&q2[h2]<=j-n)
				h2++;
			while(h1<=t1&&g[i][q1[t1]]<=g[i][j])
				t1--;
			while(h2<=t2&&g[i][q2[t2]]>=g[i][j])
				t2--;
			q1[++t1]=q2[++t2]=j;
			if(h1<=t1&&j>=n)
				maxn[i][j-n+1]=g[i][q1[h1]];
			if(h2<=t2&&j>=n)
				minn[i][j-n+1]=g[i][q2[h2]];
		}
	}
	for(int i=1;i<=b-n+1;i++)
	{
		h1=0;
		h2=0;
		t1=-1;
		t2=-1;
		for(int j=1;j<=a;j++)
		{
			if(h1<=t1&&q1[h1]<=j-n)
				h1++;
			if(h2<=t2&&q2[h2]<=j-n)
				h2++;
			while(h1<=t1&&maxn[q1[t1]][i]<=maxn[j][i])
				t1--;
			while(h2<=t2&&minn[q2[t2]][i]>=minn[j][i])
				t2--;
			q1[++t1]=q2[++t2]=j;
			if(h1<=t1&&h2<=t2&&j>=n)
				ans=min(ans,maxn[q1[h1]][i]-minn[q2[h2]][i]);
		}
	}
	printf("%d",ans);
	return 0;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值