单调队列优化DP 习题
放假
题目大意
经过几个月辛勤的工作,
假期可以在
但是奶牛的要求非常苛刻,假期不能短于
假期也不能超过
解题思路
-
设
表示第 天作为假期结尾的最大享受指数, 。显然有 -
把
提出来就是要求 的最小值。当 增大 时, 的取值范围两端同样增加 。所以考虑单调队列维护最小值,建立单调递增队列,及时排除无用决策
代码
#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;
}
松果
题目大意
有
第
它想吃尽量多的松果,但它不想在地上走,而只想从一棵树跳到另一棵树上。
松鼠的体力有个上限,每次不能跳的太远,也不能跳太多次。
设
每当它跳到一棵树上,就会把那棵树上的松果全部都吃了。
问它最多能吃到多少个松果?
解题思路
-
设
表示到了第 棵树跳了 次后所能吃到的松果数量最大值。显然即求
的最大值 -
建立单调递减队列维护即可
代码
#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] 瑰丽华尔兹
(题目传送门)
题目大意
题目太长了
不妨认为舞厅是一个
艾米丽是个天使,她知道每段时间的船体的倾斜情况。她想使钢琴在舞厅里滑行的路程尽量长,这样 1900 会非常高兴,同时也有利于治疗托尼的晕船。但艾米丽还太小,不会算,所以希望你能帮助她。
解题思路
-
考虑朴素算法,设
表示 时间时移动到 的最大移动距离,显然其中
表示方向, 表示该时间行、列的移动方向。时间复杂度 -
考虑按照时间段来进行
。因为每个时间段只能往一个方向移动,所以可以将每个时间段的 简化成相对起点的距离 ,那么就有 -
将
提出来得 -
设
,则 -
因此我们可以建立一个与起点相对位置单调递增,
单调递减的单调队列来进行维护
代码
#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 普及组] 道路游戏
(题目传送门)
题目大意
又是超级长的题目
小新正在玩一个简单的电脑游戏。
游戏中有一条环形马路,马路上有
游戏过程中,每个单位时间内,每段马路上都会出现一些金币,金币的数量会随着时间发生变化,即不同单位时间内同一段马路上出现的金币数量可能是不同的。小新需要机器人的帮助才能收集到马路上的金币。所需的机器人必须在机器人工厂用一些金币来购买,机器人一旦被购买,便会沿着环形马路按顺时针方向一直行走,在每个单位时间内行走一次,即从当前所在的机器人工厂到达相邻的下一个机器人工厂,并将经过的马路上的所有金币收集给小新,例如,小新在
以下是游戏的一些补充说明:
-
游戏从小新第一次购买机器人开始计时。
-
购买机器人和设定机器人的行走次数是瞬间完成的,不需要花费时间。
-
购买机器人和机器人行走是两个独立的过程,机器人行走时不能购买机器人,购买完机器人并且设定机器人行走次数之后机器人才能行走。
-
在同一个机器人工厂购买机器人的花费是相同的,但是在不同机器人工厂购买机器人的花费不一定相同。
-
购买机器人花费的金币,在游戏结束时再从小新收集的金币中扣除,所以在游戏过程中小新不用担心因金币不足,无法购买机器人而导致游戏无法进行。也因为如此,游戏结束后,收集的金币数量可能为负。
现在已知每段马路上每个单位时间内出现的金币数量和在每个机器人工厂购买机器人需要的花费,请你告诉小新,经过
解题思路
-
设
表示在第 个工厂购买机器人的费用 -
将工厂按
编号,这样可以将第 条路径上的金币转移到第 个工厂上,设 表示第 个工厂第 个时刻的金币 -
构造前缀和,因为与时间和地点两个维度有关,所以设
表示第 个时刻到第 个工厂的价值和,则 -
设
表示第 个时刻能收集到的最大金币,枚举此刻所在的工厂位置 ,以及走过的步数 ,则有时间复杂度
-
考虑优化。设
,则 -
我们发现转移时
数组是从对角线转移而来,所以考虑建立 个单调队列来维护每条对角线上的最大值 -
我们设
表示第 个时刻第 个工厂属于哪条对角线。初始化令 ,转移时
代码
#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 普及组] 跳房子
(题目传送门)
题目大意
跳房子,也叫跳飞机,是一种世界性的儿童游戏,也是中国民间传统的体育游戏之一。
跳房子的游戏规则如下:
在地面上确定一个起点,然后在起点右侧画
玩家每次都必须跳到当前位置右侧的一个格子内。玩家可以在任意时刻结束游戏,获得的分数为曾经到达过的格子中的数字之和。
现在小 R 研发了一款弹跳机器人来参加这个游戏。但是这个机器人有一个非常严重的缺陷,它每次向右弹跳的距离只能为固定的
现在小 R 希望获得至少
解题思路
-
显然可以二分答案
,现在考虑check
函数如何实现 -
设
表示调到第 个格子所能获得的最大分数,初始化 -
设
表示每个格子距离起点的位置, 表示格子里的数字 -
设
, 。显然对于一个位置 ,他能跳到的位置就是 。因此我们可以轻松得出状态转移方程 -
显然用一个单调队列维护即可
代码
#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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】