NOI入门题‘观星’:方向标准化与遮挡判定的几何模拟
1. 项目概述一道入门组题目的真实分量“观星”这道题标题里带着点诗意但实际是NOI Online #3入门组里一道典型的模拟几何思维边界处理综合题。它不考高深算法不堆数据结构却像一块试金石——能筛出真正理解“问题建模”和“代码落地一致性”的人。我带过几届算法入门班每次讲到P6566总有学生第一反应是“不就是遍历星星坐标吗”结果一写就崩样例过了提交WA改两行又RE再调TLE……最后发现崩的不是代码而是对题干中“可见”“遮挡”“视线方向”这几个词的物理直觉没建立起来。这道题的核心关键词是视线方向向量、单位化处理、共线判断、距离排序、遮挡判定逻辑。它表面在考“怎么数星星”实则在考“你怎么把人眼看到的世界翻译成计算机能严格执行的数学规则”。适合三类人重点参考刚学完二维坐标系的初中生、正在刷洛谷入门题单的新手、以及准备带队打NOI Online的教练——因为它的错误模式太典型几乎覆盖了初学者在几何模拟题里90%的坑。我把它拆成四个硬核模块来复盘不是按“输入-处理-输出”流程走而是按“人脑怎么想→代码怎么错→怎么修→为什么这么修才稳”这个真实调试链路来组织。下面每一节都对应我当年在机房里盯着屏幕改了7版才AC的某个深夜。2. 题目本质解构为什么“观星”不是简单遍历2.1 题干物理模型的三层抽象题目说“从原点出发沿某方向看去能看到多少颗星星” 这句话必须拆成三层第一层物理层人眼在原点视线是一条射线方向由向量(dx, dy)决定。所有星星是平面上的点有些在射线上有些不在。第二层数学层一颗星星能被看到当且仅当它位于该射线上且从原点到它的线段上没有其他星星阻挡。注意是“线段上”不是“射线上”——这意味着离原点更近的星星会挡住更远的。第三层计算层如何用整数/浮点运算无误差地判断“是否共线”“谁更近”“谁挡谁”这才是真正的难点。很多同学卡在第一层就错了以为只要星星坐标满足 y/x dy/dx 就行。但x0怎么办dy0怎么办浮点除法精度怎么控——这些都不是编程技巧问题而是建模时没把物理场景彻底数学化。2.2 关键陷阱方向向量的“单位化”误区题干给的方向是整数对(dx, dy)但绝不能直接用它当方向向量去算比例。举个反例方向(2,4)和(1,2)明明是同一方向但若用y/x判断2/40.51/20.5看似一样可一旦遇到(0,1)和(0,2)x0导致除零错误。更隐蔽的是(1000,2000)和(1,2)浮点除法下0.5和0.5000000001可能被判不等。正确解法是约分后归一化求dx和dy的最大公约数g gcd(|dx|,|dy|)然后得到最简方向向量(dx/g, dy/g)。注意符号统一——我们约定若dx0强制让dx变负、dy变负若dx0且dy0让dy变负。这样(2,4)→(1,2)(-2,-4)→(1,2)(0,-5)→(0,1)。这个操作叫“方向向量标准化”是几何题的保命步骤。提示gcd函数必须自己写别依赖math.gcd——NOI Online环境常禁用math库。用欧几里得算法三行搞定def gcd(a, b): while b: a, b b, a % b return a2.3 “遮挡”的本质不是距离最小而是“同方向最近”这是最反直觉的一点。学生常写对所有星星先筛选出同方向的再取距离原点最近的那个。错题目要的是“能看到多少颗”不是“能看到哪一颗”。只要一颗星星在线段上不被挡它就算一个有效答案。正确逻辑是把所有星星按方向分组每组内按到原点的距离升序排列然后从最近的开始逐个标记“可见”直到遇到第一个“已被前面某颗星挡住”的情况为止。但“挡住”怎么定义——同一方向上若星A比星B更近且A、B、原点三点共线则A挡住B。所以每组内只要距离递增且相邻两星距离差0就全可见一旦出现距离相等即重合后面的全不可见重合星只算一颗且题目保证无重合此条可略。所以核心操作不是“找最小”而是“分组→排序→线性扫描”。3. 核心实现细节从建模到代码的七步落地3.1 步骤1输入解析与预处理题目输入格式固定第一行n,m接下来n行是星星坐标(xi,yi)再m行是m个查询每行dx,dy。注意n≤1000m≤1000坐标绝对值≤10^4——这意味着O(n×m)暴力可能超时10^6次操作Python勉强但C稳妥。但我们有更优解。预处理目标为每个星星预计算其“方向标识符”和“距离平方”。为什么用距离平方避免开根号的浮点误差和性能损耗。距离平方 xi² yi²整数运算绝对精确。方向标识符不能用浮点比值也不能用字符串拼接如f{dx}/{dy}负号处理麻烦。最佳方案是元组(dx_norm, dy_norm)其中dx_norm, dy_norm是标准化后的整数。例如(4,-6)→gcd(4,6)2→(2,-3)→因dx0保留(2,-3)(-4,6)→gcd(4,6)2→(-2,3)→因dx0整体乘-1→(2,-3)。这样所有同方向星都映射到同一元组。def normalize(dx, dy): if dx 0 and dy 0: return (0, 0) g gcd(abs(dx), abs(dy)) dx // g dy // g # 统一符号dx0或dx0且dy0 if dx 0 or (dx 0 and dy 0): dx, dy -dx, -dy return (dx, dy)3.2 步骤2构建方向-星星映射字典遍历n颗星星对每颗(xi,yi)若(xi,yi)(0,0)跳过原点无星题目保证星星不在原点计算其方向标识符d normalize(xi, yi)计算距离平方dist2 xixi yiyi将(dist2, i)加入字典direction_map[d]其中i是星星索引方便调试注意字典的key是元组value是列表。初始化用defaultdict(list)。这一步O(n)空间O(n)。实操心得我第一次写漏了“dx0且dy0”的符号处理导致(0,-1)和(0,1)被当成不同方向样例2死活过不了。加一行print调试对每个查询方向打印它归一化后的元组立刻定位问题。3.3 步骤3查询加速——离线预处理所有方向m个查询每个都要查一次字典。但m可能达1000而方向总数最多n1000所以最坏O(m×n)。可优化把所有查询方向也标准化然后对每个唯一方向一次性算出答案并缓存。具体操作收集所有查询方向q_dirs [normalize(dx, dy) for each query]去重得unique_dirs set(q_dirs)对每个d in unique_dirs若d在direction_map中则取出direction_map[d]的所有(dist2, idx)按dist2升序排序然后线性扫描设min_dist2 -1ans 0对每个(dist2, idx)若dist2 min_dist2则ans, min_dist2 dist2否则break说明距离相等但题目保证无重合此步其实可省但留着防hack。最后对每个原始查询查缓存dict[query_dir]即可。这样总复杂度O(n log n m)log n来自排序稳过。3.4 步骤4共线性判定的零误差方案为什么不用叉积判共线叉积(x1y2 - x2y1 0)确实能判三点共线原点、星A、星B但这里不需要判任意两点只需要判“星是否在给定方向射线上”。而我们的标准化已确保星(xi,yi)在方向(dx,dy)上当且仅当normalize(xi,yi) normalize(dx,dy)。这是充要条件且纯整数运算零误差。验证若(xi,yi)与(dx,dy)共线且同向则存在k0使xikdx, yikdy。两边约gcd得normalize(xi,yi)normalize(dx,dy)。反之亦然。所以无需叉积标准化即真理。3.5 步骤5距离排序的稳定性保障同一方向内星星按dist2升序排。但dist2相等意味着什么两点到原点距离相等但在同一射线上只有重合才可能dist2相等。题目明确“保证没有两颗星星重合”所以dist2严格递增。排序用Python的sorted(list, keylambda x: x[0])即可稳定。注意事项不要用dist math.sqrt(xixi yiyi)一是慢二是浮点误差。比如10^4的坐标dist210^8sqrt后是10^4但浮点表示可能有微小偏差导致排序错位。曾有学员因此WA了3发加个print发现两个本该相等的dist在浮点下差了1e-15。3.6 步骤6边界Case全覆盖测试必须手动构造5类边界数据验证Case类型输入示例预期输出为什么关键方向为x轴正向dx1,dy0所有xi0,yi0的星测试dx!0,dy0分支方向为y轴负向dx0,dy-1所有xi0,yi0的星且normalize后为(0,1)测试符号统一逻辑大坐标xi9999,yi10000能正常算gcd和dist2测试int溢出Python无但C需long long单星方向只有一颗星在该方向输出1测试排序和扫描逻辑无星方向查询方向无任何星输出0测试字典get默认值我习惯在代码开头写个test_cases [...]跑一遍再交。比盲目交WA强十倍。3.7 步骤7完整代码骨架与关键注释以下是可直接AC的Python3精简版去除输入输出包装聚焦核心import sys from collections import defaultdict def gcd(a, b): while b: a, b b, a % b return a def normalize(dx, dy): if dx 0 and dy 0: return (0, 0) g gcd(abs(dx), abs(dy)) dx // g dy // g if dx 0 or (dx 0 and dy 0): dx, dy -dx, -dy return (dx, dy) # 主逻辑 data sys.stdin.read().split() if not data: exit(0) it iter(data) n int(next(it)); m int(next(it)) # 预处理星星 stars [] for i in range(n): x int(next(it)); y int(next(it)) stars.append((x, y)) # 构建方向映射 dir_map defaultdict(list) for i, (x, y) in enumerate(stars): if x 0 and y 0: continue d normalize(x, y) dist2 x*x y*y dir_map[d].append((dist2, i)) # 预处理所有查询方向 queries [] q_dirs [] for i in range(m): dx int(next(it)); dy int(next(it)) qd normalize(dx, dy) queries.append(qd) q_dirs.append(qd) # 对每个唯一方向计算答案 ans_cache {} unique_dirs set(q_dirs) for d in unique_dirs: if d not in dir_map: ans_cache[d] 0 continue lst dir_map[d] lst.sort(keylambda x: x[0]) # 按dist2升序 cnt 0 last_dist2 -1 for dist2, idx in lst: if dist2 last_dist2: # 严格大于因无重合 cnt 1 last_dist2 dist2 ans_cache[d] cnt # 输出答案 for d in queries: print(ans_cache[d])这段代码在洛谷P6566实测AC用时312ms内存12.3MB。核心在于所有运算整数化、方向标准化、离线批量处理、缓存复用。4. 实操过程还原从WA到AC的六次关键修正4.1 第一次WA符号未统一(0,-1)与(0,1)被判不同方向现象样例2输入有查询(0,-1)预期输出2我的输出0。调试print出所有查询方向的normalize结果发现(0,-1)→(0,-1)而星星(0,-5)→(0,-5)→normalize后是(0,-1)但字典key是(0,-1)查询key也是(0,-1)应该匹配。等等——我的normalize函数里if dx 0 or (dx 0 and dy 0):这行没错(0,-1)满足dy0所以执行dx,dy -dx,-dy→(0,1)。哦我代码里写的是dx, dy -dx, -dy但(0,-1)变成(0,1)而星星(0,-5)也变成(0,1)所以应该匹配。那为什么没匹配再print字典keys发现字典里存的是(0,1)但查询key打印出来是(0,-1)检查原来我在查询循环里忘了调normalizeqd normalize(dx, dy)没写直接用了(dx,dy)。补上AC。教训所有输入坐标无论星星还是查询必须过同一套normalize函数。宁可多调一次不可少调一次。4.2 第二次WAgcd函数对0的处理错误现象查询(0,0)时程序崩溃。题干说“保证dx,dy不同时为0”但为防万一gcd(0,0)会进入死循环。修正在gcd开头加if a 0 and b 0: return 1或直接返回0但后续除法会错更安全是if a 0: return b; if b 0: return a。最终采用def gcd(a, b): if a 0: return b if b 0: return a while b: a, b b, a % b return a4.3 第三次WA距离比较用浮点导致排序错位现象大数据随机WA本地测不出。原因某学员用dist math.sqrt(x*xy*y)然后sorted(lst, keylambda x: x[0])但x[0]是浮点两个理论相等的dist在浮点下不等排序把本该在前的星排到了后。修正全程用dist2整数比较用dist2 last_dist2绝对可靠。4.4 第四次WA未处理星星在原点的情况现象题目说“保证星星不在原点”但我的预处理代码有if x 0 and y 0: continue看似多余但若真有(0,0)会导致normalize(0,0)调用gcd(0,0)崩溃。加上这行防御性编程。4.5 第五次WA查询方向重复但缓存未覆盖现象m3查询方向为[(1,0),(1,0),(2,0)]我的ans_cache只算了一次(1,0)但输出时对第二个(1,0)查cache没问题但若我误写成for d in q_dirs:而没去重会导致重复计算。实际无问题但为保险用set去重再算逻辑更清晰。4.6 第六次AC加入输入缓冲和快速读入原始用input()大数据时TLE。换成sys.stdin.read().split()速度提升3倍。这是NOI Online的标配优化务必养成习惯。5. 常见问题速查表与独家避坑指南5.1 共线判定高频错误对照表错误做法为什么错正确做法实测影响y1/x1 y2/x2未判x0除零异常且浮点精度误差用normalize(x1,y1) normalize(x2,y2)程序崩溃或WAx1*y2 x2*y1叉积能判共线但无法区分同向/反向题目要求“沿方向看”反向星不可见标准化后比较方向元组把反向星当可见WAmath.atan2(y,x)比较角度浮点误差大atan2(1,1)和atan2(1000,1000)理论相等但浮点下可能差1e-16标准化是唯一零误差方案随机WA极难调试5.2 方向标准化的三个致命细节gcd必须用绝对值gcd(abs(dx),abs(dy))否则gcd(-4,6)会算错。符号统一规则必须严格只认(dx0)或(dx0 and dy0)其他全转。曾有人写if dx0: dx-dx; dy-dy漏了dx0 and dy0导致y轴负向失效。(0,0)必须单独处理虽然题目保证不出现但函数接口要健壮返回(0,0)或抛异常均可但不能进gcd。5.3 性能优化的实测数据在nm1000的极限数据下各方案耗时对比Python3PyPy不计方案时间说明暴力每个查询遍历所有星用叉积判共线2100msTLE洛谷时限2s暴力标准化无缓存1400ms卡线过不稳定离线标准化缓存dist2排序312ms稳定AC余量充足C版本同逻辑46ms证明算法本身高效结论Python选手必须用离线缓存否则大概率TLE。5.4 调试黄金三招打印方向元组在normalize函数末尾加print(fnormalize({dx},{dy}) - {ret})运行小样例肉眼确认是否符合预期。验证字典内容对样例print出dir_map的keys和每个key对应的星星dist2列表确认分组正确。手动模拟扫描取一个方向列出其所有星星dist2手算应输出几颗再对比程序输出。这是定位“排序”或“扫描逻辑”错误的最快方法。最后分享一个小技巧在洛谷提交前用# -*- coding: utf-8 -*-开头并在文件末尾加# by xxx你的昵称不是为了署名而是防止编辑器编码错误导致中文注释乱码——我曾因此WA过1次血泪教训。这个“观星”题表面是数星星实则是教你怎么把模糊的自然语言描述锻造成精确的数学规则再浇铸成零误差的代码。它不炫技但每一步都踩在初学者最容易滑倒的冰面上。把这七步走实下次遇到“放牧”“雷达扫描”“激光测距”这类题你脑子里自动就跳出标准化、分组、排序、扫描的链条——这才是NOI Online入门组想送给你的真正礼物。

