单调队列优化dp小结

我癌症晚期级别的dp力还敢写起小结来了,令人忍俊不禁。

烽火传递

显然这是一道dp。

显然有方程

\[dp[i]=\{ dp[i-k+1 \sim i-1]\}_{min}+w[i] \]

k是区间长。

答案为 \(\{dp[n-k+1 \sim n\}_{min}\)

传统方法对 \(\{ dp[i-k+1 \sim i-1]\}_{min}\) 的处理是\(\Theta(k)\)的,会爆。所以人们提出:使用单调队列维护区间长度为 k 的 \(dp[i]\) 的最小值。
为了方便框住区间长,队列中的元素一般为下标,处理区间的时候用 \(q[t]-q[h]+1>k\) 判断是否超出区间长,超了弹队头即可。

单调队列即保证单调性的队列。因此,\(q[h]\) 所存下标一定是当前可取区间内的最优下标。每次用 \(q[h]\) 转移 \(dp[i]\) 后,将下标 i 也加入队列进行处理,操作形成一个闭环,比较有意思。

#include<bits/stdc++.h>
#define MAXN 200005
using namespace std;
int n,m,ans=1e9;
int w[MAXN];
int dp[MAXN];
int q[MAXN],t,h;
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)scanf("%d",&w[i]);
	for(int i=1;i<=n;i++){
		while(t>=h&&q[t]-q[h]+1>m)++h;
		dp[i]=dp[q[h]]+w[i];
		while(t>=h&&dp[i]<dp[q[t]])--t;
		q[++t]=i;
		if(i>=n-m+1)ans=min(ans,dp[i]);
	}
	printf("%d",ans);
	return 0;
}

一般情况下单调队列优化dp的步骤如下:

  • 处理区间大小,即弹队头,生成转移用最优解
  • 用最优解进行状态转移
  • 将当前下标加入队列成为待选解,也是处理最优解的过程

在代码也有体现。

[USACO Open11] 修剪草坪

分析一下,对于第 \(i\) 头奶牛,选择后会产生以下影响:

  • 扩大当前选奶牛方案的效率
  • 扩大连续奶牛数超过k的可能性

因此使用 \(dp[i][0],dp[i][1]\) 存储选择或不选择当前奶牛 i 能够获得的最大效率

如果不选这头牛,那么最大效率没有变化:

\[dp[i][0]=max(dp[i-1][0],dp[i-1][1]) \]

如果选择了这头牛:

\[dp[i][1]=\{dp[j=i-k \sim i-1][0]+\sum_{j+1}^{i}{w[i]}\}_{max} \]

我们不妨假设不仅选择了这头牛,还选择了从某头牛 j 以来的所有牛。当然,要保证 \(i-j<=k\),这期间奶牛的效率和可以用前缀和优化掉一重循环,然而对最优的 j 的寻找就需要用单调队列维护。

按照上文分析的步骤打码。

#include<bits/stdc++.h>
#define int long long
#define MAXN 100005
using namespace std;
int n,m;
int sum[MAXN];
int q[MAXN],h,t;
int dp[MAXN][2];
signed main(){
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++)scanf("%lld",&sum[i]),sum[i]+=sum[i-1];
	for(int i=1;i<=n;i++){
		while(t>=h&&q[t]-q[h]+1>m)++h;
		dp[i][0]=max(dp[i-1][1],dp[i-1][0]);
		dp[i][1]=dp[q[h]][0]+sum[i]-sum[q[h]];//选这头牛的最大效率就是自从j不选以来都选的最优解的最优解j`
		while(t>=h&&dp[i][0]-sum[i]>dp[q[t]][0]-sum[q[t]])--t;//上文中可以发现,前缀和部分是无法从max函数中提取出来的。
        //也就是说实际的最优答案由不选j以前的最优解和j+1到i的效率和共同组成,加入单调队列处理。
        //然而,未来要处理的dp[i][1]=dp[q[h]][0]+sum[i]-sum[q[h]]中,sum[i]是固定但不可预测的。
        //所以我们只要让dp[q[h]][0]-sum[q[h]]尽可能大即可,这就是为什么单调队列要维护一个似乎意义不明的负数。
		q[++t]=i;
	}
	printf("%lld",max(dp[n][0],dp[n][1]));//不知道最后一个要不要选,比对一下输出即可。
	return 0;
}

练习题

首先关于处理钢琴向某个方向滑动的问题,查看题解:

const int dx[5]={0,-1,1,0,0};
const int dy[5]={0,0,0,-1,1};

...

