程序设计Week4补题——C-可怕的宇宙射线

题目描述

宇宙射线会在无限的二维平面上从一个点开始传播(可以看做一个二维网格图),初始方向默认向上。宇宙射线会在发射出一段距离后分裂,向该方向的 左右45°方向分裂出两条宇宙射线,同时威力不变!宇宙射线会分裂n次,每次分裂后会在分裂方向前进ai个单位长度。计算宇宙射线经过的位置个数。

Input

输入第一行包含一个正整数n,表示宇宙射线会分裂n(<=30)次;
第二行包含n个正整数,第i个数ai(<=5)表示第i次分裂的宇宙射线会在它原方向上继续走多少个单位长度。

Output

输出一个数ans,表示有多少个位置。

解题思路

使用搜索的方法来查找共多少给个位置,但n<=30,意味着一共有2^30条线,直接暴力搜索的话必定会超时爆内存,因此我们考虑如何优化。
简单绘图之后可以发现,线段在发散的过程中是严格对称的,我们考虑利用对称性来求解这道题目。在这个二维平面中,我们可以找到四个主要的对称轴方向(y轴,x轴,45度直线,135度直线),但实际上只需要三个方向就可以构建出整个图形,具体思想如下:

  • 从线段第一次分裂开始,整个图形完全是左右对称的,因此我们只需考虑一半的情况,再利用y轴对称得到所有点;
  • 我们不妨选择右半部分的图形,再分析,每次分裂我们只考虑向右或向右上方向的那条线,那么所有右半部分的图形均可以最后一次分裂的结果为基本图形,反复利用x轴对称,右上45度线对称,最终得到;

基于此,考虑使用dfs递归的方法来实现这个算法:

  • 每次分裂只考虑两个方向:向右或向右上,一直递归到最后一次分裂的结果,然后将本次分裂后应该到达的位置存入一个集合中;
  • 根据本次分裂的方向来选择对称轴,若分裂方向为右上,则对称轴为x轴,若分裂方向为向右,则对称轴为右上45度线;
  • 根据选定的对称轴,将集合中的点关于对称轴的对称点存入集合中,并删去重复点;
  • 返回上一层分裂,并将该层分裂应该经过的位置点不重复地存入集合中;
  • 如此递归返回到第一次分裂,再进行一次y轴对称(预设的起点在原点,因此第一个分裂点也在y轴上),并将第一次分裂前应该经过的位置点存入集合,这样就得到了分裂过程经过的所有点,统计集合中点的数目并输出即可。

PS:关于对称点的求法。

  • 关于与x轴平行的直线对称:已知某分裂点的坐标(a,b),分裂后线段的对称轴为y=b,任一右上方线段的点(x,y)其对称点为(x,2b-y);
  • 关于y轴对称:(x,y)的对称点为(x,-y);
  • 关于与y=x平行的直线对称:已知某分裂点坐标(a,b),分裂后线段的对称轴为y=x+b-a,任意右方向线段的点(x,y),其对称点为( a+y-b,b+x-a)。

实现代码

#include<iostream>
#include<set>
using namespace std;

int a[32];		//最多30次分裂,a[i]表示第i次分裂走的长度 
int n;			//分裂次数 

//利用dfs+对称,只需考虑两个方向(向右与右上)即可 
int dx[3]={0,1,1};
int dy[3]={1,1,0};

struct position
{//每个位置的信息,重载是为了使用set 
	int x,y;
	bool operator<(const position &p) const
	{
		if(x!=p.x)
			return x<p.x;
		else
			return y<p.y;
	}
};
set<position> s;	//set本身就是去重的,免去vis数组来判定是否走过 

void sym_change(position p,int f)
{//对称变换函数,p是分裂点,f是前进方向 
	position pp;	//对称点 
	if(f==0)
	{//关于y轴对称(仅有这一次) 
		for(set<position>::iterator it=s.begin();it!=s.end();it++)
		{
			pp.x=-it->x;
			pp.y=it->y;
			s.insert(pp);
		}
	}
	else if(f==1)
	{//关于直线y=p.y对称 
		for(set<position>::iterator it=s.begin();it!=s.end();it++)
		{
			pp.x=it->x;
			pp.y=2*p.y-it->y;
			s.insert(pp);
		}
	}
	else
	{//关于直线y=x+b对称,b=p.y-p.x 
		for(set<position>::iterator it=s.begin();it!=s.end();it++)
		{
			pp.x=p.x+it->y-p.y;
			pp.y=p.y+it->x-p.x;
			s.insert(pp);
		}	
	}
}

void dfs(position p,int f,int i)
{
	//已经到最底层 
	if(i>=n)
		return;
	//now是下一个分裂点 
	position now=p;
	now.x+=dx[f]*a[i];
	now.y+=dy[f]*a[i];
	if(f==1)
		dfs(now,2,i+1);
	else
		dfs(now,1,i+1);
	for(int j=0;j<a[i];++j)
	{//将该条分裂路径上的点加入set 
		s.insert(now);
		now.x-=dx[f];
		now.y-=dy[f];
	}
	//i=1,最开始分裂,关于y轴对称 
	if(i==1)
		sym_change(now,0);
	//其余情况根据前进方向f判定即可 
	else
		sym_change(now,f);
}

int main()
{
	cin>>n;
	for(int i=0;i<n;++i)
		cin>>a[i];
	//以坐标系(0,0)为起点,start是第一个分裂点 
	position start;
	start.x=0;
	start.y=a[0];
	dfs(start,1,1);
	for(int i=0;i<a[0];++i)
	{//把起点到第一个分裂点的位置加入set 
		start.y--;
		s.insert(start);
	}
	cout<<s.size()<<endl;
	return 0;
} 

总结

这道题一眼看过去显然是一道搜索的题目,但问题就在于它的搜索范围太大,因此暴力的算法是不可取的,如何优化搜索是这道题的重点,我是从数学的角度,尤其是图形的角度去考虑,利用线段在不断分裂过程中的对称性和重复性,来剪除大部分的搜索过程,仅保留一条搜索线路,然后从最后一次不断对称、加点来求解,由于这样的方式仅需要考虑三种方向:向上、向右、向右上,因此移动数组可以变为 dx[3]={0,1,1}; dy[3]={1,1,0}; 同时为了点的去重,可以使用set来免去人工去重的麻烦,除此之外,还需要找到正确的对称方程,保证准确找到每个点的对称点。
而这道题还可以使用记忆化搜索的方法来求解,根据题目数据的限制,仅需要一个vis[150][150][30][8]的四位数组,就能判定所有状态,再根据每层搜索的结果标记vis数组,去重,就能很好的剪除那些重复的情况。这种解法更直接,更清晰,不需要太多的数学思考。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值