C-可怕的宇宙射线
一、题目描述
时间与内存限制 1000ms 2632144KB
众所周知,瑞神已经达到了CS本科生的天花板,但殊不知天外有天,人外有苟。在浩瀚的宇宙中,存在着一种叫做苟狗的生物,这种生物天生就能达到人类研究生的知识水平,并且天生擅长CSP,甚至有全国第一的水平!但最可怕的是,它可以发出宇宙射线!宇宙射线可以摧毁人的智商,进行降智打击!
宇宙射线会在无限的二维平面上传播(可以看做一个二维网格图),初始方向默认向上。宇宙射线会在发射出一段距离后分裂,向该方向的左右45°方向分裂出两条宇宙射线,同时威力不变!宇宙射线会分裂 次,每次分裂后会在分裂方向前进ai个单位长度。现在瑞神要带着他的小弟们挑战苟狗,但是瑞神不想让自己的智商降到普通本科生zjm那么菜的水平,所以瑞神来请求你帮他计算出共有多少个位置会被"降智打击"。
输入说明
输入第一行包含一个正整数 n(n<=30),表示宇宙射线会分裂n 次
第二行包含n个正整数a1,a2···an ,第ai (ai<=5)个数表示第 i次分裂的宇宙射线会在它原方向上继续走多少个单位长度
输出说明
输出一个数ans ,表示有多少个位置会被降智打击
样例输入
4
4 2 2 3
样例输出
39
数据点说明
数据点 n
10% <=10
40% <=20
100% <=30
样例解释
二、思路与算法
这道题目前看比较难,因为要处理很多环节,再把它们组合在一起形成答案。
-
如何存储所有的点?
答:用int数组存储,根据给出的数据范围得知,最多分裂30次,每次最多扩散5个单位,所以每次就算一直向上走,也不会超过150个单位。
所以,为了用数组存储,要让所有位置x,y值都为非负数,则起始点设为(150,150),整个数组横纵坐标为300*300。(比这个大小更大也完全可以) -
如何标记一个点是否来过?(去重)
答:用二维数组vis记录,每到一个点,检查是否到达过,未到达过则打上标记。 -
如何把要分裂的点和其他点区分开?
答:用另外一个四维数组a记录所有要分裂的位置。 -
如何确定下一次分裂的方向?
答:也在四维数组a中记录,用一个int型数据标记。
这里涉及到找分裂方向的规律的问题。
dfs中总会有一个趋向,在有多种选择时,先遍历哪个方向,这个趋向可以随便选,选出来就可以。
这里我们选择趋向右侧。
向上——>右上——>向右——>右下——>向下——>左下——>向左——>左上——>向上
画出图来就是一个正八边形。
这个8个方向构成循环,是我们确定分裂方向的手段。
后面对左侧分裂进行讨论时,也是这样一个正八边形,只是起点不同,终点不同,前后相接的次序维持不变。
一般,我们dfs时都有设2个数组:dx、dy,用来遍历某个位置所有相邻位置。
那么,这里我们有8个相邻元素,并且注意!因为分裂方向有规律有序改变,所以dx、dy的内容也要符合这种变化顺序!
每次我们用四维数组里记录的int型与dx、dy顺序结合起来判断。 -
如何剪枝才能使时间复杂度尽可能地小?
答:记忆性剪枝!某个点已经分裂过,则不要再从这个点分裂了。因为如果现在的情况以前遇到过,那么再继续下去也和以前的经历完全一样。 -
如何记忆性剪枝而不遗漏点?
答:if判断+四维数组记录是否从这个位置分裂过。
三、代码实现
要重点理解怎么剪枝、怎么遍历、怎么统计所有点而不重复!
#include<cstdio>
using namespace std;
int dx[8]={0,1,1,1,0,-1,-1,-1};
int dy[8]={1,1,0,-1,-1,-1,0,1};//优先右侧的分裂,是一个八边形循环,dx、dy顺序严格!
int result=0;
int n=0;
int a[300][300][30][8]={0};//标记某个顶点是否分裂过
int vis[300][300]={0};//标记某点是否到达过
int st[30]={0};
void dfs(int x,int y,int num,int dire){
if(num==n){return;} //分裂次数足够了
if(a[x][y][num][dire]==0){//没从这个点分裂过 记忆剪枝,若某个点分裂过,则不再从它分裂
a[x][y][num][dire]=1;
//从这个点分裂
for(int j=1;j<=st[num];j++){
if(vis[x+dx[dire]*j][y+dy[dire]*j]==0){
vis[x+dx[dire]*j][y+dy[dire]*j]=1; result++;
}
}
//继续下一次分裂
int nextX=x+dx[dire]*st[num];
int nextY=y+dy[dire]*st[num];
int nextDire=(dire+1)%8;
dfs(nextX,nextY,num+1,nextDire);//下一点右侧方向分裂
nextDire=(dire+7)%8;
dfs(nextX,nextY,num+1,nextDire);//下一点左侧方向分裂
}
}
int main(){
scanf("%d",&n);
for(int i=0;i<n;i++){
scanf("%d",&st[i]);
}
dfs(150,150,0,0);
printf("%d",result);
return 0;
}
四、经验与总结
- 记忆性剪枝真的是dfs、bfs等降低时间复杂性的利器,影响非常大。
- 整个题难度较大,不仅考察了dfs和记忆性剪枝,还考察了很多思维上的问题。比如说,我一开始想的是构造结构体,在结构体里记录各种数据(甚至包括x、y分别的正负号),申请结构体二维数组,但明显这样写的代码长度、繁琐度都远超过这个写法。可以尝试用更简练的代码实现同样的功能!
- bfs算法猜想:一层一层地遍历,每遍历一层,去重、记忆性剪枝。(总体上和dfs差不多)
- 题目很多细节小点也很值得注意,比如原点定在(150,150)、分裂方向的规律(用dxdy而非一堆if)等。