问题描述
\hspace{17pt}
众所周知,瑞神已经达到了CS本科生的天花板,但殊不知天外有天,人外有苟。在浩瀚的宇宙中,存在着一种叫做苟狗的生物,这种生物天生就能达到人类研究生的知识水平,并且天生擅长CSP,甚至有全国第一的水平!但最可怕的是,它可以发出宇宙射线!宇宙射线可以摧毁人的智商,进行降智打击!
\hspace{17pt}
宇宙射线会在无限的二维平面上传播(可以看做一个二维网格图),初始方向默认向上。宇宙射线会在发射出一段距离后分裂,向该方向的左右45°方向分裂出两条宇宙射线,同时威力不变!宇宙射线会分裂n次,每次分裂后会在分裂方向前进
a
i
a_i
ai个单位长度。
\hspace{17pt}
现在瑞神要带着他的小弟们挑战苟狗,但是瑞神不想让自己的智商降到普通本科生zjm那么菜的水平,所以瑞神来请求你帮他计算出共有多少个位置会被"降智打击"
Input
输入第一行包含一个正整数
n
,
(
n
<
=
30
)
n,(n<=30)
n,(n<=30),表示宇宙射线会分裂n次。
第二行包含n个正整数
a
1
,
a
2
,
⋯
,
a
n
a_1,a_2,\cdots,a_n
a1,a2,⋯,an,第
i
i
i个数
a
i
(
a
i
≤
5
)
a_i(a_i\le 5)
ai(ai≤5)表示第
i
i
i次分裂的宇宙射线会在它原方向上继续走多少个单位长度。
Output
输出一个数ans,表示有多少个位置会被降智打击。
Sample input
4
4 2 2 3
Sample output
39
数据范围
数据点 | n |
---|---|
10% | <=10 |
40% | <=20 |
100% | <=30 |
时间与内存限制 1000ms 2632144KB
样例解释
下图描绘了样例中宇宙射线分裂的全过程,仅做参考
解题思路
\hspace{17pt}
这个题很明显是一个搜索类问题,使用dfs还是bfs都可以,但是要注意的是剪枝的重要性。这个题n的数据最大达到了30,如果每次分裂的点都进行搜索的话,时间复杂度是
O
(
2
n
)
O(2^n)
O(2n),绝对顶不住,只能拿到40分。那我们想要100分就需要剪枝操作。
\hspace{17pt}
剪枝的方法有很多种,关键是如何找到正确的方法,我最开始的想法是,如果当前点被搜索过,并且搜索这个点的时候,方向和向后需要搜索的距离一致,就剪掉。这样一个node节点存储的就是横坐标x,纵坐标y,方向dir,距离dis。没错,这就是我最开始的想法,所以我的结果如下:
\hspace{17pt}
静下心来仔细思考一下,就知道错在哪里了,挺明显的,有可能以后搜索到这个位置,搜索的距离和方向一致,但是下一步搜索的方向和距离和最开始标记的有可能不同了,这样就剪枝过度了,造成部分位置的丢失。
\hspace{17pt}
后来的思路是,仅仅剪掉这一层搜索的时候,重复的位置(也要保证方向相同才剪掉),因为最终搜索的范围是300*300这样的区间,但是当搜索深度很大的时候,分裂的节点必定有很多重复,这样做到了剪枝操作,并且没有重复节点的丢失。node结构体中存储的dis就换成了当前搜索的层数fl(floor的简写)。
\hspace{17pt}
我的代码使用的是bfs搜索,利用两个map来存储当前节点是否搜索过和当前节点在当层此方向是否搜索过,利用队列来保存当前要搜索的直线路径的起点。和传统bfs不同的是,这种题目需要两个层次之间分离,可以做一个标记表明队列中弹出的节点是哪一层,也可以直接用两个队列来回倒。我使用的是后者,当前层次的直线起始点放在qdir中,下一层的放在qdir2中,当qdir为空,则将qdir2中的数据放在qdir中(其实这个样子还增加了一定的时间,推荐使用标记标出层次)。更加详细的解析见代码注释。
完整代码
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <cstring>
#include <string>
#include <climits>
#include <algorithm>
#include <queue>
#include <vector>
#include <map>
using namespace std;
struct node
{
int x,y;//当前节点
int dir;//当前节点搜索的方向,从1-8分别为上,右上,右,右下,下,左下,左,左上
int fl;//当前所属于的层数
node(int _x,int _y,int _dir,int _fl):x(_x),y(_y),dir(_dir),fl(_fl){}
bool operator<(const node &a) const
{
if(x<a.x) return true;
else if(x==a.x){
if(y<a.y) return true;
else if(y==a.y){
if(dir<a.dir) return true;
else if(dir==a.dir){
if(fl<a.fl) return true;
}
}
}
return false;
}
};
int n,a[31],ans;
int dx[]={0,1,1,1,0,-1,-1,-1};
int dy[]={1,1,0,-1,-1,-1,0,1};
map<node,bool> mp;//判断当前节点是否搜索过(包含方向和距离),判断该节点是否应该再次搜索
map<long long ,bool> mp2;//单纯记录当前节点是否搜索过,使用long long是直接用x*1000+y来哈希一下
queue<node> qdir;//存储下一层的所有起始节点
queue<node> qdir2;
void bfs()
{
node start(150,149,0,1);
//假设初始节点位置为(150,149),向着上方前进,层数为1(注意,(150,149)这个点是不要的,是从这里开始的,这样整个射线的起点就是(150,150)了)
//解释:本体数据量不大,从初始节点开始,向上下左右最多走150格,默认坐标是平面直角坐标,左下角为(0,0)
qdir.push(start);
for (int i=2; i<=n+1; i++)//a[1]生成的节点要走的距离是a[2],所以从i=2开始
{
while(!qdir.empty())
{
node lattice=qdir.front(); qdir.pop();
for (int j=1; j<=a[lattice.fl]; j++)//有了起始节点后,向后搜索a[fl]个节点
{
if(!mp2[(lattice.x+j*dx[lattice.dir])*1000+(lattice.y+j*dy[lattice.dir])]){//当前节点没有搜索过
mp2[(lattice.x+j*dx[lattice.dir])*1000+(lattice.y+j*dy[lattice.dir])]=true; ans++;
}
}
if(i!=n+1)//最后一次不用分裂了,直接跳出就行了
{
//开始分裂成两个节点
node newnode1(lattice.x+a[i-1]*dx[lattice.dir],lattice.y+a[i-1]*dy[lattice.dir],(lattice.dir+1)%8,i);
node newnode2(lattice.x+a[i-1]*dx[lattice.dir],lattice.y+a[i-1]*dy[lattice.dir],(lattice.dir+7)%8,i);
if(!mp[newnode1]){
mp[newnode1]=true; qdir2.push(newnode1);
}
if(!mp[newnode2]){
mp[newnode2]=true; qdir2.push(newnode2);
}
}
}
while(!qdir2.empty()){
qdir.push(qdir2.front());qdir2.pop();
}
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n;
for (int i=1; i<=n; i++)
cin>>a[i];
bfs();
cout<<ans<<endl;
return 0;
}
学长题解
关于此题的优秀博客链接
传送门 这个大佬使用的剪枝方法是对称剪枝,利用的dfs只向右搜索,我提交后发现时间比我的要快3倍,空间少6倍。这个应该才是正解。
传送门 这个文章里面提到的和我的方法类似,不过他是使用的dfs,可以参考。