DP问题之单调队列优化

DP问题之单调队列优化

和数位DP一样,有固定的模板,特别注意下标弄清楚
如何判断表达式放在while循环前还是后?:看集合划分时包不包含i,如果包含就要先把i加进去再计算,如果最多到i-1就要先计算再把i加进去

P135. 最大子序和

i从1~n遍历,i-m<=j-1<=i-1,在这个范围内找到一个s[j-1]最小值(s[i]是定值,且满足s[i]-[j-1]最大),所以原问题转化为在i前面m的范围内,找到一个前缀和的最小值,再拿s[i]减去这个最小值,就是以i为终点,长度不超过m的子序列和的最大值了,正好符合单调队列模型
最大子序和
单调队列存的都是下标
前缀和数组一般默认s[0]=0

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
typedef long long LL;
const int N=3e5+10;
LL s[N];
int q[N];
int n,m;
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%lld",&s[i]);
		s[i]+=s[i-1];
	}
	LL res=-0x3f3f3f3f;
	int hh=0,tt=-1;
	q[++tt]=0;//先存储s[0],这样不用处理边界问题
	for(int i=1;i<=n;i++){
		while(hh<=tt&&q[hh]<i-m) hh++;
		res=max(res,s[i]-s[q[hh]]);
		while(hh<=tt&&s[q[tt]]>=s[i]) tt--;
		q[++tt]=i;
	}
	printf("%lld",res); 
	return 0;
}

P1088. 旅行问题

同环形区间DP把环形问题转为链形问题,做法也是复制一倍
把每个点的油量减去到下一个点的距离存到一个数组中,那么从1号点能成功到下一个1号点等价于中间每一个前缀和都大于等于0,所以问题转化为长度为n的区间内所有的前缀和是否大于等于0,其实就是判断长度为n的区间内前缀和的最小值是否大于等于0,这正好是单调队列模型
旅行问题1
旅行问题2

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long LL;
const int N=2e6+10;
int n;
int o[N],d[N];
LL s[N];
int q[N];
bool st[N];
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d%d",&o[i],&d[i]);
	}
	//顺时针 
	for(int i=1;i<=n;i++) s[i]=s[i+n]=o[i]-d[i];
	for(int i=1;i<=n+n;i++) s[i]+=s[i-1];
	int hh=0,tt=-1;
	q[++tt]=n*2+1;
	for(int i=n+n;i>=1;i--){
		while(hh<=tt&&q[hh]>i+n-1) hh++;
		while(hh<=tt&&s[q[tt]]>=s[i]) tt--;
		q[++tt]=i;
		if(i<=n&&s[q[hh]]-s[i-1]>=0) st[i]=true;
	}
	//逆时针 
	d[0]=d[n];
	for(int i=n;i>=1;i--) s[i]=s[i+n]=o[i]-d[i-1];
	for(int i=n+n;i>=1;i--) s[i]+=s[i+1];
	hh=0,tt=-1;
	q[++tt]=0;
	for(int i=1;i<=n+n;i++){
		while(hh<=tt&&q[hh]<i-n+1) hh++;
		while(hh<=tt&&s[q[tt]]>=s[i]) tt--;
		q[++tt]=i;
		if(i>=n+1&&s[q[hh]]-s[i+1]>=0) st[i-n]=true;
	}
	for(int i=1;i<=n;i++){
		if(st[i]) puts("TAK");
		else puts("NIE");
	}
	return 0;
}

P1089. 烽火传递

烽火传递
原问题转化为在i前面长度为m的区间中,找到一个最小的f[j],符合单调队列模型

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=200010;
int n,m;
int q[N];
int w[N];
int f[N];
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&w[i]);
	int hh=0,tt=-1;
	q[++tt]=0;
	for(int i=1;i<=n;i++){
		while(hh<=tt&&q[hh]<i-m) hh++;
		f[i]=f[q[hh]]+w[i];
		while(hh<=tt&&f[q[tt]]>=f[i]) tt--;
		q[++tt]=i;
	}
	int res=1e9;
	//答案一定在最后长度为m的区间中选,不然就不合法了 
	for(int i=n-m+1;i<=n;i++) res=min(res,f[i]);
	printf("%d",res);
	return 0;
}

P1090. 绿色通道

