[luogu9141] [THUPC 2023 初赛] 乱西星上的空战 -大模拟 - 计算几何

文章介绍了THUPC2023初赛中的一道模拟题,涉及无人机和导弹的飞行策略、目标选择、碰撞检测以及导弹爆炸等复杂逻辑。题目要求参赛者编写代码处理无人机和导弹在三维空间中的运动,包括选定目标、发射导弹、飞行路径规划以及碰撞判定等。作者分析了题目的难点在于计算几何和模块化设计,并提供了代码示例来解释解决这些问题的方法。

传送门:P9141 [THUPC 2023 初赛] 乱西星上的空战

大家好我又来了,这次带给大家的依然是THUPC的传统艺能大模拟~

个人点评:这题是我个人写完并通过的算法竞赛中的大模拟题中最长的一道,但可能并非最难写的。甚至在我看来,在你有一个相对完整的三维计算几何模板的情况下,码长只有这题三分之一的P7147 [THUPC2021 初赛] 麻将模拟器的实现难度都不低于这题。

究其原因,这题的模块化特别强,各个模块之间基本上相对独立,特别适合进行抽象及使用各种oop技巧,尤其是在题目已经天然地给你划分好9个阶段的前提下。这样下来的整个编码逻辑是十分清晰的,尤其不会出现像我写P7610 [THUPC2021] 群星连结时各种逻辑线搅乱在一起根本不知道下一步该开哪一条的现象。

甚至于我在赛前预测,说不定会有队伍专门来写这题,3个人并行工作每人写一个模块,这样如果调试顺利的话5个小时应该是够用的,但是为什么没有队这么干呢qwq

我们一点点来看吧:

首先,正如麻将模拟器那题的真正难点在于dp,这题的真正难点大概在于计算几何部分吧。除了常规的空间向量运算外,还有点到线段的距离、点在平面上的投影等。如果第一次写可能会绕不过弯来,但是熟练了或者有现成模板的话应该问题不大。

一个小细节就是千万不要偷懒不设eps,否则精度误差分分钟教你做人(主要来自于求角度的arccos,以及当心原始的acos函数在参数超过1时会爆nan)。

namespace geometry{
   
   
#define EPS 1e-12l
	inline ldb my_acosl(ldb x){
   
   return acosl(max(-1.0l,min(1.0l,x)));}
	inline bool chkeq(ldb x,ldb y){
   
   return fabsl(x - y) < EPS;}
	struct vec2{
   
   
		ldb x,y;
		ldb dis()const {
   
   return sqrtl(x * x + y * y);} //长度
		ldb dis2()const {
   
   return x * x + y * y;} //长度的平方
		ldb dot(const vec2 &p)const {
   
   return x * p.x + y * p.y;}
		ldb cross(const vec2 &p)const {
   
   return x * p.y - y * p.x;}
		ldb dis_to_rectangle(ldb lx,ldb hy)const {
   
   return min(fabsl(x - lx),fabsl(x + lx)) + min(fabsl(y - hy),fabsl(y + hy));}
	};
	struct vec3{
   
   
		ldb x,y,z;
		vec3 to_int(){
   
   x = round(x);y = round(y);z = round(z);return *this;}
		ldb dis()const {
   
   return sqrtl(x * x + y * y + z * z);} 
		ldb dis2()const {
   
   return x * x + y * y + z * z;} 
		vec3 get_norm()const {
   
   ldb d = dis();return {
   
   x / d,y / d,z / d};}//获取向量单位化后的结果,但向量本身不单位化
		vec3 norm(){
   
   ldb d = dis();x /= d;y /= d;z /= d;return *this;}//获取向量单位化后的结果并将向量本身单位化
		ldb dot(const vec3 &p)const {
   
   return x * p.x + y * p.y + z * p.z;}
		vec3 cross(const vec3 &p)const {
   
   return {
   
   y * p.z - z * p.y,z * p.x - x * p.z,x * p.y - y * p.x};}
		ldb get_angle(const vec3 &p)const {
   
   return my_acosl(dot(p) / dis() / p.dis());}
		void input(){
   
   x = read_ldb();y = read_ldb();z = read_ldb();}	
	};
	vec3 operator + (const vec3 &a,const vec3 &b){
   
   return {
   
   a.x + b.x,a.y + b.y,a.z + b.z};}
	vec3 operator - (const vec3 &a,const vec3 &b){
   
   return {
   
   a.x - b.x,a.y - b.y,a.z - b.z};}
	vec3 operator * (ldb k,const vec3 &a){
   
   return {
   
   k * a.x,k * a.y,k * a.z};}
	bool operator == (const vec3 &a,const vec3 &b){
   
   return chkeq(a.x,b.x) && chkeq(a.y,b.y) && chkeq(a.z,b.z);}
	struct line{
   
   
		vec3 p,v;
		vec3 projection(const vec3 &x)const {
   
   return p + v.dot(x - p) * v;}
		ldb get_min_dis(const vec3 &x)const {
   
   return (x - projection(x)).dis();}
	};
	struct segment{
   
   
		vec3 p,q;
		ldb len()const {
   
   return (p - q).dis();} //长度
		ldb len2()const {
   
   return (p - q).dis2();} //长度的平方
		ldb get_min_dis(const vec3 &x)const {
   
   //求点到线段的最近距离
			if(p == q) return (x - p).dis();
			ldb tmp = (x - p).dot(q - p);
			if(tmp <= 0) return (x - p).dis();
			if(tmp >= len2()) return (x - q).dis();
			line y = {
   
   p,(q - p).norm()};
			return y.get_min_dis(x);
		}
	};
	struct plain{
   
   
		vec3 p,n;
		vec3 projection(const vec3 &x)const {
   
   return x - n.dot(x - p) * n;}
	};
};

