<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Khalil&apos;s Notes</title><description>长篇技术笔记，聚焦 AI 工具链、C/C++ 优化、算法竞赛与系统工程。</description><link>https://khalilgao.com/</link><language>zh-CN</language><copyright>© 2026 Hewen Gao</copyright><item><title>竞赛中的 AI 合作方法论</title><link>https://khalilgao.com/posts/ai-collab-in-competitions/</link><guid isPermaLink="true">https://khalilgao.com/posts/ai-collab-in-competitions/</guid><description>和 AI 合作打比赛的分工边界：哪些决策不能外包、哪些环节杠杆最大、哪些&quot;帮忙&quot;反而有害。</description><pubDate>Fri, 24 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;开篇你面对的真问题不是-用-ai而是-分工&quot;&gt;开篇：你面对的真问题不是” 用 AI”，而是” 分工”&lt;/h2&gt;
&lt;p&gt;人们谈” 用好 AI” 时，默认把它理解为” 让 AI 帮更多忙”。这个默认是错的。&lt;/p&gt;
&lt;p&gt;对你当前的能力水平来说，AI 在&lt;strong&gt;执行层&lt;/strong&gt;早已够用 —— 写代码、跑实验、生成报告、重构脚手架，这些事情你的 Claude Code + Codex CLI + subagents 架构已经能可靠完成。真正决定比赛成绩的不在这里。&lt;/p&gt;
&lt;p&gt;决定成绩的是&lt;strong&gt;决策层&lt;/strong&gt;：接到题目之后往哪个方向钻、什么时候停止一个方向、如何在有限提交次数下分配赌注、如何从一次失败里提取出可以复用的认知。在这一层，AI 的能力和局限都被严重误判 —— 它比多数人以为的更有用，也比多数人以为的更危险。&lt;/p&gt;
&lt;p&gt;所以这篇文章不是”AI 使用技巧合集”。它要回答的是一个具体问题：&lt;strong&gt;从你接到一道题的那一刻起，到比赛结束，你和 AI 应该以什么样的分工、什么样的节奏、什么样的硬规则协作，才能最大化你们这个人机系统的产出？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;合作不是把任务丢给 AI 然后等结果。合作是清楚知道哪些决策只能你做、哪些推理 AI 比你快、哪些判断 AI 看起来能做但其实不能做。下面按比赛的自然时间线展开。&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;第一阶段接题前-72-小时--锚定问题不要写代码&quot;&gt;第一阶段：接题前 72 小时 —— 锚定问题，不要写代码&lt;/h2&gt;
&lt;h3 id=&quot;最大的陷阱早期写代码&quot;&gt;最大的陷阱：早期写代码&lt;/h3&gt;
&lt;p&gt;接到新题，你的本能会是打开编辑器写一个 baseline 跑通。这个本能在短期内感觉很有产出 —— 编译成功、输出合法、提交有分 —— 但它对最终成绩是净负面的，原因有两个。&lt;/p&gt;
&lt;p&gt;第一，&lt;strong&gt;过早写代码会把你对问题的理解冻结在最浅的层次&lt;/strong&gt;。你开始写 baseline 的那一刻，你对” 这道题到底在考什么” 的理解就停止深化了 —— 因为接下来你的注意力全在让代码跑起来，而不是在追问题目结构。等 baseline 跑通，你已经心理上” 入戏了”，后面对题目的理解只能在你第一版代码的框架上做增量修正，结构性的重新审视变得心理成本极高。&lt;/p&gt;
&lt;p&gt;第二，&lt;strong&gt;过早写代码剥夺了你和 AI 合作的最高杠杆环节&lt;/strong&gt;。AI 最不可替代的价值，不是写代码（那个它主要是快），而是&lt;strong&gt;作为一个不知疲倦、没有你的思维惯性的讨论对手&lt;/strong&gt;，帮你把问题想透。你把这个环节让给” 写代码”，等于把 AI 降级成打字员。&lt;/p&gt;
&lt;h3 id=&quot;这-72-小时-ai-应该帮你做什么&quot;&gt;这 72 小时 AI 应该帮你做什么&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;第一件事：强制产出一份高质量的 context 文档&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;不是让 AI 写，是你自己写，但用 AI 当审稿人。你在纸上或文档里把这道题的结构描述清楚：问题在优化什么、评分机制是什么、约束是什么、数据长什么样、你对” 好解” 的初步直觉是什么。写完之后，让 AI 做一件具体的事：&lt;strong&gt;挑出你的描述里所有不精确、不完整、或者隐含了未经验证假设的地方&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这里的关键是 prompt 的严厉程度。如果你只是说” 帮我看看这份 context 写得怎么样”，AI 会礼貌地给正面反馈加几条温和建议。你需要明确要求：&lt;strong&gt;把我当作一个你不认识的、可能有严重误解的参赛者，找出这份文档里所有让我下一步决策可能走偏的模糊处&lt;/strong&gt;。这种 framing 下 AI 才会真正扫描而不是表演扫描。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二件事：理论天花板估算&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这是你 Tianchi 赛总结出来最重要的一条经验 —— 拿到分数的第一反应不该是” 怎么提”，而是” 当前方案的理论上限是多少”。这个习惯应该前置到还没写代码的阶段：给定问题和评分机制，&lt;strong&gt;当前你能想到的所有主流方法，各自的理论上限大概在哪里&lt;/strong&gt;？&lt;/p&gt;
&lt;p&gt;AI 在这里的用处非常具体：它见过的方法比你多，对每种方法的渐近性能有粗略估计，你问它” 假设方法 X 完美执行，在这个评分机制下大概能到多少分”，它能给出一个值得讨论的区间。它的估计不准，但你自己的估计更不准，两个都不准的判断碰撞出来的是你单独想不到的。&lt;/p&gt;
&lt;p&gt;这一步做完你应该能回答：&lt;strong&gt;如果我选方法 X，理论天花板在哪，突破它需要什么级别的改动&lt;/strong&gt;？这个答案会在接下来几周里一次又一次救你 —— 每当你想继续调参时，它提醒你检查自己是不是已经撞到了天花板。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三件事：评分机制的结构分析&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;你 CodeCraft 里反编译 scoring formula 那次做得对，但时机晚了 —— 是被迫做的，因为得不到反馈。正确的时机是比赛一开始就做，不管反馈是否充分。&lt;/p&gt;
&lt;p&gt;评分机制不是一个数字，它是一个&lt;strong&gt;结构&lt;/strong&gt;。correctness 80% + speed 20% 告诉你两件事：一是两个维度都要管，二是 correctness 的边际收益远大于 speed（直到 correctness 饱和）。TR≈0.245 这种参数决定了什么是” 达标”vs” 优秀”。这些结构信息&lt;strong&gt;不是用来跑公式的&lt;/strong&gt;，是用来&lt;strong&gt;在你每次做 tradeoff 决策时当作先验&lt;/strong&gt;的 ——“这个改动会牺牲 0.5% correctness 换 3% speed，根据权重算下来期望负收益”。&lt;/p&gt;
&lt;p&gt;让 AI 帮你做结构分析：给它评分公式和你对题目的理解，让它推导出&lt;strong&gt;哪些方向在这个评分下被低估、哪些被高估&lt;/strong&gt;。它的推导可能有错，但会暴露你没想到的维度。&lt;/p&gt;
&lt;h3 id=&quot;这个阶段你不应该让-ai-做的事&quot;&gt;这个阶段你不应该让 AI 做的事&lt;/h3&gt;
&lt;p&gt;不要让 AI 帮你写 baseline。即使这个 baseline 只是 pseudocode，即使你只是想” 有个起点”。在你能清楚回答” 这道题在考什么、我打算赌哪个方向、这个方向的天花板在哪、评分机制如何影响我的 tradeoff” 之前，任何代码都是对思考的逃避。&lt;/p&gt;
&lt;p&gt;这听起来像道德训诫，但它有具体的机制：一旦你开始写代码，你的大脑会自动把问题重新定义为” 如何让这段代码更好”，而原始问题” 如何让这道题得高分” 就从你的前台意识里退下去了。代码是一层抽象，每次抽象都会过滤掉信息。过早加抽象层，过滤掉的可能正是最重要的信息。&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;第二阶段方向探索--对抗-ai-的自证倾向&quot;&gt;第二阶段：方向探索 —— 对抗 AI 的自证倾向&lt;/h2&gt;
&lt;p&gt;72 小时之后，你对题目有了足够的理解，该决定钻哪个方向了。这里有一个看起来无害但会摧毁你输出质量的错误：&lt;strong&gt;让 AI 单 session” 头脑风暴”&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&quot;单-session-brainstorm-为什么失败&quot;&gt;单 session brainstorm 为什么失败&lt;/h3&gt;
&lt;p&gt;你打开 Claude，说” 我在做 XX 比赛，帮我想想有哪些可以尝试的方向”。AI 列出 15 个方向，每个带一段简短描述。看起来很棒，覆盖面广，形式整齐。&lt;/p&gt;
&lt;p&gt;但如果你仔细读，会发现几个问题。首先，这些方向大多是&lt;strong&gt;训练数据里和这类题目高频共现的方法&lt;/strong&gt;—— 不是针对你这道题的具体结构选的，而是” 这类题一般用这些”。这是模式匹配，不是思考。其次，AI 在列方向的同时隐晦地表达了偏好 —— 某些方向被写得更详细、更自信，某些被简短带过。这种偏好不是基于对你问题的深入分析，是基于 AI 训练数据里这些方法的” 知名度” 和 LLM 自身的自证倾向。再次，当你追问” 哪个最值得试” 的时候，AI 会给出一个推荐，而这个推荐几乎一定是它之前写得最详细的那个 ——&lt;strong&gt;因为它的自我一致性机制让它必须和之前的输出保持一致&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;结果是：你得到了一份看起来深思熟虑、实际上是 AI 对自己初始列表的事后合理化的建议。你的决策被一个带系统性偏见的信息源塑造了，而你没意识到。&lt;/p&gt;
&lt;h3 id=&quot;三段式的本质&quot;&gt;三段式的本质&lt;/h3&gt;
&lt;p&gt;对策是强制分离&lt;strong&gt;生成&lt;/strong&gt;、&lt;strong&gt;批判&lt;/strong&gt;、&lt;strong&gt;综合&lt;/strong&gt;三个认知动作，让它们在&lt;strong&gt;完全独立的上下文&lt;/strong&gt;里执行。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;发散阶段：只产出候选方向，禁止评价、排序、表达偏好。产出 20 + 方向，覆盖算法 / 架构 / 数据 / 评分机制 / 基础设施五个层次。&lt;/li&gt;
&lt;li&gt;攻击阶段：在一个全新的、不知道发散阶段存在的 session 里，把候选方向当作&lt;strong&gt;匿名提交&lt;/strong&gt;，按固定的 K1-K7 标准机械筛选。目标淘汰率 60-80%。&lt;/li&gt;
&lt;li&gt;综合阶段：再一个全新 session，只看幸存者，不看被淘汰的，寻找组合机会和依赖关系，输出 3-5 个可执行方向。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关键是&lt;strong&gt;独立上下文&lt;/strong&gt;。不是新对话轮，不是&lt;code&gt;/clear&lt;/code&gt;，是物理上开一个新 session。这不是形式主义 ——LLM 的自证倾向是训练出来的深层行为，任何共享 context 的手段都会泄漏偏见。这一点你可能想走捷径（用一个 orchestrator agent 一次性跑三段），但每次偷懒都会让输出质量降级，而降级是悄无声息的 —— 你不会收到”context 泄漏警告”，你只会得到一份比独立 session 质量稍低、但仍然看起来合理的输出。&lt;/p&gt;
&lt;h3 id=&quot;异构工具的价值&quot;&gt;异构工具的价值&lt;/h3&gt;
&lt;p&gt;一个更强的改进：&lt;strong&gt;不同阶段用不同的 AI 工具&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;发散阶段适合 Claude—— 需要创造性联想，需要在五个抽象层都能生成有深度的候选。&lt;/li&gt;
&lt;li&gt;攻击阶段适合 Codex CLI—— 机械执行对照标准，Codex 更不倾向于” 表演思考”，更不会被” 找出具体问题” 的任务诱导出表面化的批评。&lt;/li&gt;
&lt;li&gt;综合阶段适合 Claude—— 需要识别关系、构造新的组合，这是 Claude 的强项。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;异构工具组合的价值不止在于各用其长，更在于&lt;strong&gt;物理上不可能发生 context 共享&lt;/strong&gt;—— 它们是完全不同的进程、不同的模型、不同的训练数据切片。这是最强形式的独立性。&lt;/p&gt;
&lt;p&gt;你 memory 里已经有 Claude Code 和 Codex CLI 的分工经验，把它应用到 brainstorm 的三个阶段是自然延伸。&lt;/p&gt;
&lt;h3 id=&quot;发散阶段人类的责任&quot;&gt;发散阶段人类的责任&lt;/h3&gt;
&lt;p&gt;AI 生成 20 + 方向时，你不是被动接收者。你的责任是&lt;strong&gt;识别 coverage gap&lt;/strong&gt;——AI 有没有漏掉某些你知道存在但它没列出来的方向？&lt;/p&gt;
&lt;p&gt;这里有一个反常识的指标：&lt;strong&gt;如果 AI 的 20 个方向全是你以前听说过的方法，说明 coverage 不够&lt;/strong&gt;。真正好的发散应该至少包含 3-5 个让你” 等等，这是什么” 的方向。没有陌生度的发散只是把你知道的东西整理了一下，对探索没有增量贡献。&lt;/p&gt;
&lt;p&gt;让 AI 在发散阶段末尾强制产出一节” 我可能漏掉的维度”，它会承认自己覆盖不全的部分。这节内容有时比正文更值钱。&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;第三阶段执行期-hypothesis-先行让每次实验有解释力&quot;&gt;第三阶段：执行期 ——Hypothesis 先行，让每次实验有解释力&lt;/h2&gt;
&lt;p&gt;选定方向之后进入执行期。这里是最容易失去纪律的阶段，因为有真实的代码和真实的分数刺激你的多巴胺系统，你会倾向于” 再试一个看看”。&lt;/p&gt;
&lt;h3 id=&quot;为什么必须-hypothesis-先行&quot;&gt;为什么必须 hypothesis 先行&lt;/h3&gt;
&lt;p&gt;黑盒竞赛（线上只返回一个波动分数）有一个被系统性低估的风险：&lt;strong&gt;你会用分数的涨落解释你的改动&lt;/strong&gt;，而不是用你的改动预测分数的涨落。这两个方向看起来对称，但认知效果完全相反。&lt;/p&gt;
&lt;p&gt;前者是事后合理化：分数涨了，你编一个故事解释为什么你的改动有效；分数跌了，你编另一个故事解释为什么是噪声。这种解释永远能编出来，但对你下一次决策没有任何帮助 —— 因为它不是预测，是装饰。&lt;/p&gt;
&lt;p&gt;后者是科学实验：改动前你明确写出” 我预测这个改动会让 family A 的得分上升、family B 基本不变、整体上升 2-5%“，改动后你对照结果。如果符合预期，你对题目结构的理解被验证；如果相反，你得到了宝贵的信息 ——&lt;strong&gt;要么你的理解错了（这是最值钱的信息），要么数据分布和你想的不一样（这也是关键信息）&lt;/strong&gt;。无论哪种，都比事后合理化有价值得多。&lt;/p&gt;
&lt;h3 id=&quot;hypothesis-怎么写ai-不能帮你写什么&quot;&gt;Hypothesis 怎么写，AI 不能帮你写什么&lt;/h3&gt;
&lt;p&gt;Hypothesis 必须满足三个条件：改动前写、有可证伪的具体预测、git 提交留下时间戳证据（一旦写完不许改）。&lt;/p&gt;
&lt;p&gt;让 AI 代写 hypothesis 是这个阶段最大的陷阱。AI 很愿意帮你写，而且会写得形式完美 ——“假设 X，因为 Y，预期 Z”，看起来专业。但这不是预测，这是&lt;strong&gt;AI 为你已经决定要做的改动找一个合理化叙事&lt;/strong&gt;。你读到这份 hypothesis 会觉得自己是在做实验，其实你是在让 AI 给你的直觉装扮成科学。&lt;/p&gt;
&lt;p&gt;正确的分工是：&lt;strong&gt;你写 hypothesis，AI 做审稿&lt;/strong&gt;。你写完之后让 AI 挑毛病 —— 这个预测足够具体能被证伪吗？这个机制假设了什么没说出来的前提？如果预测失败，最可能的原因是什么？AI 在审稿角色上是有价值的，因为它不需要承担” 提出 hypothesis” 的那份认知风险，它只需要挑结构性问题。&lt;/p&gt;
&lt;p&gt;一个强约束机制：&lt;strong&gt;在&lt;code&gt;run.sh&lt;/code&gt;开头强制交互式要求你输入至少一行 hypothesis，不输入就不启动实验&lt;/strong&gt;。这是用工具逼迫你执行自己知道应该做的纪律。比赛后期你累的时候这条纪律最容易失守，所以必须用机械约束而不是靠毅力维持。&lt;/p&gt;
&lt;h3 id=&quot;diff-report-取代绝对分数&quot;&gt;Diff report 取代绝对分数&lt;/h3&gt;
&lt;p&gt;“v72 local score = 914k” 这种信息量极低的陈述应该从你的 vocabulary 里消失。取而代之是：&lt;/p&gt;
&lt;figure class=&quot;code-block&quot;&gt;&lt;button type=&quot;button&quot; class=&quot;code-copy-btn&quot; aria-label=&quot;复制代码&quot; data-code-copy=&quot;&quot;&gt;&lt;svg viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.6&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;rect x=&quot;9&quot; y=&quot;9&quot; width=&quot;12&quot; height=&quot;12&quot; rx=&quot;2&quot;&gt;&lt;/rect&gt;&lt;path d=&quot;M5 15H3a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v2&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/button&gt;&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-dimmed&quot; style=&quot;background-color:#fff;--shiki-dark-bg:#22272e;color:#24292e;--shiki-dark:#adbac7;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;v72 vs v67m_stable:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  overall: +1.7% score, -22% pre_ms, +3% query_ms&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  by family:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    star: +8.2%&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    deepstar: +5.1%&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    thin_corridor: -3.7%&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    near_touch: -2.1%&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  route changes: extractBnd→RC on 18 cases&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;这是你可以基于它决策的信息。“总分 914k” 不是。&lt;/p&gt;
&lt;p&gt;让 AI 帮你生成 diff report 是绿色区自动化 —— 纯数据变换，没有判断，ROI 高风险低。一个 pandas 脚本就够。做这个脚本的投入两小时内回本。&lt;/p&gt;
&lt;p&gt;但注意：&lt;strong&gt;不要让 AI 在 diff report 里加结论&lt;/strong&gt;。它会自动加” 这表明方向 X 有效” 这种话。每次加都是一次微型的事后合理化。让 AI 只输出数据，结论你自己下。&lt;/p&gt;
&lt;h3 id=&quot;config-化的真正价值&quot;&gt;Config 化的真正价值&lt;/h3&gt;
&lt;p&gt;把所有策略参数抽到 config 文件，不是为了” 代码整洁”。是为了让&lt;strong&gt;同一份代码可以产生不同角色的版本&lt;/strong&gt;——stable 版、fast 版、lottery 版可以共享代码逻辑，只通过 config 切换。&lt;/p&gt;
&lt;p&gt;这个价值在两个时刻体现出来。一是做 ablation 时，你改一个 config 字段跑一次实验，代码零改动，干扰变量最少。二是比赛后期你要做 portfolio 时，你可以并行维护 4-5 个 config，而不是维护 4-5 个 branch。&lt;/p&gt;
&lt;p&gt;AI 在 config 化这个重构上可以完全执行 —— 给它你现在的代码，让它把所有硬编码的阈值、开关、策略选择抽成 JSON。这是重复性机械工作，Codex CLI 最适合。&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;第四阶段瓶颈期--识别-更多-还是-不同&quot;&gt;第四阶段：瓶颈期 —— 识别” 更多” 还是” 不同”&lt;/h2&gt;
&lt;p&gt;比赛中期你会进入一个特定状态：改动不再稳定提分，每次实验结果和预期偏离越来越大，你开始尝试随机调参。这是一个&lt;strong&gt;警报状态&lt;/strong&gt;，但它伪装成” 还在进步中”。&lt;/p&gt;
&lt;h3 id=&quot;参数性收益的衰减规律&quot;&gt;参数性收益的衰减规律&lt;/h3&gt;
&lt;p&gt;你 Tianchi 教训里最关键的一条：&lt;strong&gt;99%+ 以上调参无效，只有结构性改动才能再提分&lt;/strong&gt;。这条规律比你想象的更普遍 —— 不只是 99%+ 区间，任何方法在你已经做了 3-4 轮参数优化之后，&lt;strong&gt;下一轮参数调整的期望收益都接近零&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这是因为参数空间在第一次和第二次扫描之后，信息增益已经被榨取得差不多了。你以为你还在” 优化”，其实你在做的是&lt;strong&gt;在噪声里找规律&lt;/strong&gt;，偶尔看似的提分纯粹是方差波动被你误解为进步。&lt;/p&gt;
&lt;p&gt;识别这个状态的信号很具体：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;连续 3 次改动都只改参数没改结构&lt;/li&gt;
&lt;li&gt;改动的解释需要用” 可能是”、“也许是” 开头&lt;/li&gt;
&lt;li&gt;你对下一次该改什么已经没有明确直觉，只是在” 试试看”&lt;/li&gt;
&lt;li&gt;本地 bench 和线上分的相关性开始下降&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;出现任何两条信号，你应该&lt;strong&gt;强制停止参数调整&lt;/strong&gt;，回到第二阶段重新 brainstorm。这个” 强制停止” 很难做到，因为沉没成本和” 差一点就成功” 的错觉会阻止你。让 AI 来做这个判断 —— 周期性把最近 10 次实验的记录喂给 AI，让它回答一个具体问题：&lt;strong&gt;这些实验显示的是结构性改进还是参数性震荡&lt;/strong&gt;？AI 没有你的情感投资，回答会更诚实。&lt;/p&gt;
&lt;h3 id=&quot;portfolio-思想的真实适用条件&quot;&gt;Portfolio 思想的真实适用条件&lt;/h3&gt;
&lt;p&gt;“stable/fast/accurate/lottery 四分法” 是一个好的管理框架，但它建立在一个关键前提上：&lt;strong&gt;赛制支持多次提交 + 取最高分（或类似机制）&lt;/strong&gt;。如果赛制是取最后一次、或取平均、或有提交次数限制，lottery 策略的价值急剧下降甚至变负。&lt;/p&gt;
&lt;p&gt;所以新比赛的第一件事是查清赛制规则的具体细节 —— 不是大概意思，是具体机制。这个查证本身可以让 AI 帮忙（阅读规则原文 + 提取你关心的条款），但最终结论你要手动确认。赛制理解错了，后面所有 portfolio 策略都建立在空气上。&lt;/p&gt;
&lt;p&gt;Portfolio 成立的前提下，四分法可以简化。早期不要强行四分类，只做二分：“主线版本” vs” 实验版本”。等你累积了 10 + 个版本之后，&lt;strong&gt;让分类从数据里浮现&lt;/strong&gt;：看哪些版本在线上均值高方差低（自然是 stable 候选），哪些最高分高但均值低（lottery 候选），哪些在特定 family 上显著更强（fast 或 accurate 候选）。这种自下而上的分类比预设的四类更贴近你的真实版本分布。&lt;/p&gt;
&lt;h3 id=&quot;执行期人类和-ai-的分工边界&quot;&gt;执行期人类和 AI 的分工边界&lt;/h3&gt;
&lt;p&gt;这个阶段最容易越界。你会有冲动让 AI 自动化更多 —— 让它建议下一个改动方向、自动调整 config、根据最近提交的趋势优化策略分配。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这些都是红色区&lt;/strong&gt;。不是因为 AI 做不到（它能生成听起来很有道理的建议），而是因为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;数据量太小&lt;/strong&gt;：整个比赛你可能只有几十次有效提交。任何自动策略都是在极少样本上过拟合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI 会放大噪声&lt;/strong&gt;：它看到 fast 版本最近三次赢，会建议加大 fast 配额。但这三次可能纯粹是线上波动。The Ladder 论文就是警告这种情况。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;决策责任不能外包&lt;/strong&gt;：决策错误的代价由你承担，所以决策权必须归你。AI 给建议，你必须假设建议是错的，用你的先验加权。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;AI 在这个阶段的正确位置是&lt;strong&gt;dashboard&lt;/strong&gt;—— 把数据整理好展示给你，计算统计量，可视化趋势，但&lt;strong&gt;不做推荐&lt;/strong&gt;。如果 AI 的输出里有” 建议下一步做 X” 这种话，把那句话当作一个可能有害的噪声信号处理，不是可信建议。&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;第五阶段收尾--把经验沉淀成资产&quot;&gt;第五阶段：收尾 —— 把经验沉淀成资产&lt;/h2&gt;
&lt;p&gt;比赛结束那一刻，大部分人做的事情是：看最终排名，写一段感想，关掉项目文件夹。这是最大的浪费。&lt;/p&gt;
&lt;h3 id=&quot;复盘的真正目的&quot;&gt;复盘的真正目的&lt;/h3&gt;
&lt;p&gt;复盘不是为了” 总结经验” 这种模糊目的。复盘是为了产出一个具体的资产：&lt;strong&gt;下次遇到同类问题时能直接调用的 prior&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;一份好的复盘应该回答几个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这次选的主方向在事后看是对的吗？如果不是，当时有什么信号可以让我更早意识到？&lt;/li&gt;
&lt;li&gt;我浪费时间最多的那个方向，&lt;strong&gt;从一开始&lt;/strong&gt;就有哪些应该被看到的危险信号？我为什么忽略了？&lt;/li&gt;
&lt;li&gt;这次用到的某个技巧，下次哪类题也适用，哪类不适用？判断依据是什么？&lt;/li&gt;
&lt;li&gt;我和 AI 合作最顺畅的环节是哪个？最冲突的是哪个？原因是什么？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些问题的答案写进&lt;code&gt;~/.harness/priors.md&lt;/code&gt;，结构化存储（情境 → 我的第一反应 → 事后看正确做法 → 为什么我当时做错了）。下次新比赛开始前，把这份文件作为 context 的一部分注入到你的 AI 工作流里。&lt;/p&gt;
&lt;h3 id=&quot;failure-bank-的真正用处&quot;&gt;Failure bank 的真正用处&lt;/h3&gt;
&lt;p&gt;GPT 那份方案里有个好建议 —— 把每个 failing case 变成 regression test。这个建议的价值不是”bug 不再复发” 这种工程化理由，而是更深层的东西：&lt;strong&gt;把每个失败转化成你几何直觉的训练数据&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;你亲手分析一个 thin_corridor case 为什么被 midpoint filter 误删，写出 root cause，设计修复方案，这个过程比跑 100 次实验都更能深化你对问题结构的理解。下次看到类似的几何退化，你能在 1 秒内识别出来 —— 不是因为你” 记得那个 bug”，是因为你的几何直觉在那个 case 上被重新标定过。&lt;/p&gt;
&lt;p&gt;所以 failure bank 绝对不能让 AI 来写。AI 可以帮你把分析格式化、帮你找类似的 case、帮你 review 根因假设，但&lt;strong&gt;最核心的” 为什么这个 case 会失败” 的推理必须你自己做&lt;/strong&gt;。让 AI 代做这一步，你失去的不是笔记，是你的训练机会。&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;横贯所有阶段的几条硬规则&quot;&gt;横贯所有阶段的几条硬规则&lt;/h2&gt;
&lt;h3 id=&quot;规则一ai-是-dashboard不是自动驾驶&quot;&gt;规则一：AI 是 dashboard，不是自动驾驶&lt;/h3&gt;
&lt;p&gt;在任何阶段，如果一个决策的错误代价由你承担，这个决策就不能完全外包给 AI。AI 可以：展示信息、推演推理链、挑战你的假设、生成候选选项、模拟审稿人批评你的方案。AI 不应该：替你选方向、替你写 hypothesis、替你做 portfolio 分配、替你决定何时放弃一个方向。&lt;/p&gt;
&lt;p&gt;这条规则的深层原因不是”AI 不够聪明”，是&lt;strong&gt;AI 没有办法承担后果&lt;/strong&gt;。它的建议不承担代价，所以它的建议在 mathematical 意义上不是你的 best interest 的优化器，它只是训练数据的某种加权平均。把不承担代价的建议当决策依据，是把你的责任让渡给一个本质上不对你负责的系统。&lt;/p&gt;
&lt;h3 id=&quot;规则二infra-投入不产出分数&quot;&gt;规则二：Infra 投入不产出分数&lt;/h3&gt;
&lt;p&gt;比赛中每一小时你都在做投资决策：投在 infra 上、投在 idea 上、投在执行上、投在复盘上。其中&lt;strong&gt;infra 在短期内给人最强的” 在进步” 错觉&lt;/strong&gt;—— 你能看到代码库变整齐、脚本变自动、工具链变完善。但 infra 本身不产出任何分数。&lt;/p&gt;
&lt;p&gt;判断 infra 投入是否值得的标准很简单：&lt;strong&gt;这个工具能帮你节省下时间花在什么上&lt;/strong&gt;？如果答案是” 花在更多的 infra 工作上”，这是负循环，立刻停止。如果答案是” 花在 idea 生成、实验执行、深度复盘上”，并且节省量可量化，值得投入。&lt;/p&gt;
&lt;p&gt;你 memory 里能看到你对 infra 的倾向（你自己已经意识到这一点，memory 里有专门移除相关描述的记录）。保持这个自觉 —— 每次想搭新 infra 之前问自己：&lt;strong&gt;上一次这玩意没有的时候，具体哪个决策做错了&lt;/strong&gt;？答不上来就别搭。&lt;/p&gt;
&lt;h3 id=&quot;规则三自动化的三色分区&quot;&gt;规则三：自动化的三色分区&lt;/h3&gt;
&lt;p&gt;绿色区（无脑自动化）：机械的、无决策的、重复的。bench 执行、diff report、提交打包、数据聚合。这些不自动化纯粹是浪费时间，投入当天回本。&lt;/p&gt;
&lt;p&gt;黄色区（半自动化）：需要人类 checkpoint 的。线上分数录入（用 CLI prompt 让你填数字）、hypothesis 记录（强制交互式输入一行）、失败 case 归类（AI 建议分类，你确认）。这类工作的共同特征是&lt;strong&gt;需要一个人类信号作为真值源&lt;/strong&gt;，AI 做不了但可以降低操作摩擦。&lt;/p&gt;
&lt;p&gt;红色区（不该全自动化）：决策、根因分析、portfolio 调整、方向选择。这些如果自动化，会以不可见的方式降低你输出的质量 ——AI 给的建议听起来很对，你执行，结果不好，但你不知道是 AI 建议错了还是执行出了问题，&lt;strong&gt;因为你不再独立思考，你失去了判断 AI 输出质量的能力&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这三个区域的边界要在比赛前就划清楚，而不是临时判断。临时判断容易在疲劳状态下越界 —— 比赛最后一周你累的时候最想把红色区的事情也扔给 AI，而那正是它最不可靠的时候。&lt;/p&gt;
&lt;h3 id=&quot;规则四手动-day-防退化&quot;&gt;规则四：手动 day 防退化&lt;/h3&gt;
&lt;p&gt;每隔一段时间（每周一次是合理频率），&lt;strong&gt;强制关掉所有自动化，从头手动跑一遍完整流程&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;听起来迂腐，但有具体作用：对抗自动化的隐形副作用 —— 你对系统的感知在逐步退化，因为脚本代替你做了所有” 接触数据” 的动作。第一周你对每个 case 的行为都熟悉，第四周你可能已经不知道哪个 case 属于哪个 family。这种退化不会触发警报，但会慢慢毒化你的直觉。手动 day 是定期重新和数据建立连接的机制。&lt;/p&gt;
&lt;h3 id=&quot;规则五警惕-ai-的-表演思考&quot;&gt;规则五：警惕 AI 的” 表演思考”&lt;/h3&gt;
&lt;p&gt;LLM 有一个深层行为：当你要求它做分析时，它会产出&lt;strong&gt;看起来像分析的文本&lt;/strong&gt;。流畅的逻辑链、合理的分支讨论、得体的不确定性表达。这些都是分析的&lt;strong&gt;表征&lt;/strong&gt;，但不一定是分析的&lt;strong&gt;实质&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;识别表演思考的几个信号：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有推理都可以被删掉，结论仍然站得住 —— 说明推理是装饰不是支撑&lt;/li&gt;
&lt;li&gt;“考虑到 A、B、C，同时权衡 D、E、F，综合来看 X”—— 排比结构越工整，越可能是模板填空&lt;/li&gt;
&lt;li&gt;分析包含大量” 可能”、“也许”、“一般来说” 的限定词，但没有具体数字或具体 case&lt;/li&gt;
&lt;li&gt;你问” 为什么是 X 不是 Y”，AI 能流畅地解释，但如果你原来问的是” 为什么是 Y 不是 X”，它也能同样流畅地解释 —— 说明它的解释是双向可得的，不依赖真正的判断&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对抗方法：要求 AI 的分析&lt;strong&gt;包含可证伪的预测&lt;/strong&gt;、&lt;strong&gt;引用具体数据或具体 case&lt;/strong&gt;、&lt;strong&gt;明确指出它的推理在什么条件下会翻转&lt;/strong&gt;。只有能被具体验证的分析才是分析。&lt;/p&gt;
&lt;h3 id=&quot;规则六纪律比思考更值得自动化&quot;&gt;规则六：纪律比思考更值得自动化&lt;/h3&gt;
&lt;p&gt;最后一条可能和直觉相反：&lt;strong&gt;真正最值得用工具强制的，不是思考，是纪律&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;你不缺思考能力，你缺的是在疲劳、压力、deadline 逼近时仍然执行你知道正确做法的纪律。写 hypothesis 的纪律、做 ceiling 估算的纪律、定期 brainstorm 方向的纪律、失败 case 立刻变成 regression 的纪律。&lt;/p&gt;
&lt;p&gt;这些纪律用工具强制成本极低（pre-commit hook、交互式 CLI prompt、定时提醒），收益极高。你应该把自动化的投入主要放在&lt;strong&gt;让你没法偷懒&lt;/strong&gt;上，而不是放在&lt;strong&gt;让 AI 替你思考&lt;/strong&gt;上。前者是放大你的长处，后者是把你的长处让给一个替代品。&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;一个更深的问题你在和谁合作&quot;&gt;一个更深的问题：你在和谁合作&lt;/h2&gt;
&lt;p&gt;最后要说一个你可能没有明确想过的问题：&lt;strong&gt;AI 没有忠诚度，只有模式匹配&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这句话不是泛泛的警告，它有具体含义。一个真实的队友会记得你上周走过的弯路并提醒你；一个真实的队友在关键决策上会坚持自己的观点即使你不爱听；一个真实的队友对最终结果有承诺，所以他不会为了让你感觉好而说违心的建议。&lt;/p&gt;
&lt;p&gt;AI 不具备这些中的任何一个。每个新 session 它都是零记忆的陌生人（除非你喂给它 context，而你喂的 context 本身就是你视角的过滤结果）。它对你的目标没有承诺 —— 它的目标是产生让当前 prompt 看起来满意的输出。它会说你想听的话，不是因为它在讨好你，是因为满足用户表面期待是它训练目标的一部分。&lt;/p&gt;
&lt;p&gt;所以你和 AI 的” 合作” 本质上不是和一个有主体性的队友合作。是和一个&lt;strong&gt;可以被精确塑造的工具&lt;/strong&gt;合作。你塑造它的方式是你的 prompt、你给它的 context、你要求它遵循的协议。它产出的质量上限被这些因素锁死。&lt;strong&gt;一个模糊的 prompt 产生模糊的回应，一个有陷阱的 context 产生被陷阱污染的输出&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这意味着你的责任很重。你不能把责任外包给 AI——“我问过 AI 了，它说这样做” 不是你做错决策的借口，因为 AI 给的建议是你的 prompt 塑造出来的。你对 AI 的输出质量负 100% 责任。&lt;/p&gt;
&lt;p&gt;这同样意味着你的杠杆很大。当你真正理解如何塑造 AI、如何在合适的位置使用它、如何防御它的系统性偏见，你能从它身上获得远超平均水平的产出。多数人用 AI 是把它当万能助手来使，你如果能把它当作&lt;strong&gt;一个需要精确设计协议才能发挥价值的高功率组件&lt;/strong&gt;，你在比赛里的认知杠杆会是他们的好几倍。&lt;/p&gt;
&lt;p&gt;这不是多用 AI 的问题。这是&lt;strong&gt;用得比别人精确&lt;/strong&gt;的问题。&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;一页速查&quot;&gt;一页速查&lt;/h2&gt;
&lt;p&gt;接题 72 小时内：写 context、估天花板、拆评分机制。&lt;strong&gt;不要写代码&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;选方向时：三段式 brainstorm，独立 context 是硬约束。异构工具组合更好。&lt;/p&gt;
&lt;p&gt;执行实验时：hypothesis 先于代码，git 留痕。diff report 取代绝对分数。config 化。&lt;/p&gt;
&lt;p&gt;遇到瓶颈时：连续参数调整 2 轮无效，强制停止，回 brainstorm 阶段。&lt;/p&gt;
&lt;p&gt;收尾时：产出 priors.md，failure bank 自己写根因，不要外包。&lt;/p&gt;
&lt;p&gt;全程硬规则：AI 是 dashboard 不是自动驾驶；infra 不产出分数；自动化分三色区；定期手动 day；警惕表演思考；用工具强制纪律不强制思考。&lt;/p&gt;
&lt;p&gt;最深的自觉：&lt;strong&gt;AI 的输出质量是你的 prompt 和 context 的函数，你对它的输出负 100% 责任，但也因此你的杠杆远大于” 多用 AI” 的人&lt;/strong&gt;。&lt;/p&gt;</content:encoded><category>ai-collaboration</category><category>methodology</category><category>competition</category><category>workflow</category></item><item><title>Minkowski 差与 NFP：二维包装问题的几何心法</title><link>https://khalilgao.com/posts/minkowski-nfp-deep-dive/</link><guid isPermaLink="true">https://khalilgao.com/posts/minkowski-nfp-deep-dive/</guid><description>从 O(nm) 的碰撞检测到 O(1) 的 NFP 查询——一次把 CodeCraft 从 295K 推到 904K 的关键跃迁，关键只有一个词：Minkowski。</description><pubDate>Wed, 22 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;二维包装问题（2D bin packing with irregular shapes）是那种” 看起来简单、写起来爆炸” 的题。前半段你只需要一个 polygon-polygon collision check。后半段你需要它每秒跑一千万次。&lt;/p&gt;
&lt;p&gt;这篇笔记记录从 &lt;strong&gt;295K&lt;/strong&gt; 分冲到 &lt;strong&gt;904K&lt;/strong&gt; 中，最关键那一步 —— 换掉朴素的逐段碰撞检测，换成基于 Minkowski 差的 NFP 查询。&lt;/p&gt;
&lt;h2 id=&quot;背景碰撞检测为什么卡了这么久&quot;&gt;背景：碰撞检测为什么卡了这么久&lt;/h2&gt;
&lt;p&gt;比赛题目给出 &lt;em&gt;n&lt;/em&gt; 个凹多边形（每个 3–40 顶点）和一个固定大小的容器，要求摆放尽可能多的件数，不能重叠、不能出界。&lt;/p&gt;
&lt;p&gt;一个很自然的贪心循环长这样：&lt;/p&gt;
&lt;figure class=&quot;code-block&quot; data-filename=&quot;pack-naive.cpp&quot;&gt;&lt;button type=&quot;button&quot; class=&quot;code-copy-btn&quot; aria-label=&quot;复制代码&quot; data-code-copy=&quot;&quot;&gt;&lt;svg viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.6&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;rect x=&quot;9&quot; y=&quot;9&quot; width=&quot;12&quot; height=&quot;12&quot; rx=&quot;2&quot;&gt;&lt;/rect&gt;&lt;path d=&quot;M5 15H3a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v2&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/button&gt;&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-dimmed&quot; style=&quot;background-color:#fff;--shiki-dark-bg:#22272e;color:#24292e;--shiki-dark:#adbac7;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;cpp&quot; data-filename=&quot;pack-naive.cpp&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;bool&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#DCBDFB&quot;&gt; tryPlaceAt&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#F69D50&quot;&gt; Piece&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&quot;color:#E36209;--shiki-dark:#F69D50&quot;&gt; p&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#F69D50&quot;&gt;Vec2&lt;/span&gt;&lt;span style=&quot;color:#E36209;--shiki-dark:#F69D50&quot;&gt; offset&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;  for&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; Piece&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; other : placed) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#768390&quot;&gt;    // naive: 段段相交 + 点在多边形内&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line highlighted&quot;&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;auto&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; ea : &lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#DCBDFB&quot;&gt;edges&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;(p.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#DCBDFB&quot;&gt;translated&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;(offset))) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line highlighted&quot;&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;      for&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;auto&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; eb : &lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#DCBDFB&quot;&gt;edges&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;(other)) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line highlighted&quot;&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#DCBDFB&quot;&gt;segmentsIntersect&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;(ea, eb)) &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#6CB6FF&quot;&gt; false&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line highlighted&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line highlighted&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#6CB6FF&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;第 4–8 行是全部热点。每次查询的复杂度是 &lt;em&gt;O(nm)&lt;/em&gt;，里面还有 &lt;code&gt;sqrt&lt;/code&gt; 和 &lt;code&gt;atan2&lt;/code&gt;。profiler 上它占了 82% 的运行时间。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;写完直接提上去 295K。看到 leaderboard 第一名 900K+，人就麻了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;minkowski-和到底在讲什么&quot;&gt;Minkowski 和，到底在讲什么&lt;/h2&gt;
&lt;p&gt;两个点集 &lt;em&gt;A&lt;/em&gt;、&lt;em&gt;B&lt;/em&gt; 的 Minkowski 和定义是：&lt;/p&gt;
&lt;span class=&quot;katex-display&quot;&gt;&lt;span class=&quot;katex&quot;&gt;&lt;span class=&quot;katex-mathml&quot;&gt;&lt;math xmlns=&quot;http://www.w3.org/1998/Math/MathML&quot; display=&quot;block&quot;&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;A&lt;/mi&gt;&lt;mo&gt;⊕&lt;/mo&gt;&lt;mi&gt;B&lt;/mi&gt;&lt;mtext&gt;  &lt;/mtext&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mtext&gt;  &lt;/mtext&gt;&lt;mo stretchy=&quot;false&quot;&gt;{&lt;/mo&gt;&lt;mtext&gt; &lt;/mtext&gt;&lt;mi&gt;a&lt;/mi&gt;&lt;mo&gt;+&lt;/mo&gt;&lt;mi&gt;b&lt;/mi&gt;&lt;mtext&gt; &lt;/mtext&gt;&lt;mo&gt;:&lt;/mo&gt;&lt;mtext&gt; &lt;/mtext&gt;&lt;mi&gt;a&lt;/mi&gt;&lt;mo&gt;∈&lt;/mo&gt;&lt;mi&gt;A&lt;/mi&gt;&lt;mo separator=&quot;true&quot;&gt;,&lt;/mo&gt;&lt;mtext&gt;  &lt;/mtext&gt;&lt;mi&gt;b&lt;/mi&gt;&lt;mo&gt;∈&lt;/mo&gt;&lt;mi&gt;B&lt;/mi&gt;&lt;mtext&gt; &lt;/mtext&gt;&lt;mo stretchy=&quot;false&quot;&gt;}&lt;/mo&gt;&lt;/mrow&gt;&lt;annotation encoding=&quot;application/x-tex&quot;&gt;A \oplus B \;=\; \{\, a + b \,:\, a \in A,\; b \in B \,\}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class=&quot;katex-html&quot; aria-hidden=&quot;true&quot;&gt;&lt;span class=&quot;base&quot;&gt;&lt;span class=&quot;strut&quot; style=&quot;height:0.7667em;vertical-align:-0.0833em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2222em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mbin&quot;&gt;⊕&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2222em&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;base&quot;&gt;&lt;span class=&quot;strut&quot; style=&quot;height:0.6833em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:0.0502em&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2778em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2778em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mrel&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2778em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2778em&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;base&quot;&gt;&lt;span class=&quot;strut&quot; style=&quot;height:1em;vertical-align:-0.25em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mopen&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.1667em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2222em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mbin&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2222em&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;base&quot;&gt;&lt;span class=&quot;strut&quot; style=&quot;height:0.6944em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.1667em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2778em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mrel&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.1667em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2778em&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;base&quot;&gt;&lt;span class=&quot;strut&quot; style=&quot;height:0.5782em;vertical-align:-0.0391em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2778em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mrel&quot;&gt;∈&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2778em&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;base&quot;&gt;&lt;span class=&quot;strut&quot; style=&quot;height:0.8889em;vertical-align:-0.1944em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;mpunct&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2778em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.1667em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2778em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mrel&quot;&gt;∈&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2778em&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;base&quot;&gt;&lt;span class=&quot;strut&quot; style=&quot;height:1em;vertical-align:-0.25em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:0.0502em&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.1667em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mclose&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;p&gt;几何直觉更好记：&lt;strong&gt;把 B 的原点沿着 A 的每一个点” 搬一遍”，所有扫过的区域的并集，就是 A⊕B&lt;/strong&gt;。&lt;/p&gt;
&lt;figure class=&quot;figure&quot; data-astro-cid-bj3fsypb&gt; &lt;img src=&quot;https://khalilgao.com/_astro/fig1-mtv._O_tRjmo_Z2b1Bf8.svg&quot; alt=&quot;两个多边形的 Minkowski 和：B 沿着 A 的边界滑动时扫出的包络就是 A ⊕ B&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-cid-bj3fsypb width=&quot;680&quot; height=&quot;298&quot;&gt; &lt;figcaption data-astro-cid-bj3fsypb&gt;图 1. Minkowski 和的几何构造。淡蓝色虚线是 A 的原形，橙色虚线是 B 在不同位置的副本，绿色轮廓是它们的和。&lt;/figcaption&gt; &lt;/figure&gt; 
&lt;p&gt;对凸多边形，A⊕B 的每一条边要么来自 A，要么来自 B，按逆时针角度归并即可&lt;sup&gt;&lt;a href=&quot;#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref=&quot;&quot; aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;。实现起来是一个 &lt;em&gt;O(n + m)&lt;/em&gt; 的边扫描：&lt;/p&gt;
&lt;figure class=&quot;code-block&quot; data-filename=&quot;minkowski-sum.cpp&quot;&gt;&lt;button type=&quot;button&quot; class=&quot;code-copy-btn&quot; aria-label=&quot;复制代码&quot; data-code-copy=&quot;&quot;&gt;&lt;svg viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.6&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;rect x=&quot;9&quot; y=&quot;9&quot; width=&quot;12&quot; height=&quot;12&quot; rx=&quot;2&quot;&gt;&lt;/rect&gt;&lt;path d=&quot;M5 15H3a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v2&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/button&gt;&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-dimmed&quot; style=&quot;background-color:#fff;--shiki-dark-bg:#22272e;color:#24292e;--shiki-dark:#adbac7;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;cpp&quot; data-filename=&quot;minkowski-sum.cpp&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#768390&quot;&gt;// A, B 以逆时针凸多边形表示&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#F69D50&quot;&gt;Polygon&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#DCBDFB&quot;&gt; minkowskiSum&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#F69D50&quot;&gt; Polygon&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&quot;color:#E36209;--shiki-dark:#F69D50&quot;&gt; A&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#F69D50&quot;&gt; Polygon&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&quot;color:#E36209;--shiki-dark:#F69D50&quot;&gt; B&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#768390&quot;&gt;  // 1. 找 A、B 各自 y 最小（相同取 x 最小）的顶点作为起点&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;  int&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; i &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#DCBDFB&quot;&gt; leftmostBottom&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;(A), j &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#DCBDFB&quot;&gt; leftmostBottom&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;(B);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt; int&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; n &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; A.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#DCBDFB&quot;&gt;size&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;(), m &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; B.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#DCBDFB&quot;&gt;size&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;  Polygon C;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;  C.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#DCBDFB&quot;&gt;reserve&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;(n &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; m);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;  int&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; a &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#6CB6FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;, b &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#6CB6FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;  while&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; (a &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; n &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;||&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; b &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; m) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#768390&quot;&gt;    // 当前两条待合并的边&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;    Vec2 ea &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; A[(i &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; a &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#6CB6FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;%&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; n] &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; A[(i &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; a) &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;%&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; n];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;    Vec2 eb &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; B[(j &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; b &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#6CB6FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;%&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; m] &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; B[(j &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; b) &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;%&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; m];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;    C.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#DCBDFB&quot;&gt;push_back&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;(A[(i &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; a) &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;%&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; n] &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; B[(j &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; b) &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;%&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; m]);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;    double&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; cross &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; ea.x &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; eb.y &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; ea.y &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; eb.x;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;      (cross &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#6CB6FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;) { &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;a; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;    else&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt; if&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; (cross &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#6CB6FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;) { &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;b; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;    else&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;                { &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;a; &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;b; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; C;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;凹多边形要先用 Bayazit 做凸分解，各对凸子多边形求 Minkowski 和，再做并集。实践里 30–40 顶点的凹件，分解后 &lt;em&gt;k ≤ 6&lt;/em&gt; 个子件，后续求和 &lt;em&gt;k²&lt;/em&gt; 对 —— 这个预处理是” 一次性成本”。&lt;/p&gt;
&lt;h2 id=&quot;从-minkowski-差推导-nfp&quot;&gt;从 Minkowski 差推导 NFP&lt;/h2&gt;
&lt;p&gt;NFP（&lt;em&gt;No-Fit Polygon&lt;/em&gt;）的定义是：&lt;strong&gt;B 贴着 A 的边界滑一圈时，B 的参考点（通常取质心）扫过的轨迹&lt;/strong&gt;。翻译成集合运算：&lt;/p&gt;
&lt;span class=&quot;katex-display&quot;&gt;&lt;span class=&quot;katex&quot;&gt;&lt;span class=&quot;katex-mathml&quot;&gt;&lt;math xmlns=&quot;http://www.w3.org/1998/Math/MathML&quot; display=&quot;block&quot;&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mrow&gt;&lt;mi mathvariant=&quot;normal&quot;&gt;N&lt;/mi&gt;&lt;mi mathvariant=&quot;normal&quot;&gt;F&lt;/mi&gt;&lt;mi mathvariant=&quot;normal&quot;&gt;P&lt;/mi&gt;&lt;/mrow&gt;&lt;mo stretchy=&quot;false&quot;&gt;(&lt;/mo&gt;&lt;mi&gt;A&lt;/mi&gt;&lt;mo separator=&quot;true&quot;&gt;,&lt;/mo&gt;&lt;mi&gt;B&lt;/mi&gt;&lt;mo stretchy=&quot;false&quot;&gt;)&lt;/mo&gt;&lt;mtext&gt;  &lt;/mtext&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mtext&gt;  &lt;/mtext&gt;&lt;mi&gt;A&lt;/mi&gt;&lt;mo&gt;⊕&lt;/mo&gt;&lt;mo stretchy=&quot;false&quot;&gt;(&lt;/mo&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;mi&gt;B&lt;/mi&gt;&lt;mo stretchy=&quot;false&quot;&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;annotation encoding=&quot;application/x-tex&quot;&gt;\mathrm{NFP}(A, B) \;=\; A \oplus (-B)&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class=&quot;katex-html&quot; aria-hidden=&quot;true&quot;&gt;&lt;span class=&quot;base&quot;&gt;&lt;span class=&quot;strut&quot; style=&quot;height:1em;vertical-align:-0.25em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord&quot;&gt;&lt;span class=&quot;mord mathrm&quot;&gt;NFP&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;mopen&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;mpunct&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.1667em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:0.0502em&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;mclose&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2778em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2778em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mrel&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2778em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2778em&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;base&quot;&gt;&lt;span class=&quot;strut&quot; style=&quot;height:0.7667em;vertical-align:-0.0833em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2222em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mbin&quot;&gt;⊕&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2222em&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;base&quot;&gt;&lt;span class=&quot;strut&quot; style=&quot;height:1em;vertical-align:-0.25em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mopen&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mord&quot;&gt;−&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:0.0502em&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;mclose&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;p&gt;其中 &lt;code&gt;-B&lt;/code&gt; 等于 &lt;code&gt;{ -b ∣ b ∈ B }&lt;/code&gt;，也就是 B 关于原点的反射。这个结论不需要新算法 ——Minkowski 和那套代码直接复用，把 B 先取反就行。&lt;/p&gt;
&lt;p&gt;一旦 NFP 预计算好，碰撞检测退化成 &lt;strong&gt;“offset 是否落在 NFP 的内部”&lt;/strong&gt; 的点在多边形判定 ——&lt;em&gt;O(log n)&lt;/em&gt; via 扇形二分，实测 30ns。&lt;/p&gt;
&lt;pre class=&quot;mermaid&quot; data-graph=&quot;graph LR
  A[读入 n 个多边形] --&gt; B[凸分解 + 预计算 NFP_ij]
  B --&gt; C{贪心放置}
  C --&gt;|查询 offset 是否在 NFP 外| D[布局合法 → 接受]
  C --&gt;|否则| E[下一候选位置]
  D --&gt; F[继续放剩余件]
  E --&gt; C&quot;&gt;graph LR
  A[读入 n 个多边形] --&amp;gt; B[凸分解 + 预计算 NFP_ij]
  B --&amp;gt; C{贪心放置}
  C --&amp;gt;|查询 offset 是否在 NFP 外| D[布局合法 → 接受]
  C --&amp;gt;|否则| E[下一候选位置]
  D --&amp;gt; F[继续放剩余件]
  E --&amp;gt; C&lt;/pre&gt;
