题目链接:P5506 封锁
题解
根据要求模拟飞机的运动即可。对于飞机“正前方”的理解有点难度,需要一定的时间理顺。然后,就可以愉快地写代码了。定义一个结构体 struct Plane
存储飞机的各种属性及其操作,除了题目中涉及的属性,我在结构体中额外增加了 xdir
、ydir
、zdir
三个属性,描述飞机前进的方向向量在
x
x
x,
y
y
y,
z
z
z 三个坐标轴上的分量。
对于给定的飞机,如何判断另一架飞机是否在它的正前方是一个棘手的问题,也是我在代码实现过程中,碰到的最大的困难。具体地,可以这样处理:
记给定飞机所在位置为
P
1
P_1
P1,该飞机的“正前方”对应的方向向量为
d
→
\overrightarrow{d}
d (由 xdir
、ydir
、zdir
确定),另一架飞机用点
P
2
P_2
P2 表示,考察
d
→
\overrightarrow{d}
d 与
P
1
P
2
→
\overrightarrow{P_1P_2}
P1P2的点积和叉积即可。
飞机
P
2
P_2
P2 在飞机
P
1
P_1
P1 的正前方,当且仅当:
d
→
×
P
1
P
2
→
=
0
→
∧
d
→
⋅
P
1
P
2
→
>
0.
\overrightarrow{d}\times \overrightarrow{P_1P_2}=\overrightarrow{0}\ \wedge\ \overrightarrow{d}\cdot \overrightarrow{P_1P_2}\gt0.
d×P1P2=0 ∧ d⋅P1P2>0.
叉积为
0
→
\overrightarrow{0}
0 保证了两个向量共线,在此基础上,点积为
0
0
0 又使得两个向量方向相同,它们的点积与飞机间的距离
∣
P
1
P
2
→
∣
|\overrightarrow{P_1P_2}|
∣P1P2∣ 成正比,因此当
P
2
P_2
P2 在正前方时,可以直接通过向量点积反映距离的远近,无需特别计算。上述功能通过结构体中 judge
成员函数实现。
实现的代码如下:
#include <cstdio>
#include <cstring>
#include <algorithm>
const int dx[8] = {1, 1, 0, -1, -1, -1, 0, 1};
const int dy[8] = {0, 1, 1, 1, 0, -1, -1, -1};
const int dh[5] = {-1, -1, 0, 1, 1};
struct Plane {
int x, y, z, h, f;
int atk, def, mat, mdf, hp, fix;
int xdir, ydir, zdir;
char op[102];
// 根据h, f,更新当前的坐标(前进一步)
void updateCoord() {
if (h != 0 && h != 4) {
xdir = dx[f];
ydir = dy[f];
} else {
xdir = 0;
ydir = 0;
}
zdir = dh[h];
x += xdir;
y += ydir;
z += zdir;
}
// 判断另一个飞机是否在该飞机前面
// 在前面, 则返回向量的点积(与距离成正比), 否则返回-1;
int judge(const Plane& t) {
int xdi = t.x - x, ydi = t.y - y, zdi = t.z - z;
if (xdi == 0 && ydi == 0 && zdi == 0) return 0;
int cs1, cs2, cs3, dot;
cs1 = xdi * ydir - ydi * xdir;
cs2 = ydi * zdir - zdi * ydir;
cs3 = zdi * xdir - xdi * zdir;
dot = xdi * xdir + ydi * ydir + zdi * zdir;
if (cs1 != 0 || cs2 != 0 || cs3 != 0 || dot <= 0) return -1;
return dot;
}
} p[103];
int n, t;
void init() {
scanf("%d%d", &n, &t);
for (int i = 0; i < n; ++i) {
scanf("%d%d%d%d%d%d%d%d%d%d%d%s", &p[i].x, &p[i].y, &p[i].z, &p[i].h, &p[i].f, &p[i].atk, &p[i].def, &p[i].mat, &p[i].mdf, &p[i].hp, &p[i].fix, p[i].op);
}
}
// 模拟时刻为 time 时,飞机的行为
void simulateOneRound(int time) {
for (int i = 0; i < n; ++i)
if (p[i].hp > 0) p[i].updateCoord();
for (int i = 0; i < n; ++i) {
if (p[i].hp <= 0) continue;
switch (p[i].op[time]) {
case 'N': break;
case 'U': if (p[i].h < 4) ++p[i].h; break;
case 'D': if (p[i].h > 0) --p[i].h; break;
case 'L': if (++p[i].f > 7) p[i].f = 0; break;
case 'R': if (--p[i].f < 0) p[i].f = 7; break;
case 'F': p[i].hp += p[i].fix; break;
case 'A': {
int loc = -1, mndis;
for (int j = 0; j < n; ++j) {
if (p[j].hp <= 0 || j == i) continue;
int dis = p[i].judge(p[j]);
if (dis >= 0) {
if (loc == -1 || dis < mndis)
loc = j, mndis = dis;
}
}
if (loc != -1 && mndis > 0) p[loc].hp -= std::max(0, p[i].atk - p[loc].def);
break;
}
case 'M':
for (int j = 0; j < n; ++j) {
if (p[j].hp <= 0) continue;
if (p[i].judge(p[j]) > 0) {
p[j].hp -= std::max(0, p[i].mat - p[j].mdf);
}
}
break;
}
}
}
int main() {
init();
for (int i = 0; i < t; ++i)
simulateOneRound(i);
for (int i = 0; i < n; ++i)
printf("%d %d %d %d\n", p[i].x, p[i].y, p[i].z, std::max(0, p[i].hp));
return 0;
}
总结
这道模拟题还是很有意思的,在题意理解,飞机位置判断以及细节处理上都对我们提出了一定的要求。我写完这题的代码,经过一番调试,发现过不了样例
2
2
2,对此,我百思不得其解。于是,在讨论区里请教了一下提供该样例的大佬,十几分钟后就得到了回复,第一次在洛谷社区参与到了和题目有关的交流,得到了耐心的解答,还是挺开心的,附上截图,顺便帮看到的人避避坑。
题外话
其实最近还写了好几道其他模拟题,但是因为时间原因,加上自己实现的代码不如别人的简洁等因素,不想专门写题解了。这里简单提一提感受。
P4872 OIer们的东方梦:
说实话,读完题目感觉要素挺多,还是有点难 (恶心) 的。
考虑什么时候需要往回走是该题的难点所在。经过分析,玩家的状态可以分为三种:初始状态、仅获得太阳花后的状态、获得楼观剑后的状态。这三个状态人物的能力是由弱到强的,所以只有当人物状态发生转变时,才有可能需要往回走。我们把网格图分为三层,用优先队列进行BFS即可。
P5006 [yLOI2018] 大美江湖:
模拟水题。做过这么多模拟题里最简单的一个。直接分情况处理,就不解释了。
P5756 [NOI2000] 程序分析器:
模拟语句的执行本身不难,让我困惑的是如何判断程序能否正常结束。想了不少方法,后面都被自己否定掉了,我以为这是需要用到深奥图论知识来处理的难题。
看了一眼题解,居然还是暴力模拟,确立一个上限,语句的执行总次数超过上限,就判定为不能正常结束。好吧,果然是一道货真价实的模拟题呢。
后来想了想,我们使用的编译器尚且无法找出程序中的死循环,对一个程序能否正常结束给出答案,这个问题应该也没有有效的算法来解决吧。这背后的渊源或许牵涉到图灵停机问题吧······也就是说,没有通用的算法判断程序能否正常结束。