本文主要是介绍[csp模拟1]可怕的宇宙射线——(三),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
目录
- 题意
- Input
- Output
- 输入样例
- 输出样例
- 提示
- 分析
- 总结
- 代码
题意
众所周知,瑞神已经达到了CS本科生的天花板,但殊不知天外有天,人外有苟。在浩瀚的宇宙中,存在着一种叫做苟狗的生物,这种生物天生就能达到人类研究生的知识水平,并且天生擅长CSP,甚至有全国第一的水平!但最可怕的是,它可以发出宇宙射线!宇宙射线可以摧毁人的智商,进行降智打击!
宇宙射线会在无限的二维平面上传播(可以看做一个二维网格图),初始方向默认向上。宇宙射线会在发射出一段距离后分裂,向该方向的左右45°方向分裂出两条宇宙射线,同时威力不变!宇宙射线会分裂n次,每次分裂后会在分裂方向前进ai个单位长度。
现在瑞神要带着他的小弟们挑战苟狗,但是瑞神不想让自己的智商降到普通本科生zjm那么菜的水平,所以瑞神来请求你帮他计算出共有多少个位置会被"降智打击"。
Input
输入第一行包含一个正整数n(n<=30)),表示宇宙射线会分裂n次
第二行包含n个正整数a1,a2…an,第i个数ai(ai<=5)表示第i次分裂的宇宙射线会在它原方向上继续走多少个单位长度。
Output
输出一个数ans,表示有多少个位置会被降智打击
输入样例
4
4 2 2 3
输出样例
39
提示
每个测试点 1000ms 262144KB
分析
小菜鸡看到第三天的题目和图片就赶紧退出了【233】
但是仔细想想这道题也不是完全做不起的题【就是限时内交不了的233】
- 题目理解
这个题目很好理解。可以看作是在一个二维坐标图上的任意一点开始发散分裂,问最后一共经过多少个坐标。
第一次分裂只有一次且一定朝上,其后的分裂都是从上次分裂的终点开始朝两侧的45度前进。显然,这是指数级的增长。
- 题目分析
虽然我后来AC的做法和最开始的思路不一样,但是我想在这里记录和分析讨论一下我最开始的思路。
- 对称(没有做下去🔜)
在刚开始思考这道题的时候,我们都能很快发现样图中分裂的图形一定根据第一次分裂的直线对称。而指数级的增长如果暴力解算是必定会爆掉的。所以我想如果能找到一种办法只计算一半的路径,那么这个问题起码就能减少一倍吧。不过显然,图中最大的问题就在于那些会重复经过的点,而一点不论被路过几次,都只会算作一次。
那么问题关键就在于如何计算出这一半的路径,并且排除掉重复点。
- 类似bfs求路径
由于比较笨,所以最开始并没有直接想到用bfs和dfs去求路径,而是自己搞了个稀奇古怪的方法。
除去第一次分裂单独处理。只处理单侧分裂。
循环分裂次数次,将每次分裂分为第奇或偶次。将奇偶两次分裂的起点分别装到不同的队列中,在奇偶次分裂时交换调用和存储,来保证区分以及及时停止。
当队列不为空时依次调出队列中的点进行分裂。每个起点都将其接下来的两个分类方向存在了一个pair类型的vector中。因此只需要在调出一个点时,两次调用分裂函数,传入分裂方向、分裂起点、分裂步数即可。
分裂函数中根据不同的分裂方向计算其经过的路径。将经过的每个点进行判定,若其未出现过,则表明已到达,总到达点数+1。否则忽略。在计算每个单边点的同时计算它的对称点,根据特征,对称点一定是高度相同,横向距离相反(将第一次分裂处作为(0,0)的话)。计算出其对称点后,在原点之后进行判定,同样处理。
- 问题❗️
这个做法在分裂次数达到10的时候就会出现错误,分裂次数大于正确答案,也就是说存在冗余。
我之前一直在想是否会是标记是否到达出现了问题,但是只要出现过就不会二次存储,这应该也没有什么太大问题。
那么问题或许就是出现在求对称点上。但是根据图形特征,任何一个在单侧分裂中出现过的点在其对称处都一定会出现,不论是否重复。
所以,这种思路的问题仍在等待解决中🔄
- dfs(AC)
dfs就没有什么复杂的问题了。
- 第一个关键在于每次在一个方向中计算出下次分裂起点后,应该有两次不同方向的调用。
- 第二个关键在于边界的限制。由于是通过递归依次叠加读取当前点应分裂的步数,所以会有数组下标的限制,以防止当前递归的次数已经超过了题目给出数据分裂的次数。
- 第三个关键在于剪枝。这种搜索法将会走过指数级增长的所有点,因此如果不进行 剪枝,递归是一定会💥的。显然,如果一个点已经走过,那么他接下来的这条分叉路就不需要再重复遍历。这里就用到了记忆性剪枝。通过递归函数的三个参数的四个变量(方向、横坐标、纵坐标、第i次分裂)来唯一确定一个分叉口的状态。若当前分叉扣已经到达,则return,否则将该状态标记为已到达。
- 代码实现
bfs代码书写难度不大,参照模版即可。
但在这个题目中有一些地方需要小小的注意。
- 标记状态数组
既然需要一个四维数组来唯一标记每一个状态,那么这四位数组的四个容量该如何确定呢?
方向共有8种,分裂次数最多为30次。因此这两个数组容量都很好控制。
可是该如何来记录所有点的横坐标和纵坐标呢?
根据题目可知,每次分裂最多走5步,最多分裂30次,也就是说一次分裂最多走出150步。而如果我们将分裂起点的坐标设置为(0,0)的话,就一定会出现负数,而普通的数组无法支持负数下标。
因此,我们需要将坐标起点设置在一个保守的中间值,保证不会出现负数。那么我们数组的长度只要能保证能包含该中间值的下界和上界即可。
比如大家普遍选择的(200,200)这个起始值,如果走150步都是负数方向,也不会出现负坐标。而四位数组中横坐标和竖坐标的数组长度都是400,就将200上增和下减150的幅度都包含在内了。
- 是否需要单独存储方向?
这不是一定的。如果在递归函数中进行if判定数字其实就已经足够。
又因为每个点的分裂方向都可以根据其分裂得到的方向推测出来,并且不会发生改变,即指存在固定情况。因此可以在每个if最后直接将其对应接下来的两个方向作为参数调用递归函数。
但是如果要存储也是可以的,模仿迷宫的坐标偏移量或许可以(?)。
总结
- bfs和dfs模版还是很重要
- 今后努力学着骗分!
- 心态很重要,还是不太习惯csp赛制,技术和心理都没有良好适应。
- 继续加油鸭🍟
代码
//
// main.cpp
// lab3
//
//#include <iostream>
#include <vector>
#include <map>
#include <queue>
using namespace std;vector<int> times; //分裂步数
//map<int,pair<int, int>,int,bool> op; //标记已搜索节点
bool judge[8][400][400][35]={false};
map<pair<int, int>,bool> point; //标记已到达点
//map<pair<int, int>,pair<int, int>> next; //记录下一次分裂方向
//map<pair<int, int>,pair<int,int>> direction; //记录方向(-1无,0上,1下,2右,3左,4左上,5右上,6左下,7右下)
pair<int, int> start(200,200); //记录每一次分裂的起点,first为横坐标,second为竖坐标
//queue<pair<int, int>> begin1,begin2; //记录每一次分裂的所有起点(单侧),begin1为偶次分裂起点(奇次终点),begin2为奇数分裂起点(偶次终点)
int sum = 0,n = 0;void move(int d,pair<int, int> p,int s)
{if( s >= n ) //设置边界{
// cout<<" ----- "<<endl;return;}if( judge[d][p.first][p.second][s] ) //记忆性剪枝{
// cout<<" ----- "<<endl;return;}judge[d][p.first][p.second][s] = true; //标记为已访问int step = times[s];
// pair<int, int> next(-1,-1);
// int d1 = -1,d2 = -1;if( d == 0 ){while( step-- ){p.second++;if( !point[p] )//point.count(p) == 0 ) //若当前点未被到达{
// cout<<p.first<<" "<<p.second<<" ** "<<endl;point[p] = 1;sum++;}}move(4,p,s+1);move(5,p,s+1);}if( d == 1 ){while( step-- ){p.second--;if(!point[p] )//point.count(p) == 0 ) //若当前点未被到达{
// cout<<p.first<<" "<<p.second<<" ** "<<endl;point[p] = 1;sum++;}}move(6,p,s+1);move(7,p,s+1);}if( d == 2 ){while( step-- ){p.first++;if( !point[p] )//point.count(p) == 0 ) //若当前点未被到达{
// cout<<p.first<<" "<<p.second<<" ** "<<endl;point[p] = 1;sum++;}}move(5,p,s+1);move(7,p,s+1);}if( d == 3 ){while( step-- ){p.first--;if( !point[p] )//point.count(p) == 0 ) //若当前点未被到达{
// cout<<p.first<<" "<<p.second<<" ** "<<endl;point[p] = 1;sum++;}}move(4,p,s+1);move(6,p,s+1);}if( d == 4 ){while( step-- ){p.first--;p.second++;if( !point[p] )//point.count(p) == 0 ) //若当前点未被到达{
// cout<<p.first<<" "<<p.second<<" ** "<<endl;point[p] = 1;sum++;}}move(0,p,s+1);move(3,p,s+1);}if( d == 5 ){while( step-- ){p.first++;p.second++;if( !point[p] )//point.count(p) == 0 ) //若当前点未被到达{
// cout<<p.first<<" "<<p.second<<" ** "<<endl;point[p] = 1;sum++;}}move(0,p,s+1);move(2,p,s+1);}if( d == 6 ){while( step-- ){p.first--;p.second--;if( !point[p] )//point.count(p) == 0 ) //若当前点未被到达{
// cout<<p.first<<" "<<p.second<<" ** "<<endl;point[p] = 1;sum++;}}move(3,p,s+1);move(1,p,s+1);}if( d == 7 ){while( step-- ){p.first++;p.second--;if( !point[p] )//point.count(p) == 0 ) //若当前点未被到达{
// cout<<p.first<<" "<<p.second<<" ** "<<endl;point[p] = 1;sum++;}}move(2,p,s+1);move(1,p,s+1);}/*//分裂图形一定对称,即每个点都一定存在与其横坐标完全相反的对称点(除第一次分裂)
// symmetry.second = p.second;
// symmetry.first = -p.first;int x = p.first,y = p.second;if( point.count(p) == 0 ) //若当前点未被到达{cout<<p.first<<" "<<p.second<<" ** "<<endl;point[p] = 1;sum++;}if( point.count(symmetry) == 0 ) //若当前点的对称点未被到达{cout<<symmetry.first<<" "<<symmetry.second<<" ^^ "<<endl;point[symmetry] = 1;sum++;}*/}int main()
{int step = 0;cin>>n;for( int i = 1 ; i <= n ; i++ ){cin>>step;times.push_back(step);}move(0,start,0);cout<<sum<<endl;return 0;
}
这篇关于[csp模拟1]可怕的宇宙射线——(三)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!