接下来是无人机的定义。因为要预判无人机的走位,这里采用的方法是在一个对象里分别定义了无人机当前的运动状态和预判的运动状态。比较麻烦的部分大概就是求敌机在自己雷达平面上的投影,以及判断一个位置是否能飞到。前者在已有的计算几何板子下也并不难,后者主要就是把各种是否要滚转、向哪个方向滚转,以及到底是正杆还是负杆搞清楚就好。

struct plane{
   
   
	int id;
	int status;
	bool team;
	vec3 p,d,u,l; //当前的运动状态向量 
	vec3 nxtp,nxtd,nxtu,nxtl; //这一回合即将移动到的运动状态向量 
	ldb tu,td,r,vm,lx,hy;
	int target;
	bool tar_in_radar;
	vector<int> exploded;
	void getl(){
   
   l = u.cross(d);}
	void input(int _id,bool _team){
   
   
		id = _id;
		team = _team;
		p.input();d.input();u.input();getl();
		tu = read_ldb();td = read_ldb();r = read_ldb();
		vm = read_ldb();lx = read_ldb();hy = read_ldb();
		status = ALIVE;
		target = 0;
		exploded.clear();
	}
	bool in_horizon(const plane &x)const {
   
   return d.dot(x.p - p) > EPS;}
	bool in_nxt_horizon(const plane &x)const {
   
   return nxtd.dot(x.p - nxtp) > EPS;}
	vec2 get_radar_r(const plane &x)const {
   
   //求x在自己的雷达平面上的投影
		plain pl = {
   
   p,d};
		vec3 nd = pl.projection(x.p);
		return {
   
   l.dot(nd - p),u.dot(nd - p)};
	}
	vec2 get_nxt_radar_r(const plane &x)const {
   
   //求移动后x在自己的雷达平面上的投影
		plain pl = {
   
   nxtp,nxtd};
		vec3 nd = pl.projection(x.p);
		return {
   
   nxtl.dot(nd - p),nxtu.dot(nd - p)};
	}
	bool in_radar_r(const vec2 &r)const {
   
   return fabsl(r.x) <= lx + EPS && fabsl(r.y) <= hy + EPS;}
	bool can_reach(const vec3 &x){
   
   //飞机能否飞行x距离(飞到p+x位置),能的话更新nxtp,nxtu,nxtd
		nxtp = p + x;
		nxtd = x.get_norm();
		if(d == nxtd) nxtl = l;//不需要滚转
		else if(d == -1 * nxtd) return 0;
		else{
   
   //需要滚转
			nxtl = d.cross(nxtd).norm();//要通过滚转把l转到与d和nxtd所在的平面垂直的方向
			if(l.dot(nxtl) < 0) nxtl = -1 * nxtl;//滚转只能在90度以内
		}
		nxtu = nxtd.cross(nxtl);
		return l.get_angle(nxtl) / r //滚转
			+ d.get_angle(nxtd) / (u.dot(nxtd) >= 0 ? tu : td) //俯仰
			+ x.dis() / vm <= 1 + EPS; //直线飞行
	}
}a[210]; 