上一题+二分(具有两段性)
绿色通道1
绿色通道2
上一题最长只能有k-1个连续的空格,而这题可以有k个(0<=k<=n)
对于最优解k来说,它右边的点所需时间都小于等于t,所以都满足要求,不是最优解而已,而它左边的点都不把满足要求,因此可以在空题长度不超过k的情况下,求一下最小时间是不是小于等于t,如果小于等于t说明这个长度ok,就可以进一步缩小区间(选小于等于k的区间),如果大于t,就要扩大区间(选大于k的区间)

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=5e4+10;
int w[N];
int f[N],q[N];
int n,m;
bool check(int k){
	int hh=0,tt=-1;
	q[++tt]=0;
	for(int i=1;i<=n;i++){
		while(hh<=tt&&q[hh]<i-k-1) hh++;
		f[i]=f[q[hh]]+w[i];
		while(hh<=tt&&f[q[tt]]>=f[i]) tt--;
		q[++tt]=i;
	}
	int res=0x3f3f3f3f;
	//答案一定在最后长度为k+1的区间中选,不然就不合法了 
	for(int i=n-k;i<=n;i++) res=min(res,f[i]);
	return res<=m;
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&w[i]);
	int l=0,r=n;
	while(l<r){
		int mid=l+r>>1;
		if(check(mid)) r=mid;
		else l=mid+1;
	}
	printf("%d",l);
	return 0;
}

P1087. 修剪草坪

修剪草坪1
集合划分:对于每头牛i来说,有选和不选两种方案,不选第i头牛 , 则f[i] = f[i-1];选第i头牛,又可以按照连续长度划分为:长度为1,长度为2,长度为3,长度为k
当我们所选的牛连续长度为j时 (1<=j<=k),所选的牛可以是:
修剪草坪2
注意第i-j头牛不选,因为如果选了连续长度就大于j了,故选择第i头牛方程为:f[i]= f[i-j-1]+s[i]-s[i-j],对方程进行变换
修建草坪3
综上,状态转移方程为f[i]=max(f[i-1],g[i-j]+s[i]) (1<=j<=k)
最大值只依赖于g[i-j],因此定义一个长度为k的单调队列维护g[i-j],使得队头的g[i-j]最大,1<=j<=k,其实就是在求i前面长度为k的滑动窗口的最大值,因为1<=j<=k,所以i-k<=i-j<=i-1,所以队头小于i-k时要滑动
边界问题处理:g[0]=f[-1]-s[0],此时i=j,说明此时取满了整个队列长度仍小于等于k,此时f[i]=g[0]+s[i]=s[i],因此g[0]=0

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long LL;
const int N=1e5+10;
int n,k;
LL s[N];
LL f[N];
int q[N]; 
LL g(int i){
	if(!i) return 0; 
	return f[i-1]-s[i];
}
int main(){
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;i++){
		scanf("%lld",&s[i]);
		s[i]+=s[i-1];
	}
	int hh=0,tt=-1;
	q[++tt]=0;//先存储g[0] 
	for(int i=1;i<=n;i++){
		while(hh<=tt&&q[hh]<i-k) hh++;
		f[i]=max(f[i-1],g(q[hh])+s[i]);
		while(hh<=tt&&g(q[tt])<=g(i)) tt--;
		q[++tt]=i;
	}
	printf("%lld",f[n]);
	return 0;
} 

P1091. 理想的正方形

二维单调队列模板
l理想的正方形2
理想的正方形2
二维单调队列1
二维单调队列2
AcWing

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1010;
int n,m,k;
int w[N][N];
int row_max[N][N],row_min[N][N];
int q[N];
void get_min(int a[],int b[],int tot){
	int hh=0,tt=-1;
	for(int i=1;i<=tot;i++){
		while(hh<=tt	&&q[hh]<i-k+1) hh++;
		while(hh<=tt&&a[q[tt]]>=a[i]) tt--;
		q[++tt]=i;
		b[i]=a[q[hh]];
	}
}
void get_max(int a[],int b[],int tot){
	int hh=0,tt=-1;
	for(int i=1;i<=tot;i++){
		while(hh<=tt&&q[hh]<i-k+1) hh++;
		while(hh<=tt&&a[q[tt]]<=a[i]) tt--;
		q[++tt]=i;
		b[i]=a[q[hh]];
	}
}
int main(){
	scanf("%d%d%d",&n,&m,&k);
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			scanf("%d",&w[i][j]);
		}
	}
	for(int i=1;i<=n;i++){
		get_min(w[i],row_min[i],m);
		get_max(w[i],row_max[i],m);
	}
	int res=1e9;
	int a[N],b[N],c[N];
	for(int i=k;i<=m;i++){//枚举列 
		//因为列方向数组不连续,我们要把它们单独放在连续的数组里  
		for(int j=1;j<=n;j++) a[j]=row_min[j][i];
		get_min(a,b,n);
		for(int j=1;j<=n;j++) a[j]=row_max[j][i];
		get_max(a,c,n);
		for(int j=k;j<=n;j++) res=min(res,c[j]-b[j]);
	}
	printf("%d",res); 
	return 0;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值