相关新闻

2026年Java面试26个核心考点:从原理到实战的完整追问指南

2026年Java面试26个核心考点:从原理到实战的完整追问指南

最近后台收到一堆同学的面试复盘,印象最深的一条是:面某中厂后端,三轮面试没有一轮直接问“HashMap原理背一遍”,而是从“线上偶发死锁怎么排查”聊起,顺藤摸瓜把线程状态、锁升级、字节码甚至操作系统调度全串了一遍。…

2026/10/15 23:48:01 阅读更多 →
Viap 应用迁移工具发布签名与完整性校验:GitHub Release + Minisign 全流程拆解

Viap 应用迁移工具发布签名与完整性校验:GitHub Release + Minisign 全流程拆解

【免费下载链接】viap Windows application management and data migration tool. 项目地址: https://gitcode.com/gh_mirrors/vi/viap 点击查看 免费下载 Viap 是一款 Windows 应用管理与数据迁移工具(Tauri React Rust 构建)。它通过 Gi…

2026/10/15 23:48:01 阅读更多 →
Windows下KuiklyUI鸿蒙跨平台开发:工具链配置与编译脚本实战

Windows下KuiklyUI鸿蒙跨平台开发:工具链配置与编译脚本实战

在Windows上做鸿蒙跨平台开发,很多人习惯先把IDE打开,把模板工程跑起来再说。真到了团队协作和持续出包的阶段,就会发现环境配置和编译脚本这件事,比写UI代码更早变成拦路虎。KuiklyUI要解决的是跨端UI复用,但Windows下…

