末影之眼 的题解
在解题之前提交题解的代码会导致封禁。
作者:
概述
本题是一道交互题:世界中有 座要塞,每次抛掷末影之眼可获知「距抛掷点最近的要塞」的水平方位角,且该眼有
的概率碎裂,最终只允许作答一次。核心解法是先用两次抛掷确定目标要塞——第二次沿第一条射线前进以拉长基线,再对两条射线求交——使落点稳定落在容错半径
格以内。
分析
核心观察
- 通过率的上限由眼数预算决定,与几何精度无关:抛掷
次时单点通过率的上限为
。
- 单次抛掷只提供方向,不提供距离,因此至少需要两条射线才能定位。
- 每个要塞的 Voronoi 胞是凸集,原点与目标要塞同属一个胞,因此沿射线前进绝不会切到别的要塞——这是本题的钥匙。
思路
第一步:算清眼数预算。 设共抛掷 次,碎裂数
,剩余眼数为
。通过条件为
于是单点通过率为
| 抛掷次数 |
|||||
|---|---|---|---|---|---|
| 眼数关通过率 |
抛掷次数越多,通过率越低: 的概率为
,这类世界中任意一只眼碎裂即失败。而抛掷
次或
次都无法定位(一条射线只有方向),因此最优方案是恰好抛掷
次,单点通过率上限
。
期望得分。 本题共 个测试点、每点
分,故期望得分为
分,得分标准差约
分;一次提交通过全部
个测试点(即该题 AC)的概率为
。该上限由题目机制决定,与解法无关。
第二步:世界结构。 要塞由环算法生成,第 环的距离带为
即每环一条宽 格的距离带,带中心相隔
格。环内方位角按固定步长铺开:第
环
座相隔
,第
环
座相隔
,依此类推;最后一环为剩余的
座(
,留一个
空隙),本解法只依赖第
环,该空隙不影响任何结论。
| 环 | 座数 | 距离(格) |
|---|---|---|
由此可得一个直接结论:在原点抛掷时,最近的要塞必定属于第 环——第
环最远
格,第
环最近也有
格。
下图同时展示了要塞的环状分布与它们对应的 Voronoi 胞。每个胞内的点都会把同一座要塞视为最近要塞。

将要塞视为平面点集,它们划分出 Voronoi 图,每个要塞 对应区域
抛掷返回的方位角,本质是「当前点落在哪个 cell 内」。因此整道题等价于用两条射线找回 cell 的主人。
下文各层级的「落点率」只指几何命中率;单点通过率还要再乘眼数关通过率(两次抛掷时为 )。
L0:一次抛掷,在环带中碰运气。 抛掷一次得到角度 ,沿该射线在环带中心
处作答。角度是精确的,但沿射线的距离完全未知,仅落在
这条宽
格的带内,落点误差为
,命中概率约为
是三个均匀半径的最小值,密度偏向近端,因此取中心并非最优:取中点的实测落点率约
,沿带内均匀随机约
,而单次抛掷的最优策略(押在带近端
附近)上限可达
——但仍远低于两层策略。该层的结论是:一条射线不足以定位,至少需要两条。
一次抛掷得到的只是方向,而不是距离;下图中的环带区间正是这种不确定性的几何表示。