void solve(int len,int dir,int x,int y){
	h=0,t=-1;
	for(int i=1;;x+=dx[dir],y+=dy[dir],i++){

...

int main(){
...

	for(int i=1;i<=n;i++)scanf("%s",mp[i]+1);
	for(int i=1;i<=k;i++)scanf("%d%d%d",&opti[i].s,&opti[i].e,&opti[i].dir);
	for(int j=1;j<=k;j++){
		int l=opti[j].e-opti[j].s+1;
		if(opti[j].dir==1)for(int i=1;i<=m;i++)solve(l,opti[j].dir,n,i);
		if(opti[j].dir==2)for(int i=1;i<=m;i++)solve(l,opti[j].dir,1,i);
		if(opti[j].dir==3)for(int i=1;i<=n;i++)solve(l,opti[j].dir,i,m);
		if(opti[j].dir==4)for(int i=1;i<=n;i++)solve(l,opti[j].dir,i,1);
                                                                        

使用了搜索题走迷宫的方法。真他妈聪明

我们假设 \(dp[i][j]\) 表示钢琴在数据中滑到坐标 \((i,j)\) 时的最远滑动距离。

如果现在在横坐标上滑动了一段时间:

\[dp[i][j]=\{dp[k][j]+i-k+1\}_{max} \]

考虑题目中滑动时间的问题,\(i-k+1<=endtime-starttime\),所以单调队列代码\(q[t]-q[h]+1>k\)的判定中,k变为当前滑动时间段的长度。

纵坐标同理,略。

我们发现本题中时间顺序和施魔法是无意义的,\(n,m<=200\),每次枚举坐标转移即可,开始时,起始点 \((sx,sy)\) 的dp值改为0,其余改为 -inf 即可自动跑出滑动情况。反正又没让输出施魔法方案

另外,为了不讨论移动方向对最优下标 \(q[h]\) 放在 \(dp[i][j]\) 的哪个下标的影响,这里使用两个队列:下标队列 qloc 和数值队列 qval,维护 qloc 的单调性即可,qval 负责转移 \(dp[x][y]\)

#include<bits/stdc++.h>
#define MAXN 205
using namespace std;
int n,m,X,Y,k;
char mp[MAXN][MAXN];
struct node{
	int s,e,dir;
}opti[MAXN];
const int dx[5]={0,-1,1,0,0};
const int dy[5]={0,0,0,-1,1};
int dp[MAXN][MAXN];
int qval[MAXN],qloc[MAXN],h,t;
int ans;
void solve(int len,int dir,int x,int y){
	h=0,t=-1;
	for(int i=1;;x+=dx[dir],y+=dy[dir],i++){
		if(x<1||x>n||y<1||y>m)break;//出界了就不能dp当前方向了,面向题目背景:小精灵在钢琴到达边缘后一直施法直到该时间段结束即可
	//	printf("nowx:%d nowy:%d\n",x,y);
		if(mp[x][y]=='x')h=0,t=-1;//遇到障碍也不能再dp了,但是不影响我们继续转移别的移动情况,所以不用break。
		else{
			while(h<=t&&qval[t]+i-qloc[t]<dp[x][y])--t;
			qloc[++t]=i;
			qval[t]=dp[x][y];
			while(t>=h&&qloc[t]-qloc[h]>len)++h;
			dp[x][y]=qval[h]-qloc[h]+i;
        //放本题的原因就在这五行:dp与单调队列的进行顺序是要依题目变化的。
        //遇到障碍重置队列后需要用下一个点在上一段时间中的答案来更新它。所以要先放入队列。
        //h与t的初始化也有讲究,不过我现在还没明白,如果哪天明白再回来改。
		}
	}
}
int main(){
	scanf("%d%d%d%d%d",&n,&m,&X,&Y,&k);
	memset(dp,128,sizeof(dp));//memset只能初始化特殊值,128代表-inf
	dp[X][Y]=0;
	for(int i=1;i<=n;i++)scanf("%s",mp[i]+1);
	for(int i=1;i<=k;i++)scanf("%d%d%d",&opti[i].s,&opti[i].e,&opti[i].dir);
	for(int j=1;j<=k;j++){
		int l=opti[j].e-opti[j].s+1;
		if(opti[j].dir==1)for(int i=1;i<=m;i++)solve(l,opti[j].dir,n,i);
		if(opti[j].dir==2)for(int i=1;i<=m;i++)solve(l,opti[j].dir,1,i);
		if(opti[j].dir==3)for(int i=1;i<=n;i++)solve(l,opti[j].dir,i,m);
		if(opti[j].dir==4)for(int i=1;i<=n;i++)solve(l,opti[j].dir,i,1);
	}	
	for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)ans=max(ans,dp[i][j]);
	printf("%d",ans);
	return 0;
}
posted @ 2024-02-20 19:32  RVm1eL_o6II  阅读(5)  评论(0编辑  收藏  举报