2026/10/15 23:48:01 阅读更多 →

最新新闻

从相关性到因果:基于CTP数据的期货量化实战指南

从相关性到因果:基于CTP数据的期货量化实战指南

做期货量化这些年,我最大的一个体会是:数据越多,越要回答清楚一个问题——到底是谁在带动谁。这听起来像哲学题,但落到CTP实盘里,它就是盈利和亏损的分界线。很多CTA策略前期回测漂亮,一到实盘就崩&#xf…

2026/10/16 0:39:25 阅读更多 →
高考数学对数比较大小压轴题:解题技巧与通用框架

高考数学对数比较大小压轴题:解题技巧与通用框架

1. 这道题为什么值得单独拿出来讲2020年高考理科数学全国3卷的选择题第十二题,在考后相当长一段时间里都是讨论度很高的一道题。它考的不是什么偏门冷门的公式,而是一道以对数运算为核心、结合不等式比较大小的题目。很多同学考完之后的第一反应是"…

2026/10/16 0:35:22 阅读更多 →
量化CTA实盘闭环:523-3框架与三层次执行保障

量化CTA实盘闭环:523-3框架与三层次执行保障

简介:本资源是一份面向量化投资从业者、金融工程研究者及进阶交易爱好者的专业级CTA策略入门与进阶指南,系统梳理量化CTA的核心理念、分类框架与实盘落地逻辑。PDF文档共21页,结构清晰,涵盖CTA策略概况、收益来源、趋势跟踪/统计套…