L1:两次抛掷,垂直基线 格。 从原点
抛掷得到指向要塞
的射线,再从另一点
抛掷得到第二条射线,两线交会即为
。注意
不能落在第一条射线上,否则两射线共线、方程组退化,因此需要垂直偏移。设要塞距第一点
、基线长
、方位角量化误差
rad,则交会误差为
其来源是:两条射线在要塞处的夹角 满足
,第一条射线转过
后在要塞距离处横向偏移
,除以
即得
即基线越短、目标越远,交会越不稳定。代入 与
:当
时
格,是容错半径
的
倍,实测落点率
。该层说明交会思路正确,且指出下一层的方向:拉长基线。
L2:基线拉长到 格。 从原点垂直偏移
格再抛掷一次:
精度进入容错半径量级,实测落点率 。仍非满分,有两个独立原因(
个随机世界,
垂直偏移):
| 现象 | 占比 |
|---|---|
| 第二次抛掷点仍在 |
|
| ↳ 其中落点不超过 |
|
| 第二次抛掷点已离开 |
|
| ↳ 其中落点不超过 |
其一,误差本身处于容错边缘: 随
增长,
时
格,超过
。其二更致命:cell 在原点附近非常薄。第
环相邻要塞相距约
格,但三座要塞半径各自独立抖动,其中垂面几乎都从原点附近穿过——实测原点到目标 cell 边界的垂直距离中位数仅约
格(
,
)。因此垂直偏移
格时,超过四分之一的世界中该点已不在原 cell 内,第二次读数指向别的要塞。继续拉长垂直基线同样不可行:垂直偏移
格的实测结果中,仍在同一 cell 内的比例为
。
L3:沿射线前进。 既然垂直方向很薄,就换方向——沿第一条射线朝要塞前进,基线照样拉长。
定理. 设要塞
是点
处的最近要塞,则线段
上任意点
的最近要塞仍是
。
证明. 每个 cell 是一族半平面之交:
其中每个 是半平面(两边平方后二次项抵消,剩余线性不等式)。半平面是凸集,凸集之交仍为凸集,故
是凸的。由题设
,且
(自身距离为
),凸集包含其中任意两点之间的整条线段,故
。线段上任一点抛掷返回的方位角都指向
。
这是定理而非概率:沿射线前进永远不会切到别的要塞,L2 中「离开 cell」的失败模式被彻底消除。
剩余的取参细节:若 严格落在射线上,两射线共线、交会退化,因此仍需保留一个较小的垂直偏移
;基线
越接近要塞距离
,交会越稳定,误差式为
其推导与 L2 一致(横向偏移 除以两射线夹角的正弦,此处夹角由「
对着
」给出)。
时化为
,即
应贴近
、
应尽量大。但
受 cell 宽度约束:
必须仍在
的 cell 内。实测沿射线
处的横向半宽最小约
格,取数百格是安全的。
四个层级的对照:
| 层级 | 做法 | 落点率 | 失败主因 |
|---|---|---|---|
| L0 | 只有方向没有距离 | ||
| L1 | 基线过短, |
||
| L2 | 垂直基线 |
||
| L3 | 沿射线前进,配小垂直偏移 | —— |
独立复测(真机对拍)为 L1 、L2
、L3
,与上表相差
个百分点,属小样本涨落。
本题的关键:L2 已发现「基线越长越准」,但沿垂直方向拉长会撞上 cell 边界;凸性定理指出的方向是沿射线拉长。同一个「拉长基线」的念头,方向不同,结果从 变为
。
具体示例
以题面样例的世界为例:第 环三座要塞位于
、
、
。
| 输出 | 交互器返回 | 说明 |
|---|---|---|
? 0 0 |
-159.5 12 |
最近要塞为 |
? 0 256 |
-151.8 11 |
最近要塞不变;碎裂一只,剩余 |
! -1577 -590 |
两条射线交会于 |
作答点到要塞 的距离约
格,不超过
格;若此时
,则
满足,该测试点通过。
算法步骤
- 输出
? 0 0并从标准输入读取与剩余眼数,换算为弧度得到第一条射线方向
ux、uz。 - 取第
环的环带(
),令
lo、hi为带的两端;基线b取环带中点,垂直偏移
side取。
- 由
ux、uz沿射线前进b并垂直偏移side,得到第二个抛掷点p2x、p2z,输出? p2x p2z并读取。
- 将两条射线方向记为
d1、d2,计算叉积分母den。 - 若
den的绝对值大于,由交会公式得到沿第一条射线的距离
t,并检查t是否落在环带外扩格的范围内;否则将
t置为b。 - 输出
! round(t \cdot d1x) round(t \cdot d1z)并结束。
复杂度分析
- 时间:
- 空间:
实现注意事项
- 每次输出后必须刷新缓冲区(C++ 用
fflush(stdout),Python 用sys.stdout.flush()),否则交互器与程序可能互相等待导致超时。 - 方位角量化为一位小数,即
rad,这是全部误差的来源;坐标量级为
,
double精度不构成瓶颈。 - 第二个抛掷点与最终答案均需取整为整数坐标,取整误差不超过
格,且只影响几何而不再进入读数。
- 交会公式在两条射线接近平行时分母趋零,代码需检查分母;留出
后两射线夹角至少数十度,正常不会退化。若解出的距离明显落在环带之外,说明第二次读数指向了别的要塞,此时退化为「沿第一条射线前进到环带中点」的保底答案。
- 读数中的剩余眼数不参与决策:作答只有一次机会,读到第二个角度时已无选择余地。
- 基线取环带中点而非固定值,使
尽可能小;若抛掷点半径改变,只需先由半径定出所属环、再取该环带中点。
- 参数余量:参考实现取
、
。经典参数
、
的实测落点率同样是
,但穷举最坏构型(
且两次量化误差反向)可达
格,已超出容错半径
格——该构型约
稀有,实测未能触发;把基线取到环带中点、偏移取大一些即可把这个尾巴压掉。
| 穷举最坏落点误差 | cell 横向余量 | ||
|---|---|---|---|
- 本文源代码在真实交互器上各跑
局:C++ 版通过率约
、Python 版约
,失败全部来自眼数关,落点失败
例;与理论上限
的偏差属抽样涨落。
源代码
#include <cmath>
#include <cstdio>
// 第 0 环环带:2048 ± 640
static const double BAND_LO = 1408.0;
static const double BAND_HI = 2688.0;
int main() {
double th1;
int eyes;
printf("? 0 0\n");
fflush(stdout);
if (scanf("%lf %d", &th1, &eyes) != 2) return 0;
double a1 = th1 * M_PI / 180.0;
double ux = cos(a1), uz = sin(a1);
double b = 0.5 * (BAND_LO + BAND_HI); // 2048:环带中点
const double SIDE = 384.0; // 垂直偏移
long long p2x = llround(b * ux - SIDE * uz);
long long p2z = llround(b * uz + SIDE * ux);
printf("? %lld %lld\n", p2x, p2z);
fflush(stdout);
double th2;
if (scanf("%lf %d", &th2, &eyes) != 2) return 0;
double a2 = th2 * M_PI / 180.0;
double d1x = cos(a1), d1z = sin(a1), d2x = cos(a2), d2z = sin(a2);
double den = d1x * d2z - d1z * d2x;
double t = b; // 保底:前进到环带中点
if (fabs(den) > 1e-12) {
double cand = (p2x * d2z - p2z * d2x) / den;
if (cand > BAND_LO - 256.0 && cand < BAND_HI + 256.0) t = cand;
}
printf("! %lld %lld\n", llround(t * d1x), llround(t * d1z));
fflush(stdout);
return 0;
}
import math
import sys
BAND_LO, BAND_HI = 1408.0, 2688.0
def main():
def ask(x, z):
sys.stdout.write('? %d %d\n' % (x, z))
sys.stdout.flush()
return sys.stdin.readline().split()
first = ask(0, 0)
if len(first) < 2:
return
a1 = math.radians(float(first[0]))
ux, uz = math.cos(a1), math.sin(a1)
b = 0.5 * (BAND_LO + BAND_HI) # 2048:环带中点
side = 384.0 # 垂直偏移
second = ask(round(b * ux - side * uz), round(b * uz + side * ux))
if len(second) < 2:
return
a2 = math.radians(float(second[0]))
den = math.cos(a1) * math.sin(a2) - math.sin(a1) * math.cos(a2)
t = b # 保底:前进到环带中点
if abs(den) > 1e-12:
p2x, p2z = round(b * ux - side * uz), round(b * uz + side * ux)
cand = (p2x * math.sin(a2) - p2z * math.cos(a2)) / den
if BAND_LO - 256.0 < cand < BAND_HI + 256.0:
t = cand
sys.stdout.write('! %d %d\n' % (round(t * ux), round(t * uz)))
sys.stdout.flush()
if __name__ == '__main__':
main()
评论