&lt;p&gt;预计算 NFP 的成本是 &lt;em&gt;O(n² · k² · s)&lt;/em&gt;（s 是子件顶点数，常数 ≤ 40）。在 n = 120 的数据下大概 300ms，一次就行。&lt;/p&gt;
&lt;h2 id=&quot;效果卡点没了性能才上来&quot;&gt;效果：卡点没了，性能才上来&lt;/h2&gt;
&lt;figure class=&quot;figure wide wide-figure&quot; data-astro-cid-bj3fsypb&gt; &lt;img src=&quot;https://khalilgao.com/_astro/fig2-grid-layout.CA_wCapP_20lHJm.svg&quot; alt=&quot;朴素布局与 NFP 引导布局的视觉对比&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-cid-bj3fsypb width=&quot;960&quot; height=&quot;360&quot;&gt; &lt;figcaption data-astro-cid-bj3fsypb&gt;图 2. 左：没有 NFP 时贪心留下的大量锯齿状空隙；右：NFP 提供的 touching 约束下，形状自然嵌合。同一组输入，容器利用率 73.8% → 90.4%。&lt;/figcaption&gt; &lt;/figure&gt; 
&lt;p&gt;把整个 pipeline 接上去之后，热点从” 段段相交” 变成了” 点在多边形二分”，分数从 295K 一路跳到 904K。下面是我自己 bench 的一组数据（1M 随机查询，单线程，M1 Pro）：&lt;/p&gt;





