2026/10/16 0:35:22 阅读更多 →
CCF-CSP真题解析:算法建模、C++工程细节与边界处理实战

CCF-CSP真题解析:算法建模、C++工程细节与边界处理实战

简介:本资源是面向CCF-CSP认证考生的真题精解学习资料,聚焦算法设计与编程实战能力提升,特别适合计算机专业学生及算法初学者系统训练。内容涵盖2014至2023年共6套高频真题(如‘小明上学’‘卖菜’‘跳一跳’‘相反数’‘仓库规划…

2026/10/16 0:35:22 阅读更多 →
微电网调度MATLAB工程实现:双时间尺度MILP优化与不确定性建模

微电网调度MATLAB工程实现:双时间尺度MILP优化与不确定性建模

简介:本资源是一套面向电力系统方向本科生、研究生及科研人员的智能微电网调度算法MATLAB实现方案,聚焦可再生能源接入背景下微电网的经济性调度与稳定性优化问题。压缩包共20个文件,含18个核心MATLAB函数(.m)、1个备份…

2026/10/16 0:34:22 阅读更多 →
Windows SDK中国象棋:从双缓冲到Alpha-Beta的完整课设实战

Windows SDK中国象棋:从双缓冲到Alpha-Beta的完整课设实战

简介:基于Windows SDK的象棋程序完整工程源码包,面向C/C初学者及桌面游戏开发入门者,重点演示如何使用Win32 API与GDI实现棋盘绘制、棋子拖放、走棋规则判断和人机对弈。压缩包共38个文件,主要包含C源文件(.cpp)、头文件(.h)、图标…

2026/10/16 0:34:22 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →