这几天又把14年给做了,这是day1的第三题
飞扬的小鸟
Flappy Bird 是一款风靡一时的休闲手机游戏。玩家需要不断控制点击手机屏幕的频率来调节小鸟的飞行高度,让小鸟顺利通过画面右方的管道缝隙。如果小鸟一不小心撞到了水管或者掉在地上的话,便宣告失败。
为了简化问题,我们对游戏规则进行了简化和改编:
游戏界面是一个长为 n,高为 m 的二维平面,其中有k 个管道(忽略管道的宽度)。
小鸟始终在游戏界面内移动。小鸟从游戏界面最左边 任意整数高度位置出发,到达游戏界面最右边时,游戏完成。
小鸟每个单位时间沿横坐标方向右移的距离为 1,竖直移动的距离由玩家控制。如果点击屏幕,小鸟就会上升一定高度 X,每个单位时间可以点击多次,效果叠加; 如果不点击屏幕,小鸟就会下降一定高度 Y。小鸟位于横坐标方向不同位置时,上 升的高度 X 和下降的高度 Y 可能互不相同。
小鸟高度等于 0 或者小鸟碰到管道时,游戏失败。小鸟高度为 m 时,无法再上升。
现在,请你判断是否可以完成游戏。如果可以,输出最少点击屏幕数;否则,输出小鸟最多可以通过多少个管道缝隙。输入格式
第 1 行有 3 个整数 n,m,k,分别表示游戏界面的长度,高度和水管的数量,每两个 整数之间用一个空格隔开;
接下来的 n 行,每行 2 个用一个空格隔开的整数 X 和 Y,依次表示在横坐标位置 0~n-1 上玩家点击屏幕后,小鸟在下一位置上升的高度 X,以及在这个位置上玩家不点击屏幕时, 小鸟在下一位置下降的高度 Y。
接下来 k 行,每行 3 个整数 P,L,H,每两个整数之间用一个空格隔开。每行表示一个管道,其中 P 表示管道的横坐标,L 表示此管道缝隙的下边沿高度为 L,H 表示管道缝隙上边沿的高度(输入数据保证 P 各不相同,但不保证按照大小顺序给出)。输出格式
共两行。
第一行,包含一个整数,如果可以成功完成游戏,则输出 1,否则输出 0。 第二行,包含一个整数,如果第一行为 1,则输出成功完成游戏需要最少点击屏幕数,否则,输出小鸟最多可以通过多少个管道缝隙。样例输入1
10 10 6
3 9
9 9
1 2
1 3
1 2
1 1
2 1
2 1
1 6
2 2
1 2 7
5 1 5
6 3 5
7 5 8
8 7 9
9 1 3样例输出1
1
6样例输入2
10 10 4
1 2
3 1
2 2
1 8
1 8
3 2
2 1
2 1
2 2
1 2
1 0 2
6 7 9
9 1 4
3 8 10样例输出2
0
3限制
对于 30%的数据:5≤n≤10,5≤m≤10,k=0,保证存在一组最优解使得同一单位时间最多点击屏幕 3 次;
对于 50%的数据:5≤n≤20,5≤m≤10,保证存在一组最优解使得同一单位时间最多点击屏幕 3 次;
对于 70%的数据:5≤n≤1000,5≤m≤100;
对于 100%的数据:5≤n≤10000,5≤m≤1000,0≤k< n,0< X< m,0< Y< m,0< P< n,0≤L< H ≤m,L+1< H。
提示
如下图所示,蓝色直线表示小鸟的飞行轨迹,红色直线表示管道。
来源
NOIP2014 提高组 Day1
图片来源vijos
这道题一拿到手无论从题目条件还是数据范围来看都是dp,问题就是怎么dp,怎么不会超时,怎么考虑清楚各种情况。
一开始我以为f[1000][10000]要超,就没敢用,就写成了滚动数组,那么我这里就干脆将滚动数组算了。
我们定义f[i]表示在当前这个横坐标,纵坐标为i是的最小点击次数,如果在上一步不能到达i,那么就将i初始化为0。
那么怎么算?
很多同学会直接想到再在两重循环里面再加一重,来表示这一步跳几下,这样的话最坏时间复杂度就变成了O(nm*m/x)
根据测试,如果写成这样会T五个点,那么我们就必须要优化,这是我们想到了以前学习的完全背包,是的,这里可以用完全背包的思想优化
将这个想象成一个完全背包问题,一共有两个物品,一个是点击屏幕一次,一个是向下坠,点击屏幕这个物品有无限个,而向下坠只有1个,并且他们两个不能同时去,但也不能一个不取。
这是我们的状态转移方程实际上是很简单的,但我们还要考虑很多种情况。
状态转移方程:f[i]=min(f[i],f[i+y[i]],f[i-x[i]]+1)
但是不存在的情况不能算,而且如果他向上飞会撞墙,把这些情况都考虑了就可以了,dp不难,基本上都能想到。
代码放下面,写的有点复杂,可以用作参考。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define M 10005
#define N 1005
using namespace std;
int x[M],y[M],p[M],l[M],h[M],vis[N],g[M],d[N];
//vis表示是否这个点可以走到,d表示这个点是否需要临时数组来更新
//g[i]表示前i个有多少水管,p[i]表示在i这个位置有没有水管,对应的位置是什么
int n,m,k;
int f[N],ff[N],ans;
//f是我们dp的主要数组,ff是临时数组
int main()
{
#ifdef LOCAL
freopen("bird.in","r",stdin);
freopen("bird.out","w",stdout);
#endif
cin>>n>>m>>k;
for(int i=1;i<=n;i++)scanf("%d%d",&x[i],&y[i]);
for(int i=1;i<=k;i++)
{
int a;
scanf("%d%d%d",&a,&l[i],&h[i]);
p[a]=i;
}
for(int i=1;i<=n;i++)//初始化g
{
g[i]=g[i-1];
if(p[i])g[i]++;
}
//memset(f,-63,sizeof(f));
for(int i=1;i<=n;i++)
{
ans=2147000000;
memset(vis,0,sizeof(vis));
memset(ff,0,sizeof(ff));
memset(d,0,sizeof(d));
bool bo=false;
for(int j=1;j<=m;j++)//先判断向下坠的情况,再放进临时数组
{
if(f[j]<0)continue;
int down=j-y[i];
if(down>0&&(!p[i]||(p[i]&&down>l[p[i]]&&down<h[p[i]])))//判断是否有柱子
{
ff[down]=f[j];
d[down]=1;//更改标志,为后面更新做准备
}
}
for(int j=1;j<=m;j++)//在考虑向上飞的情况
{
if(f[j]<0)continue;
int up=j+x[i];
if(up>=m)up=m;//判断碰顶
if(f[up]<0||f[j]+1<f[up])f[up]=f[j]+1;//完全背包思想,重复向上更新
if(!p[i]||(p[i]&&up>l[p[i]]&&up<h[p[i]]))//判断是否能飞到i点
{
if(f[up]<0||f[j]+1<=f[up])//思想同上,向上更新
{
f[up]=f[j]+1;
vis[up]=1;
continue;
}
if(d[up])ff[up]=min(ff[up],f[j]+1);//如果无法向上更新存进临时数组
else ff[up]=f[j]+1; //因为有可能一会无法到达i点
d[up]=1;
}
}
for(int j=1;j<=m;j++)//将临时数组与f合并,更新f
if(d[j])
{
if(!vis[j])f[j]=ff[j];
else f[j]=min(ff[j],f[j]);
vis[j]=1;
}
for(int j=1;j<=m;j++)//判断是否可以继续向前飞
{
if(!vis[j])f[j]=-1;
else
{
bo=true;
ans=min(ans,f[j]);//求出最小步数(本来应该放在循环外,那样要多些一个循环,我懒就写里面了)
}
}
if(!bo)//是否需要弹出0
{
cout<<0<<'\n'<<g[i-1];
return 0;
}
}
cout<<1<<'\n'<<ans;
}
如果有什么问题,或错误,请在评论区提出,谢谢。