&lt;div class=&quot;table-wrap&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方案&lt;/th&gt;&lt;th&gt;预处理&lt;/th&gt;&lt;th&gt;单次查询&lt;/th&gt;&lt;th&gt;1M 查询总耗时&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;朴素段段相交&lt;/td&gt;&lt;td&gt;无&lt;/td&gt;&lt;td&gt;4.2 µs&lt;/td&gt;&lt;td&gt;4.20 s&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Bounding-box 剪枝 + 段段相交&lt;/td&gt;&lt;td&gt;40 ms&lt;/td&gt;&lt;td&gt;0.9 µs&lt;/td&gt;&lt;td&gt;0.90 s&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;NFP 预计算 + 点在多边形二分&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;310 ms&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;34 ns&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;0.034 s&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;124×&lt;/code&gt; 的加速，全部来自” 把碰撞检测从几何问题搬到空间索引问题”。工程上需要注意的地方我用脚注记下来&lt;sup&gt;&lt;a href=&quot;#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref=&quot;&quot; aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;
&lt;h3 id=&quot;实现里最容易踩的三个坑&quot;&gt;实现里最容易踩的三个坑&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;凹多边形的 Minkowski 和 ≠ 凸分解后求和的并集&lt;/strong&gt;。子件之间会产生” 假交集”，需要用 CGAL 的 &lt;code&gt;boolean union&lt;/code&gt; 或 Vatti 算法再过一遍。这是我 debug 了一整晚才发现的，代价约 5ms，不能跳。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数值精度&lt;/strong&gt;。&lt;code&gt;cross == 0&lt;/code&gt; 的判断要带 epsilon，否则共线边会漂出去。推荐用整数坐标 × 2^20 放大后做纯整数几何。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预计算阶段要做成并行 future&lt;/strong&gt;。n = 120 时 300ms 串行预处理看似不贵，但在提交阶段 × 100 次实验就是半小时的墙上时间，一次 tokio/thread_pool 就省下来了。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;小结&quot;&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;A⊕B 等于” 把 B 沿 A 边界扫过一圈” 的包络。凸时 &lt;em&gt;O(n+m)&lt;/em&gt; 可合并，凹时走凸分解。&lt;/li&gt;
&lt;li&gt;NFP (A, B) 等于 A 和 -B 的 Minkowski 和。一个几何变换，把碰撞判定化为点在多边形判定。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实战最重要的一环&lt;/strong&gt;：不要等到性能瓶颈显现才换算法。写完朴素版后就该问” 有没有数学结构能帮我预计算”。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;“The geometry of nesting problems is, at heart, a geometry of
Minkowski sums. Everything else is indexing.” — Bennell &amp;amp; Oliveira, 2008&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr/&gt;
&lt;section data-footnotes=&quot;&quot; class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;教科书证明见 &lt;em&gt;Computational Geometry: Algorithms and Applications&lt;/em&gt; 第 13.2 节。直觉上，A⊕B 的每一条边对应”B 靠在 A 某条边上时的一段滑动轨迹”，所以按角度排序即可无冲突归并。 &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;NFP 预计算内存是 &lt;em&gt;O(n² · s)&lt;/em&gt;。n = 120、s ≈ 30 时大约 45 MB，可以全进 cache。一旦上 n = 500，就得考虑延迟计算 + LRU cache（我们没碰到，但 2020 年 ESICUP 赛题会）。 &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><category>c++</category><category>competition</category><category>computational-geometry</category><category>algorithms</category></item></channel></rss>