单调队列优化DP 习题

放假

题目大意

经过几个月辛勤的工作,FJ 决定让奶牛放假。

假期可以在 1n 天内任意选择一段(需要连续),每一天都有一个享受指数 a

但是奶牛的要求非常苛刻,假期不能短于 p 天,否则奶牛不能得到足够的休息;

假期也不能超过 q 天,否则奶牛会玩的腻烦。

FJ 想知道奶牛们能获得的最大享受指数

解题思路

  • f[i] 表示第 i 天作为假期结尾的最大享受指数,sum[i]=a1++ai。显然有

    f[i]=maxiq+1jip+1{sum[i]sum[j1]}

  • sum[i] 提出来就是要求 sum[j1] 的最小值。当 i 增大 1 时,j 的取值范围两端同样增加 1。所以考虑单调队列维护最小值,建立单调递增队列,及时排除无用决策

代码

#include<bits/stdc++.h>
#define LL long long
using namespace std;

const int N=100010;

int n,p,q,a[N];
LL sum[N],ans=-1e14;
int qq[N],h,t;

int main()
{
	scanf("%d%d%d",&n,&p,&q);
	for(int i=1; i<=n; i++)
		scanf("%d",&a[i]),sum[i]=sum[i-1]+(LL)a[i];
	
	h=1;  t=0;
	for(int i=p; i<=n; i++)
	{
		while(h<=t && sum[qq[t]-1]>=sum[i-p])
			t--;
		qq[++t]=i-p+1;
		while(h<=t && qq[h]<i-q+1)
			h++;
        
   		ans=max(ans,sum[i]-sum[qq[h]-1]);
	}
	
	printf("%lld",ans);
	
	return 0;
}

松果

题目大意

n 棵松果树从左往右排一行,桃桃是一只松鼠,它现在在第 1 棵松果树上。

i 棵松果树有 ai 棵松果,它距离第 1 棵树的距离为 bi,保证 b1=0bi 递增

它想吃尽量多的松果,但它不想在地上走,而只想从一棵树跳到另一棵树上。

松鼠的体力有个上限,每次不能跳的太远,也不能跳太多次。

d 表示松鼠每次跳跃的最大距离,m 表示松鼠最多能跳跃的次数。

每当它跳到一棵树上,就会把那棵树上的松果全部都吃了。

问它最多能吃到多少个松果?

解题思路

  • f[i][j] 表示到了第 i 棵树跳了 j 次后所能吃到的松果数量最大值。显然

    f[i][j]=maxbibkd{f[k][j1]+ai}

    即求 f[k][j1] 的最大值

  • 建立单调递减队列维护即可

代码

#include<bits/stdc++.h>
#define LL long long
using namespace std;

const int N=2010;

int n,d,m,a[N],s[N];
int q[N][N],h[N],t[N];
LL f[N][N],ans;

int main()
{
	scanf("%d%d%d",&n,&d,&m);
	for(int i=1; i<=n; i++)
		scanf("%d%d",&a[i],&s[i]);
		
	for(int i=0; i<=m; i++)
		h[i]=1;
	f[1][0]=a[1];
	q[0][++t[0]]=1;
	
	for(int i=2; i<=n; i++)
	{
		for(int j=0; j<m; j++)
		{
			while(h[j]<=t[j] && s[i]-s[q[j][h[j]]]>d)
				h[j]++;
			f[i][j+1]=f[q[j][h[j]]][j]+(LL)a[i];

		}
		
		for(int j=1; j<=m; j++)
		{
			while(h[j]<=t[j] && f[i][j]>f[q[j][t[j]]][j])
				t[j]--;
			q[j][++t[j]]=i;
		}
	}
	
	for(int i=1; i<=n; i++)
		for(int j=1; j<=m; j++)
			ans=max(ans,f[i][j]);
	
	printf("%lld",ans);
	
	return 0;
}

[NOI2005] 瑰丽华尔兹

题目传送门

题目大意

题目太长了

不妨认为舞厅是一个 NM 列的矩阵,矩阵中的某些方格上堆放了一些家具,其他的则是空地。钢琴可以在空地上滑动,但不能撞上家具或滑出舞厅,否则会损坏钢琴和家具,引来难缠的船长。每个时刻,钢琴都会随着船体倾斜的方向向相邻的方格滑动一格,相邻的方格可以是向东、向西、向南或向北的。而艾米丽可以选择施魔法或不施魔法:如果不施魔法,则钢琴会滑动;如果施魔法,则钢琴会原地不动。

艾米丽是个天使,她知道每段时间的船体的倾斜情况。她想使钢琴在舞厅里滑行的路程尽量长,这样 1900 会非常高兴,同时也有利于治疗托尼的晕船。但艾米丽还太小,不会算,所以希望你能帮助她。

解题思路

  • 考虑朴素算法,设 f[t][x][y] 表示 t 时间时移动到 (x,y) 的最大移动距离,显然

    f[t][x][y]=max{f[t1][x][y],f[t1][xdx[d]][ydy[d]]}

    其中 d 表示方向,dx[d],dy[d] 表示该时间行、列的移动方向。时间复杂度 O(TNM)

  • 考虑按照时间段来进行 dp。因为每个时间段只能往一个方向移动,所以可以将每个时间段的 x,y 简化成相对起点的距离 i,那么就有

    f[i]=max{f[j]+ij}

  • i 提出来得

    f[i]i=max{f[j]j}

  • g[i]=f[i]i,则 g[i]=max{g[j]}

  • 因此我们可以建立一个与起点相对位置单调递增,g 单调递减的单调队列来进行维护

代码

#include<bits/stdc++.h>
using namespace std;

const int N=210;
const int dx[4]={-1,1,0,0},dy[4]={0,0,-1,1};

int n,m,sx,sy,k,h,t;
int f[N][N],ans;
char c[N][N];
pair <int,int> q[N];

void calc(int x,int y,int d,int len) //(x,y)表示起点,d表示方向,len表示最大移动距离
{
	h=1;  t=0;
	int delta=0; //表示与起点相对位置的变化量
	
	while(x>=1 && x<=n && y>=1 && y<=m)
	{
		if(c[x][y]=='x')
			h=1,t=0;
		else
		{
			while(h<=t && q[t].second<=f[x][y]-delta)
				t--;
			q[++t]=make_pair(delta,f[x][y]-delta);
			while(h<=t && delta-q[h].first>len)
				h++;
				
			f[x][y]=q[h].second+delta;
			ans=max(ans,f[x][y]);
		}
		
		x+=dx[d];  y+=dy[d];
		delta++;
	}
} 

int main()
{
	memset(f,~0x3f,sizeof(f));
	
	scanf("%d%d%d%d%d",&n,&m,&sx,&sy,&k);
	for(int i=1; i<=n; i++)
		scanf("%s",c[i]+1);	
	
	f[sx][sy]=0;
	for(int i=1; i<=k; i++)
	{
		int s,t,d;
		scanf("%d%d%d",&s,&t,&d);
		
		switch(d)
		{
			case 1:
				for(int j=1; j<=m; j++)
					calc(n,j,0,t-s+1);
				break;
			case 2:
				for(int j=1; j<=m; j++)
					calc(1,j,1,t-s+1);
				break;
			case 3:
				for(int j=1; j<=n; j++)
					calc(j,m,2,t-s+1);
				break;	
			default:
				for(int j=1; j<=n; j++)
					calc(j,1,3,t-s+1);
		}
	}
	
	printf("%d",ans);
	
	return 0;
}

[NOIP2009 普及组] 道路游戏

(题目传送门)

题目大意

又是超级长的题目

小新正在玩一个简单的电脑游戏。

游戏中有一条环形马路,马路上有 n 个机器人工厂,两个相邻机器人工厂之间由一小段马路连接。小新以某个机器人工厂为起点,按顺时针顺序依次将这 n 个机器人工厂编号为 1n,因为马路是环形的,所以第 n 个机器人工厂和第 1 个机器人工厂是由一段马路连接在一起的。小新将连接机器人工厂的这 n 段马路也编号为 1n,并规定第 i 段马路连接第 i 个机器人工厂和第 i+1 个机器人工厂(1in1),第 n 段马路连接第 n 个机器人工厂和第 1 个机器人工厂。

游戏过程中,每个单位时间内,每段马路上都会出现一些金币,金币的数量会随着时间发生变化,即不同单位时间内同一段马路上出现的金币数量可能是不同的。小新需要机器人的帮助才能收集到马路上的金币。所需的机器人必须在机器人工厂用一些金币来购买,机器人一旦被购买,便会沿着环形马路按顺时针方向一直行走,在每个单位时间内行走一次,即从当前所在的机器人工厂到达相邻的下一个机器人工厂,并将经过的马路上的所有金币收集给小新,例如,小新在 i1in)号机器人工厂购买了一个机器人,这个机器人会从 i 号机器人工厂开始,顺时针在马路上行走,第一次行走会经过 i 号马路,到达 i+1 号机器人工厂(如果 i=n,机器人会到达第 1 个机器人工厂),并将 i 号马路上的所有金币收集给小新。游戏中,环形马路上不能同时存在 2 个或者 2 个以上的机器人,并且每个机器人最多能够在环形马路上行走 p 次。小新购买机器人的同时,需要给这个机器人设定行走次数,行走次数可以为 1 p 之间的任意整数。当马路上的机器人行走完规定的次数之后会自动消失,小新必须立刻在任意一个机器人工厂中购买一个新的机器人,并给新的机器人设定新的行走次数。

以下是游戏的一些补充说明:

  1. 游戏从小新第一次购买机器人开始计时。

  2. 购买机器人和设定机器人的行走次数是瞬间完成的,不需要花费时间。

  3. 购买机器人和机器人行走是两个独立的过程,机器人行走时不能购买机器人,购买完机器人并且设定机器人行走次数之后机器人才能行走。

  4. 在同一个机器人工厂购买机器人的花费是相同的,但是在不同机器人工厂购买机器人的花费不一定相同。

  5. 购买机器人花费的金币,在游戏结束时再从小新收集的金币中扣除,所以在游戏过程中小新不用担心因金币不足,无法购买机器人而导致游戏无法进行。也因为如此,游戏结束后,收集的金币数量可能为负。

现在已知每段马路上每个单位时间内出现的金币数量和在每个机器人工厂购买机器人需要的花费,请你告诉小新,经过 m 个单位时间后,扣除购买机器人的花费,小新最多能收集到多少金币。

解题思路

  • cost[i] 表示在第 i 个工厂购买机器人的费用

  • 将工厂按 0n1 编号,这样可以将第 i 条路径上的金币转移到第 imodn 个工厂上,设 a[i][j] 表示第 i 个工厂第 j 个时刻的金币

  • 构造前缀和,因为与时间和地点两个维度有关,所以设 sum[i][j] 表示第 i 个时刻到第 j 个工厂的价值和,则 sum[i][j]=sum[i1][(j1+n)modn]+a[j][i]

  • f[i] 表示第 i 个时刻能收集到的最大金币,枚举此刻所在的工厂位置 j,以及走过的步数 k,则有

    f[i]=max1kp{f[ik]+sum[i][j]sum[ik][jk]cost[jk]}

    时间复杂度 O(n3)

  • 考虑优化。设 g[i][j]=f[i]sum[i][j]cost[j],则

    f[i]=max1kp{g[ik][jk]}+sum[i][j]

  • 我们发现转移时 g 数组是从对角线转移而来,所以考虑建立 n 个单调队列来维护每条对角线上的最大值

  • 我们设 line[i][j] 表示第 i 个时刻第 j 个工厂属于哪条对角线。初始化令 line[0][(j1+n)modn]=j,转移时 line[i][j]=line[i1][(j1+n)modn]

代码

#include<bits/stdc++.h>
using namespace std;

const int N=1010;

int n,m,p,a[N][N],cost[N];
int sum[N][N],f[N],line[N][N];
int h[N],t[N];
pair <int,int> q[N][N];

int main()
{
	memset(f,~0x3f,sizeof(f));
	
	scanf("%d%d%d",&n,&m,&p);
	for(int i=1; i<=n; i++)
		for(int j=1; j<=m; j++)
			scanf("%d",&a[i%n][j]);
	for(int i=0; i<n; i++)
		scanf("%d",&cost[i]);
	
	for(int i=1; i<=m; i++)
		for(int j=0; j<n; j++)
			sum[i][j]=sum[i-1][(j-1+n)%n]+a[j][i];
			
	for(int j=0; j<n; j++)
		line[0][(j-1+n)%n]=j;
			
	for(int i=0; i<n; i++)
	{
		int tmp=line[0][i];
		h[tmp]=1;
		q[tmp][++t[tmp]]=make_pair(0,-cost[i]);
	}
		
	for(int i=1; i<=m; i++)
	{
		for(int j=0; j<n; j++)
		{
			line[i][j]=line[i-1][(j-1+n)%n];
			int tmp=line[i][j];
			while(h[tmp]<=t[tmp] && q[tmp][h[tmp]].first<i-p)
				h[tmp]++;
			
			f[i]=max(f[i],q[tmp][h[tmp]].second+sum[i][j]);
		}
		 
		for(int j=0; j<n; j++)
		{
			int tmp=line[i][j];
			while(h[tmp]<=t[tmp] && q[tmp][t[tmp]].second<f[i]-sum[i][j]-cost[j])
				t[tmp]--;
			q[tmp][++t[tmp]]=make_pair(i,f[i]-sum[i][j]-cost[j]);
		}
	}
	
	printf("%d",f[m]);
	
	return 0;
}

[NOIP2017 普及组] 跳房子

题目传送门

题目大意

跳房子,也叫跳飞机,是一种世界性的儿童游戏,也是中国民间传统的体育游戏之一。

跳房子的游戏规则如下:

在地面上确定一个起点,然后在起点右侧画 n 个格子,这些格子都在同一条直线上。每个格子内有一个数字(整数),表示到达这个 格子能得到的分数。玩家第一次从起点开始向右跳,跳到起点右侧的一个格子内。第二次再从当前位置继续向右跳,依此类推。规则规定:

玩家每次都必须跳到当前位置右侧的一个格子内。玩家可以在任意时刻结束游戏,获得的分数为曾经到达过的格子中的数字之和。

现在小 R 研发了一款弹跳机器人来参加这个游戏。但是这个机器人有一个非常严重的缺陷,它每次向右弹跳的距离只能为固定的 d。小 R 希望改进他的机器人,如果他花 g 个金币改进他的机器人,那么他的机器人灵活性就能增加 g,但是需要注意的是,每 次弹跳的距离至少为 1。具体而言,当 g<d 时,他的机器人每次可以选择向右弹跳的距离为 dg,dg+1,dg+2,,d+g1,d+g;否则当 gd 时,他的机器人每次可以选择向右弹跳的距离为 1,2,3,,d+g1,d+g

现在小 R 希望获得至少 k 分,请问他至少要花多少金币来改造他的机器人。

解题思路

  • 显然可以二分答案 g,现在考虑 check 函数如何实现

  • f[i] 表示调到第 i 个格子所能获得的最大分数,初始化 f[0]=0

  • posi 表示每个格子距离起点的位置,ai 表示格子里的数字

  • l=min(dmid,1)r=max(d+mid,posn)。显然对于一个位置 i,他能跳到的位置就是 [posi+l,posi+r]。因此我们可以轻松得出状态转移方程

    f[i]=maxposirposjposil{f[j]+ai}

  • 显然用一个单调队列维护即可

代码

#include<bits/stdc++.h>
#define LL long long
using namespace std;

const int N=500010;
const LL INF=5e10;

int n,d,k,pos[N],a[N];
LL f[N];
int q[N],h,t;

bool check(int mid)
{
	int l=max(d-mid,1),r=min(d+mid,pos[n]);
	h=1;  t=0;
	memset(f,~0x3f,sizeof(f));
	
	int j=0;
	f[0]=0;
	for(int i=1; i<=n; i++)
	{
		while(j<i && pos[i]-pos[j]>=l)
		{
			if(f[j]>-INF)
			{
				while(h<=t && f[q[t]]<=f[j])
					t--;
				q[++t]=j;
			}
			j++;
		}
		
		while(h<=t && pos[i]-pos[q[h]]>r)
			h++;
		
		if(h<=t)
			f[i]=f[q[h]]+a[i];
		if(f[i]>=k)
			return 1;
	}
	
	return 0;
}

int find()
{
	int lo=0,hi=pos[n]+1;
	while(lo+1<hi)
	{
		int mid=(lo+hi)>>1;
		if(check(mid))
			hi=mid;
		else
			lo=mid;
	}
	
	return hi;
}

int main()
{
	scanf("%d%d%d",&n,&d,&k);
	for(int i=1; i<=n; i++)
		scanf("%d%d",&pos[i],&a[i]);
		
	int ans=find();
	if(ans<=pos[n])
		printf("%d",ans);
	else
		printf("-1");
	
	return 0;
}
posted @   xishanmeigao  阅读(48)  评论(0编辑  收藏  举报
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示