导弹的定义与无人机类似,鉴于同一时刻存在的来自于同一架无人机的导弹最多只会存在一枚,我这里的写法是一个导弹对象就是固定由某架无人机发射的,那么需要注意多次初始化的问题;另外导弹会比无人机多一个功能,即预判是否锁定目标及相应的锁定角,不过这不难写。

struct missile{
   
   
	int id;
	int status;
	bool team;
	vec3 p,d; //当前的运动状态向量 
	vec3 nxtp,nxtd; //这一回合即将移动到的运动状态向量 
	ldb tr,vm,ds,dp,bs;
	int tz,timer,target;
	void input(int _id,bool _team){
   
   
		id = _id;
		team = _team;
		tr = read_ldb();vm = read_ldb();ds = read_ldb();
		dp = read_ldb();bs = read_ldb();tz = read();
		status = DEAD;
		timer = target = 0;
	}
	void init(const vec3 &_p,const vec3 &_d,int _target){
   
   
		p = _p;d = _d;
		timer = 0;
		target = _target;
		status = INACTIVE;
	}
	bool nxt_can_lock(const plane &x,ldb &angle){
   
   //导弹即将飞到的位置能否锁定目标即将飞到的位置,能的话锁定角是多少(传给angle)
		if(x.nxtp == nxtp){
   
   //两者目标位置相同
			angle = 0;
			return 1;
		}
		ldb dott = nxtd.dot(x.nxtp - nxtp); 
		angle = nxtd.get_angle(x.nxtp - nxtp);//锁定角
		return dott > 0 && angle <= bs + EPS;//在前方且锁定角不超过最大锁定角
	}
	bool can_reach(const vec3 &x){
   
   //导弹能否飞行x距离(飞到p+x位置),能的话更新nxtp,nxtd
		nxtp = p + x;
		nxtd = x.get_norm();
		return d.get_angle(nxtd) / tr //偏航
			+ x.dis() / vm <= 1 + EPS; //直线飞行
	}
}b[210];

接下来,抽象出几个比较麻烦的步骤:

无人机目标选择:这一部分需要注意优先级为“先前选定的目标——在雷达范围内的目标(按到自己的距离排序)——在视野范围内但不在雷达范围内的目标(按到雷达范围的曼哈顿距离排序)”,然后分类讨论即可。为了后续实现方便,可以在无人机对象里记录这一步求出的“目标敌机是否在雷达范围内”信息。

void find_target(plane &x){
   
   //无人机选定目标
	if(x.status != ALIVE) return;
	int last_target = x.target;x.target = 0;x.tar_in_radar = 0;
	ldb min_dis = INF,min_dis_r = INF;
	for(int i = 1;i <= m;++i){
   
   
		plane &y = a[i];
		if(y.status != ALIVE || y.team == x.team || !x.in_horizon(y)) continue;
		vec2 r = x.get_radar_r(y);
		if(last_target == i){
   
   //先前的锁定目标,现在还能锁定 
			x.target = i;
			x.tar_in_radar = x.in_radar_r(r);
			<
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值