<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>zaochen</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>http://ttzc.github.io/</id>
  <link href="http://ttzc.github.io/" rel="alternate"/>
  <link href="http://ttzc.github.io/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, zaochen</rights>
  <title>zaochen's blog</title>
  <updated>2026-08-08T11:56:00.000Z</updated>
  <entry>
    <author>
      <name>zaochen</name>
    </author>
    <category term="AI" scheme="http://ttzc.github.io/categories/AI/"/>
    <category term="Agent" scheme="http://ttzc.github.io/tags/Agent/"/>
    <category term="Skill" scheme="http://ttzc.github.io/tags/Skill/"/>
    <category term="Obsidian" scheme="http://ttzc.github.io/tags/Obsidian/"/>
    <category term="CRM" scheme="http://ttzc.github.io/tags/CRM/"/>
    <category term="WorkBuddy" scheme="http://ttzc.github.io/tags/WorkBuddy/"/>
    <content>
      <![CDATA[<h2 id="背景"><a class="markdownIt-Anchor" href="#背景"></a> 背景</h2><p>凌晨，突然意识到一个问题：认识的所有 OI 圈朋友、初中高中同学，他们的信息全散落在各个软件里。大部分人不记得手机号或者没有加 QQ 微信，甚至不知道真名。社交软件的备注只能写几个字，塞不下这个人和我关系多好，在什么关键时刻能帮我什么。</p><p>更关键的是，AI 读不到这些信息。当对话中提到「ICPC 打星」时，它不会自动想起某位教练可以帮我争取的名额；提到「北京面基」时，它不知道我有三个朋友在北京。</p><p>我需要一个 AI 可读的通讯录。不是存电话号的那种——是存「关系网络」的那种。</p><hr /><h2 id="开发过程"><a class="markdownIt-Anchor" href="#开发过程"></a> 开发过程</h2><h3 id="1-数据模型设计"><a class="markdownIt-Anchor" href="#1-数据模型设计"></a> 1. 数据模型设计</h3><p>让 AI 完成了数据模型设计，核心字段围绕一个目的：让 AI 认识我的朋友，并在有实际需求的时候想到对应的人或许可以帮忙。</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">nickname:</span> <span class="string">&quot;&quot;</span>       <span class="comment"># 怎么称呼（主键）</span></span><br><span class="line"><span class="attr">platforms:</span>         <span class="comment"># QQ / 微信 / B站 / 洛谷 / CF</span></span><br><span class="line"><span class="attr">identity:</span> <span class="string">&quot;&quot;</span>       <span class="comment"># 学校 / 年级 / 专业 / 角色</span></span><br><span class="line"><span class="attr">context:</span> <span class="string">&quot;&quot;</span>        <span class="comment"># 怎么认识的</span></span><br><span class="line"><span class="attr">strength:</span> <span class="string">&quot;&quot;</span>       <span class="comment"># 关系强度</span></span><br><span class="line"><span class="attr">help_areas:</span> []     <span class="comment"># 可帮事项（AI 联想核心）</span></span><br><span class="line"><span class="attr">trigger_tags:</span> []   <span class="comment"># 触发标签（wiki-link 格式）</span></span><br><span class="line"><span class="attr">notes:</span> <span class="string">&quot;&quot;</span>          <span class="comment"># 关系边界、注意事项</span></span><br></pre></td></tr></table></figure><p>和传统通讯录的区别：传统通讯录只存「姓名 + 电话」，这里每个联系人是一份结构化的社交画像，标签是机器可解析的触发词。</p><h3 id="2-独立-obsidian-vault-架构"><a class="markdownIt-Anchor" href="#2-独立-obsidian-vault-架构"></a> 2. 独立 Obsidian Vault 架构</h3><p>决定把 vault 放在独立目录，与博客完全隔离。原因很实际：联系人数据含平台 ID、个人背景等敏感信息，混在博客里万一误发出去就麻烦了。</p><p>目录结构经过两次迭代：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">~/people/</span><br><span class="line">├── README.md          # 使用约定与设计决策</span><br><span class="line">├── INDEX.md           # AI 优先读的速查索引</span><br><span class="line">├── .trash/            # 安全归档（不用 rm）</span><br><span class="line">├── contacts/          # 每人一个 .md</span><br><span class="line">├── templates/</span><br><span class="line">│   └── contact.md</span><br><span class="line">└── tags/</span><br><span class="line">    ├── school/        # 学校标签</span><br><span class="line">    ├── field/         # 专业方向标签</span><br><span class="line">    └── city/          # 城市标签</span><br></pre></td></tr></table></figure><p><a href="http://INDEX.md">INDEX.md</a> 是 AI 的入口——里面有联系人总览表 + 三层标签导航表，AI 匹配时先查这里。</p><h3 id="3-联系人批量录入"><a class="markdownIt-Anchor" href="#3-联系人批量录入"></a> 3. 联系人批量录入</h3><p>逐条从聊天记录中提取信息，一口气录入了一批联系人，覆盖竞赛圈前辈、学弟、同学、老师等多个圈层。</p><p>录入过程中有一个原则被明确下来：<strong>主要录入「关系不深但潜在高价值」的人</strong>。很熟的人都在脑袋里，知识库的核心价值是 AI 帮你记住那些半熟人。对于关系边界明确的人，备注里直接写「只能问简单问题，不能欠人情」。</p><h3 id="4-标签体系的三次重构"><a class="markdownIt-Anchor" href="#4-标签体系的三次重构"></a> 4. 标签体系的三次重构</h3><p>这是整个构建过程中迭代最多次的部分。</p><p><strong>第一版：组合标签。</strong> 把学校和专业方向拼在一起，如「A 大学 数学」「B 大学 计算机」。建完后跑 Obsidian 图谱，发现标签之间全连成了大杂烩——两个标签因为共享一个联系人就被强制关联，两个数学专业的朋友反而没有共同标签，图谱毫无结构美感。</p><p><strong>第二版：两层原子化。</strong> 拆成 <code>tags/school/</code> 和 <code>tags/field/</code>，不要组合。旧文件移入 <code>.trash/</code>。</p><p><strong>第三版：加城市层。</strong> 另开 <code>tags/city/</code>。最终三层体系：</p><table><thead><tr><th>层</th><th>目录</th><th>边规则</th></tr></thead><tbody><tr><td>学校</td><td>tags/school/</td><td>学校 ↔ 城市（双向结构边）</td></tr><tr><td>方向</td><td>tags/field/</td><td>同体系可连（如 OI ↔ ICPC）</td></tr><tr><td>城市</td><td>tags/city/</td><td>同区域相邻城市可连</td></tr></tbody></table><p><strong>图谱净化是关键一步。</strong> 两个标签仅因共享联系人而相关的边是「无用边」，应该删除。比如「兰州大学」和「大学生数学竞赛 CMC」不该直接连——走 <a href="https://mp.weixin.qq.com/s/4QFloR5b0O3kZOe40rW5Bg">刘适禛</a> 作为桥梁才对。脚本跑完后删了大量冗余边，最终结构边只保留学校↔城市、同体系方向、相邻城市，图谱从一团乱麻变成了清晰的层级结构。</p><h3 id="5-图谱审计脚本audit_graphpy"><a class="markdownIt-Anchor" href="#5-图谱审计脚本audit_graphpy"></a> 5. 图谱审计脚本（audit_graph.py）</h3><p>写了一个 Python 脚本来做自动化审计，因为手动检查数百条 wiki-link 太容易出错，让 Agent 查也是这样。</p><p>脚本做了五件事：</p><ul><li><strong>Wiki-link 解析</strong>：正则匹配 <code>[[目标|别名]]</code>、<code>[[目标#锚点]]</code> 等四种格式</li><li><strong>断链检测</strong>：所有 <code>[[...]]</code> 目标是否都有对应文件</li><li><strong>孤立节点发现</strong>：没有标签的联系人 / 没有联系人的标签</li><li><strong>冗余边识别</strong>：标签对仅通过联系人相连的，标记为冗余候选</li><li><strong>圈层分析</strong>：每个联系人输出 <code>学校层 + 方向层 + 城市层</code> 的综合画像</li></ul><h3 id="6-从个人工具到可发布的-skill"><a class="markdownIt-Anchor" href="#6-从个人工具到可发布的-skill"></a> 6. 从个人工具到可发布的 Skill</h3><p>录完联系人后，回头看这个过程——从零建 vault、设计标签体系、图谱净化、写审计脚本——是一套完整的、可复用的方法论。于是做了三件事：</p><p><strong>脱敏</strong>：把所有个人标识替换为通用占位符，只留结构和流程。</p><p><strong>打包为 Skill</strong>：核心方法论文档（<a href="http://SKILL.md">SKILL.md</a>）+ 两个模板资产（联系人模板、索引模板）+ 虚构人物的完整 onboarding 示例 + 审计脚本。通过官方打包脚本验证。</p><p><strong>安全审计发布</strong>：推送 GitHub 后，用安全审查工具做了完整审计。结论 Benign——无远程下载、无自动执行、无凭证泄露。顺便试用了一下 WorkBuddy 的智能体邮箱功能，直接把打包好的 zip 作为附件发给了一个初中同学——从打包到发送全程不用离开对话界面，比手动开网页邮箱快多了。</p><p>已发布到 SkillHub 市场：<a href="https://skillhub.cn/skills/user_e267c873/relationship-graph">relationship-graph</a></p><h3 id="7-最终-skill-结构"><a class="markdownIt-Anchor" href="#7-最终-skill-结构"></a> 7. 最终 Skill 结构</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">relationship-graph/</span><br><span class="line">├── CLAUDE.md                      # Claude Code 工作指南</span><br><span class="line">├── README.md                      # 快速开始</span><br><span class="line">├── SKILL.md                       # 核心方法论（设计原则+数据模型+工作流+审计）</span><br><span class="line">├── assets/</span><br><span class="line">│   ├── contact-template.md        # 联系人录入模板</span><br><span class="line">│   ├── INDEX-template.md          # 索引导航骨架</span><br><span class="line">│   └── onboarding-example.md      # 虚构人物完整 walkthrough</span><br><span class="line">└── scripts/</span><br><span class="line">    └── audit_graph.py             # 图谱完整性审计脚本</span><br></pre></td></tr></table></figure><hr /><h2 id="关键决策"><a class="markdownIt-Anchor" href="#关键决策"></a> 关键决策</h2><ol><li><strong>独立 vault，不混博客</strong>：联系人含敏感信息，隔离是底线</li><li><strong>半熟人优先录入</strong>：很熟的人在脑子里，AI 帮我们记住的是「目前关系不深但潜在高价值」的人</li><li><strong>三层原子标签 + 联系人桥接</strong>：标签不搞组合，不同圈层的连接通过联系人节点自然形成</li><li><strong>图谱净化：人是桥，标签不过度互联</strong>：删除仅共享联系人而存在的标签边，让图谱从大杂烩变成有结构的网络</li><li><strong>冲突先问，不猜</strong>：疑似重复、信息矛盾时不自动合并/覆写，先展示给用户确认</li><li><strong>审计脚本不是摆设</strong>：数百条 wiki-link 的手动审计不可靠，一个脚本跑完比肉眼准</li></ol><hr /><h2 id="下一步"><a class="markdownIt-Anchor" href="#下一步"></a> 下一步</h2><ul><li>收集实际使用反馈：标签粒度够不够？INDEX 表在大规模时怎么维护？</li><li>规模边界分析：一百个联系人时 <a href="http://INDEX.md">INDEX.md</a> 还读得动吗？需要上数据库吗？</li></ul><hr /><h2 id="资源链接"><a class="markdownIt-Anchor" href="#资源链接"></a> 资源链接</h2><p>如果你也想建立自己的关系图谱，可以直接在SkillHub安装这个Skill，按模板录入即可。当然，这个项目在数据组织等方面还存在很多可以改进的地方，欢迎开发者朋友在 GitHub 进行贡献。</p><ul><li>GitHub：<a href="https://github.com/ttzc/relationship-graph">github.com/ttzc/relationship-graph</a></li><li>SkillHub：<a href="https://skillhub.cn/skills/user_e267c873/relationship-graph">skillhub.cn/skills/user_e267c873/relationship-graph</a></li></ul>]]>
    </content>
    <id>http://ttzc.github.io/a1377ca2/</id>
    <link href="http://ttzc.github.io/a1377ca2/"/>
    <published>2026-08-08T11:56:00.000Z</published>
    <summary>记录从凌晨灵感出发，设计并发布一个 AI 可读的人脉图谱 Skill 的全过程：数据模型、独立 Obsidian Vault 架构、三层原子标签体系、图谱净化与审计脚本，以及打包发布到 SkillHub 的关键决策与资源链接。</summary>
    <title>从凌晨灵感到发布：一个 AI 可读的人脉图谱 Skill 开发全记录</title>
    <updated>2026-08-08T11:56:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>zaochen</name>
    </author>
    <category term="题解" scheme="http://ttzc.github.io/categories/solution/"/>
    <category term="LIS" scheme="http://ttzc.github.io/tags/LIS/"/>
    <category term="贪心" scheme="http://ttzc.github.io/tags/%E8%B4%AA%E5%BF%83/"/>
    <category term="AtCoder" scheme="http://ttzc.github.io/tags/AtCoder/"/>
    <content>
      <![CDATA[<h2 id="1-题目数据-problem-metadata"><a class="markdownIt-Anchor" href="#1-题目数据-problem-metadata"></a> 1. 题目数据 (Problem Metadata)</h2><ul><li><strong>题目类型</strong>：传统题</li><li><strong>题目链接</strong>：<a href="https://atcoder.jp/contests/abc468/tasks/abc468_f">F - Chmax - AtCoder Beginner Contest 468</a></li><li><strong>时间限制</strong>：2 秒</li><li><strong>内存限制</strong>：1024 MB</li></ul><h2 id="2-题意简述-problem-summary"><a class="markdownIt-Anchor" href="#2-题意简述-problem-summary"></a> 2. 题意简述 (Problem Summary)</h2><p>给定正整数 $N$（$1\le N\le5\times10^5$）与一个 $1\sim N$ 的排列 $P=(P_1,P_2,\dots,P_N)$。维护变量 $x,y$ 与计数器 $c$，初始均为 $0$。依次处理 $k=1,\dots,N$，每步把 $P_k$ 分给 $x$ 或 $y$ 之一，若该变量当前值小于 $P_k$ 则 $c$ 加 $1$，再令该变量更新为 $\max(\cdot,P_k)$。求最终 $c$ 的最大可能值。</p><h2 id="3-朴素解法-brute-force"><a class="markdownIt-Anchor" href="#3-朴素解法-brute-force"></a> 3. 朴素解法 (Brute-Force)</h2><p>枚举每个元素分给 $x$ 还是 $y$，共 $2^N$ 种方案。$N=5\times10^5$ 时不可能枚举完，且决策之间相互影响（先到的大元素会&quot;堵住&quot;后续小元素），无法简单剪枝。需要先找到结构的本质。</p><h2 id="4-核心解法-main-solution"><a class="markdownIt-Anchor" href="#4-核心解法-main-solution"></a> 4. 核心解法 (Main Solution)</h2><h3 id="特殊性质"><a class="markdownIt-Anchor" href="#特殊性质"></a> 特殊性质</h3><p>先观察两个事实：</p><ul><li><strong>前缀最大值必贡献</strong>。若 $P_i$ 大于此前所有元素，则时刻 $i$ 时 $x,y$ 的当前值都小于 $P_i$，无论分给谁，$c$ 必加 $1$。</li><li><strong>同一变量内的贡献位置值严格上升</strong>。某位置贡献后，该变量变为 $P_i$，下一个贡献位置的值必须更大。</li></ul><h3 id="关键突破"><a class="markdownIt-Anchor" href="#关键突破"></a> 关键突破</h3><p>把贡献位置分成两类：前缀最大值（必贡献）与非前缀最大值。可以证明：<strong>非前缀最大值的贡献位置按时间序，其值必然严格上升</strong>，于是它们构成&quot;删除全部前缀最大值后的序列 $R$“的一个上升子序列，数量至多为 $\mathrm{LIS}®$。反过来，构造一组方案恰好达到&quot;前缀最大值个数 $p$ 加上 $\mathrm{LIS}®$”。由此得出答案</p><p>$$\text{ans}=p+\mathrm{LIS}®$$</p><h3 id="推导过程"><a class="markdownIt-Anchor" href="#推导过程"></a> 推导过程</h3><p>删除所有前缀最大值，剩下按原顺序排成 $R$，任取 $R$ 的一条最长上升子序列 $S$。构造如下分配方案：</p><ul><li>所有前缀最大值分给 $x$。由引理 1，它们恰好贡献 $p$ 次；</li><li>$S$ 中的元素分给 $y$。$S$ 严格上升，$y$ 的当前值恒等于上一个 $S$ 元素，故 $S$ 中每个元素都贡献，共 $|S|=\mathrm{LIS}®$ 次；</li><li>$R$ 中其余元素分给 $x$。时刻 $k$ 时 $x$ 的当前值等于此前前缀最大值中的最大者，即 $\max_{j&lt;k}P_j$；而 $P_k$ 非前缀最大值意味着 $\max_{j&lt;k}P_j&gt;P_k$，故它们不产生贡献。</li></ul><p>于是该方案总贡献恰为 $p+\mathrm{LIS}®$，达到上界。</p><h2 id="5-正确性证明-proof-of-correctness"><a class="markdownIt-Anchor" href="#5-正确性证明-proof-of-correctness"></a> 5. 正确性证明 (Proof of Correctness)</h2><p><strong>引理 1（前缀最大值必贡献）</strong>：设所有前缀最大值构成的集合为$\mathrm{PM}$，对 $i\in\mathrm{PM}$，$P_i$ 大于所有 $j&lt;i$ 的 $P_j$，即 $\mathrm{PM}={i|\forall j &lt; i,P_j&lt;P_i}$，而 $x,y$ 在时刻 $i$ 前的值只能来自某个 $P_j$（$j&lt;i$）或 $0$，故均小于 $P_i$。无论分给谁，该变量当前值小于 $P_i$，$c$ 必加 $1$。</p><p><strong>引理 2（非前缀最大值的贡献位置值严格上升）</strong>：设 $i&lt;j$ 均为非前缀最大值的贡献位置，且 $i$ 分给 $x$、$j$ 分给 $y$（同变量时由基本事实直接得 $P_i&lt;P_j$）。反设 $P_i&gt;P_j$。因 $i\notin\mathrm{PM}$，存在 $k&lt;i$ 使 $P_k&gt;P_i$。若 $k$ 分给 $x$，则 $x$ 在时刻 $i$ 前已不小于 $P_k&gt;P_i$，$i$ 不贡献，矛盾；故 $k$ 必分给 $y$，但 $k&lt;j$，于是 $y$ 在时刻 $j$ 前已不小于 $P_k&gt;P_i&gt;P_j$，$j$ 不贡献，矛盾。所以 $P_i&lt;P_j$。</p><p><strong>上界</strong>：任意策略的贡献 $=|\mathrm{PM}|+|\text{非 }\mathrm{PM}\text{ 贡献}|\le p+\mathrm{LIS}®$。由引理 2，非前缀最大值的贡献位置构成 $R$ 的上升子序列，其长度不超过 $\mathrm{LIS}®$。</p><p><strong>构造可达</strong>：§4 已给出达到 $p+\mathrm{LIS}®$ 的方案。综上所述，$\text{ans}=p+\mathrm{LIS}®$，贪心策略正确。</p><p>通俗地，前缀最大值是&quot;怎么分都躲不掉&quot;的贡献，必须全部计入；剩下的元素里，只有能形成一条严格上升链的那些才有机会额外贡献，而两条变量链不可能同时占便宜——跨链的下降会被某一边的前驱大元素堵死，这正是引理 2 反证抓住的本质。</p><h2 id="6-复杂度分析-complexity"><a class="markdownIt-Anchor" href="#6-复杂度分析-complexity"></a> 6. 复杂度分析 (Complexity)</h2><ul><li><strong>时间复杂度</strong>：扫描一遍统计前缀最大值并构造 $R$，$O(N)$；对 $R$ 求 $\mathrm{LIS}$ 用二分优化，$O(N\log N)$。总复杂度 $O(N\log N)$，$N=5\times10^5$ 可轻松通过。</li><li><strong>空间复杂度</strong>：$O(N)$。全局数组存 $P$，$R$ 与 LIS 辅助数组长度不超过 $N$，远低于内存限制。</li></ul><h2 id="7-实现细节与避坑指南-implementation-details"><a class="markdownIt-Anchor" href="#7-实现细节与避坑指南-implementation-details"></a> 7. 实现细节与避坑指南 (Implementation Details)</h2><ul><li><strong>$R$ 的顺序</strong>：非前缀最大值的元素要按原顺序压入 $R$，LIS 依赖子序列的相对顺序，打乱顺序会错。</li><li><strong>二分细节</strong>：$P$ 是排列（元素互异），严格上升 LIS 用 $\texttt{lower_bound}$ 与 $\texttt{upper_bound}$ 等价；若 $P$ 允许重复值，则求非降子序列需要用 $\texttt{upper_bound}$。</li><li><strong>边界情况</strong>：$N=1$ 时 $P_1$ 是前缀最大值，答案 $1$；全递增序列所有元素都是前缀最大值，答案 $N$；全递减序列答案恰为 $2$（首元素加任意一个剩余元素）。</li><li><strong>整数范围</strong>：答案不超过 $N\le5\times10^5$，$32$ 位整数足够，无需 $\texttt{long long}$。</li></ul><table><thead><tr><th style="text-align:left">坑点</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left">$R$ 顺序</td><td style="text-align:left">必须按原序列顺序收集，否则 LIS 结果无意义</td></tr><tr><td style="text-align:left">答案上限</td><td style="text-align:left">每个位置至多贡献一次，答案 $\le N$，不会溢出</td></tr></tbody></table><h2 id="8-参考代码-reference-code"><a class="markdownIt-Anchor" href="#8-参考代码-reference-code"></a> 8. 参考代码 (Reference Code)</h2><p>下列代码按上述前缀最大值 + LIS 的思路实现，已经通过 AtCoder 评测。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;bits/stdc++.h&gt;</span></span></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std;</span><br><span class="line"></span><br><span class="line"><span class="type">const</span> <span class="type">int</span> N = <span class="number">5e5</span> + <span class="number">5</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> n, p[N];</span><br><span class="line"></span><br><span class="line"><span class="comment">// 求序列 a 的最长严格上升子序列长度；tails[k] 表示长度为 k+1 的 LIS 的最小末尾值</span></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">lis</span><span class="params">(<span class="type">const</span> vector&lt;<span class="type">int</span>&gt; &amp;a)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    vector&lt;<span class="type">int</span>&gt; tails;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> val : a)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">auto</span> it = <span class="built_in">lower_bound</span>(tails.<span class="built_in">begin</span>(), tails.<span class="built_in">end</span>(), val);</span><br><span class="line">        <span class="keyword">if</span> (it == tails.<span class="built_in">end</span>())</span><br><span class="line">            tails.<span class="built_in">push_back</span>(val);</span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">            *it = val;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> tails.<span class="built_in">size</span>();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    ios::<span class="built_in">sync_with_stdio</span>(<span class="literal">false</span>);</span><br><span class="line">    cin.<span class="built_in">tie</span>(<span class="literal">nullptr</span>);</span><br><span class="line"><span class="meta">#<span class="keyword">ifdef</span> DEBUG</span></span><br><span class="line">    <span class="type">clock_t</span> t0 = <span class="built_in">clock</span>();</span><br><span class="line">    <span class="built_in">freopen</span>(<span class="string">&quot;data.in&quot;</span>, <span class="string">&quot;r&quot;</span>, stdin);</span><br><span class="line">    <span class="built_in">freopen</span>(<span class="string">&quot;data.out&quot;</span>, <span class="string">&quot;w&quot;</span>, stdout);</span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// Don&#x27;t stop. Don&#x27;t hide. Follow the light, and you&#x27;ll find tomorrow.</span></span><br><span class="line"></span><br><span class="line">    cin &gt;&gt; n;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= n; i++)</span><br><span class="line">        cin &gt;&gt; p[i];</span><br><span class="line"></span><br><span class="line">    vector&lt;<span class="type">int</span>&gt; rest; <span class="comment">// 删除全部前缀最大值后剩下的序列，保持原顺序</span></span><br><span class="line">    <span class="type">int</span> ans = <span class="number">0</span>;      <span class="comment">// 先统计前缀最大值个数，它们无论怎么分配都必贡献</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>, maxv = <span class="number">0</span>; i &lt;= n; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (p[i] &gt; maxv) <span class="comment">// p[i] 是前缀最大值</span></span><br><span class="line">        &#123;</span><br><span class="line">            maxv = p[i];</span><br><span class="line">            ans++;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">else</span> <span class="comment">// 非前缀最大值，留给 LIS</span></span><br><span class="line">            rest.<span class="built_in">push_back</span>(p[i]);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    ans += <span class="built_in">lis</span>(rest); <span class="comment">// 剩余序列中最多还能贡献 LIS 长度次</span></span><br><span class="line"></span><br><span class="line">    cout &lt;&lt; ans &lt;&lt; endl;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">ifdef</span> DEBUG</span></span><br><span class="line">    cerr &lt;&lt; <span class="string">&quot;Time used:&quot;</span> &lt;&lt; <span class="built_in">clock</span>() - t0 &lt;&lt; <span class="string">&quot;ms&quot;</span> &lt;&lt; endl;</span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="9-补充说明-additional-notes"><a class="markdownIt-Anchor" href="#9-补充说明-additional-notes"></a> 9. 补充说明 (Additional Notes)</h2><ul><li>本题属于&quot;把排列划分为两条链、每条链的贡献等于其内部记录数（前缀最大/最小值）&quot;的问题族。知识关联较强的类似题：<ul><li><a href="https://www.luogu.com.cn/problem/P11106">P11106 [ROI 2023 Day 1] 峰值</a>：最接近的变种，同样把排列分成两个子序列，一条数峰值（前缀最大值）、一条数反峰值（前缀最小值），区别是&quot;最大+最小&quot;两条链而非&quot;最大+最大&quot;两条链，解法落到枚举断点 + LIS/LDS + 树状数组优化。</li><li><a href="https://codeforces.com/contest/1801/problem/C">Codeforces 1801C Music Festival</a>：多个序列拼成一个大序列，最大化前缀最大值个数，本质是把每个序列压缩成有效上升子序列再做 DP。</li><li><a href="https://www.luogu.com.cn/problem/P9307">P9307 [DTOI-5] 进行一个排的重</a>：对两个排列的二元组重排，同时最大化两条链的前缀最大值计数，最优解结构与按一维排序后的 LIS 相关。</li></ul></li><li>这一族题的核心都是&quot;记录只沿上升链产生，把贡献拆到链上后归约为 LIS/LDS 类结构&quot;，方法经典，具有学习的价值。</li><li>此 $O(N\log N)$ 算法是较为高效的 LIS 算法，也可以使用代码略复杂的树状数组优化 DP 求 LIS，<br />各种 LIS 方法对比详见 <a   href='/428bd345/#最长上升子序列（lis）'>【动态规划】线性 DP 学习笔记</a>。</li></ul>]]>
    </content>
    <id>http://ttzc.github.io/776526af/</id>
    <link href="http://ttzc.github.io/776526af/"/>
    <published>2026-08-03T11:59:00.000Z</published>
    <summary>将 1~N 的排列依次分配到两个变量上，最大化&quot;当前值小于新值&quot;的计数。核心结论：前缀最大值必贡献，剩余元素的最大贡献数为其 LIS 长度，答案 = 前缀最大值个数 + LIS(剩余序列)，时间复杂度 O(N log N)。</summary>
    <title>AtCoder ABC468 F - Chmax - Solution</title>
    <updated>2026-08-03T11:59:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>zaochen</name>
    </author>
    <category term="学习笔记" scheme="http://ttzc.github.io/categories/study-notes/"/>
    <category term="二分查找" scheme="http://ttzc.github.io/tags/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/"/>
    <category term="动态规划" scheme="http://ttzc.github.io/tags/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/"/>
    <category term="线性DP" scheme="http://ttzc.github.io/tags/%E7%BA%BF%E6%80%A7DP/"/>
    <category term="LIS" scheme="http://ttzc.github.io/tags/LIS/"/>
    <category term="贪心" scheme="http://ttzc.github.io/tags/%E8%B4%AA%E5%BF%83/"/>
    <content>
      <![CDATA[<h2 id="最长上升子序列lis"><a class="markdownIt-Anchor" href="#最长上升子序列lis"></a> 最长上升子序列（LIS）</h2><h3 id="问题描述"><a class="markdownIt-Anchor" href="#问题描述"></a> 问题描述</h3><p>给定一个长度为 $n$ 的序列 $a[1 \dots n]$，求其中最长的<strong>严格上升子序列</strong>的长度。</p><p>一个子序列是从原序列中删除若干元素（可以不连续）后剩余的元素保持原有顺序组成的序列。称该子序列是&quot;上升&quot;的，当且仅当对于序列中任意两个位置 $i &lt; j$，都有 $a[i] &lt; a[j]$。</p><h3 id="示例"><a class="markdownIt-Anchor" href="#示例"></a> 示例</h3><p>$$<br />a = [1, 5, 2, 3, 4]<br />$$</p><p>其中最长上升子序列为 $[1, 2, 3, 4]$，长度为 $4$。</p><hr /><h3 id="朴素-dp时间复杂度-on2"><a class="markdownIt-Anchor" href="#朴素-dp时间复杂度-on2"></a> 朴素 DP（时间复杂度 $O(n^2)$）</h3><h4 id="推导过程"><a class="markdownIt-Anchor" href="#推导过程"></a> 推导过程</h4><p>下面以 LIS 为例，对照动态规划的一般步骤：</p><ol><li><p>找子问题，把问题划分为各个阶段。<br />子问题：以 $a[i]$ 结尾的最长上升子序列。将原问题按元素位置划分为 $n$ 个阶段。</p></li><li><p>根据阶段划分确定动态规划的<strong>状态</strong>。<br />定义 $f[i]$ 表示以 $a[i]$ 结尾的最长上升子序列的长度。</p></li><li><p>找到初始状态。<br />对于任意位置 $i$，仅包含 $a[i]$ 的子序列长度为 $1$，即初始值 $f[i] = 1$。</p></li><li><p>通过阶段之间的<strong>决策</strong>找出<strong>状态转移方程</strong>。<br />若已知以 $a[j]$ 结尾的 LIS 长度为 $f[j]$ 且 $a[j] &lt; a[i]$，则将 $a[i]$ 接在后面，得到长度为 $f[j] + 1$ 的上升子序列。对所有满足条件的 $j$ 取最大值：</p><p>$$<br />f[i] = \max_{j &lt; i,, a[j] &lt; a[i]} { f[j] } + 1<br />$$</p></li><li><p>通过<strong>状态转移方程</strong>，通过递推或者记忆化搜索写出代码、优化，求出问题的解。<br />全局最优解为 $\max f[i]$。按 $i = 1$ 到 $n$ 的顺序递推，时间复杂度 $O(n^2)$。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">lis</span><span class="params">(<span class="type">const</span> vector&lt;<span class="type">int</span>&gt;&amp; a)</span> </span>&#123;</span><br><span class="line">    <span class="type">int</span> n = a.<span class="built_in">size</span>();</span><br><span class="line">    <span class="function">vector&lt;<span class="type">int</span>&gt; <span class="title">f</span><span class="params">(n, <span class="number">1</span>)</span></span>;          <span class="comment">// 初始状态：每个元素自身构成一个 LIS</span></span><br><span class="line">    <span class="type">int</span> ans = <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; n; i++) &#123; <span class="comment">// 按顺序递推</span></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> j = <span class="number">0</span>; j &lt; i; j++) &#123;</span><br><span class="line">            <span class="keyword">if</span> (a[j] &lt; a[i]) &#123;</span><br><span class="line">                f[i] = <span class="built_in">max</span>(f[i], f[j] + <span class="number">1</span>); <span class="comment">// 状态转移</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        ans = <span class="built_in">max</span>(ans, f[i]);       <span class="comment">// 记录全局最优解</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> ans;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ol><hr /><p>LIS 问题满足动态规划的两个关键性质：</p><h4 id="最优子结构"><a class="markdownIt-Anchor" href="#最优子结构"></a> 最优子结构</h4><p>全局最优解可以通过子问题的最优解递推得到。各个以 $a[j]$ 结尾的最优子序列组合起来，便得到了原问题的最优解。</p><h4 id="无后效性"><a class="markdownIt-Anchor" href="#无后效性"></a> 无后效性</h4><p>一旦 $f[i]$ 被确定，其值便固定下来，不会再被后续的计算改变。在计算 $f[k]$（$k &gt; i$）时，只需用到 $f[i]$ 的最终值，而不关心 $f[i]$ 是通过怎样的顺序、怎样的路径计算出来的。即&quot;未来和过去无关&quot;——后续状态只依赖于之前状态的值，与这些状态的历史无关。</p><hr /><h4 id="例题noip-2004-提高组-合唱队形"><a class="markdownIt-Anchor" href="#例题noip-2004-提高组-合唱队形"></a> 例题：[NOIP 2004 提高组] 合唱队形</h4><p>给定 $n$ 位同学的身高序列 $t_1, t_2, \dots, t_n$，从中选取一个子序列（保持原顺序）形成&quot;合唱队形&quot;：存在某个峰值位置 $i$，使得身高严格递增至 $t_i$，再严格递减至末尾，即：</p><p>$$<br />t_{j_1} &lt; t_{j_2} &lt; \cdots &lt; t_i &gt; t_{k_1} &gt; t_{k_2} &gt; \cdots &gt; t_{k_m}<br />$$</p><p>求最少需要出列的人数。数据范围：$2 \le n \le 100$，$130 \le t_i \le 230$。</p><p>问题等价于 $n$ 减去最长「先增后减」子序列的长度。即若以每个位置 $i$ 为峰值，定义 $L_i$ 为以 $i$ 结尾的最长上升子序列（LIS）长度、$R_i$ 为以 $i$ 开头的最长下降子序列（LDS）长度，则答案为：</p><p>$$<br />n - \max_{1 \le i \le n} \left( L_i + R_i - 1 \right)<br />$$</p><p>求解 LDS 的方法和 LIS 类似，相信理解了前文推导过程读者可以自行思考出解法。同理还有各种常见变形，比如不下降/不上升等等。</p><hr /><h3 id="二分贪心patience-sortingon-log-n"><a class="markdownIt-Anchor" href="#二分贪心patience-sortingon-log-n"></a> 二分+贪心（Patience Sorting，$O(n \log n)$）</h3><p>$\text{tails}$ 数组的单调性是整个算法的核心。下面从纸牌游戏类比出发，逐步介绍其定义，并推导其单调性与更新逻辑。</p><h4 id="直观理解"><a class="markdownIt-Anchor" href="#直观理解"></a> 直观理解</h4><p>想象你在玩一个接龙游戏纸牌游戏（Patience Solitaire）：</p><ul><li>桌面上有若干牌堆。</li><li>规则：对于新来的一张牌 $x$，你从左到右找到第一张 牌顶 $\ge x$ 的牌堆，把 $x$ 放在该牌堆的最上面（覆盖旧的牌顶）。</li><li>如果所有牌堆的牌顶都 $&lt; x$，那么就在最右边新建一个牌堆，$x$ 就是新牌堆的牌顶。</li></ul><p>在代码中，$\text{tails}$ 数组按从左到右的顺序，存储的就是每个牌堆当前的牌顶值。</p><hr /><p><img src="https://bee-reg-ab.imagency.cn/p/b2ba9b1825fc95cae4db249c1ef38a81.png" alt="接龙游戏举例" /></p><hr /><h4 id="核心性质"><a class="markdownIt-Anchor" href="#核心性质"></a> 核心性质</h4><p>$\text{tails}$ 数组始终保持<strong>严格递增</strong>，即：</p><p>$$<br />\text{tails}[0] &lt; \text{tails}[1] &lt; \text{tails}[2] &lt; \dots<br />$$</p><p>这是 Patience Sorting 最重要的不变式（Invariant），下面用<strong>数学归纳法</strong>严格证明。</p><p><strong>初始状态</strong>：空数组或单元素数组，显然严格递增。</p><p><strong>归纳假设</strong>：在处理当前元素 $x$ 之前，$\text{tails}$ 数组严格递增。</p><p><strong>执行操作</strong>：用 <code>lower_bound</code> 找到第一个满足 $\text{tails}[pos] \ge x$ 的位置 $pos$，将 $\text{tails}[pos]$ 替换为 $x$（若 $pos$ 越界则追加）。需证明替换后严格递增性不变：</p><ul><li><p><strong>左侧</strong>（$pos &gt; 0$）：<code>lower_bound</code> 保证了 $\text{tails}[pos-1] &lt; x$（否则 $pos-1$ 才是第一个 $\ge x$ 的位置）。替换后 $\text{tails}[pos-1] &lt; \text{tails}'[pos]$ 成立。</p></li><li><p><strong>右侧</strong>（$pos &lt; \text{len}-1$）：由归纳假设，替换前有 $\text{tails}[pos] &lt; \text{tails}[pos+1]$。又因 $pos$ 是第一个 $\ge x$ 的位置，故 $x \le tails[pos]$，从而：</p></li></ul><p>$$<br />x \le \text{tails}[pos] &lt; \text{tails}[pos+1]<br />$$</p><p>即 $x &lt; \text{tails}[pos+1]$，替换后 $\text{tails}‘[pos] &lt; \text{tails}’[pos+1]$ 成立。</p><ul><li><strong>其他位置</strong>：未涉及 $pos$ 的相邻关系保持不变。</li></ul><p>综上，无论追加还是原地替换，$\text{tails}$ 的严格递增性始终维持。</p><hr /><h4 id="数学定义"><a class="markdownIt-Anchor" href="#数学定义"></a> 数学定义</h4><p>$\text{tails}[i]$ 实际上代表：<strong>在所有长度为 $i+1$ 的严格递增子序列中，最小的末尾元素值是多少？</strong></p><p>因为 $\text{tails}$ 严格递增，所以这个定义是自洽的：</p><ul><li>长度为 1 的最小末尾 = $\text{tails}[0]$（全局最小值）。</li><li>长度为 2 的最小末尾 = $\text{tails}[1]$（比如 $[1, 3]$ 的末尾 3，一定大于长度为 1 的某个末尾）。</li><li>因为末尾值越小，越容易接上更大的数，所以算法总是贪心地<strong>用更小的值去替换掉相同长度下的旧末尾</strong>。</li></ul><p>这就是耐心排序法与 LIS 问题的关联。</p><hr /><h4 id="单调性与二分查找"><a class="markdownIt-Anchor" href="#单调性与二分查找"></a> 单调性与二分查找</h4><p>由于 $\text{tails}$ <strong>严格递增</strong>，当新元素 $x$ 到来时：</p><ul><li>我们要先找到位置 $pos$，是第一个满足 $\text{tails}[pos] \ge x$ 的下标值（使用 <code>lower_bound</code>）。</li><li>位置 $pos$ 的意义：<ul><li>如果 <code>pos == tails.size()</code>（即 $x$ 比所有牌顶都大），说明 $x$ 可以接在当前最长的子序列后面，形成更长的 LIS，所以新建堆（<code>push_back</code>）。</li><li>如果 <code>pos &lt; tails.size()</code>，说明 $x$ 可以接在长度为 $pos$ 的子序列后面（因为 $x$ 小于等于原来的 $\text{tails}[pos]$，但大于 $\text{tails}[pos-1]$）。我们将 $\text{tails}[pos]$ 替换为 $x$，使得长度为 $pos+1$ 的子序列末尾变得更小了，这对未来更有利。</li></ul></li></ul><hr /><p><strong>画个图理解更新过程：</strong></p><p><img src="https://bee-reg-ab.imagency.cn/p/fa2434bc66c196b841f5c6e9b0ab54de.png" alt="单调性与二分查找" /></p><hr /><p>假设当前 $\text{tails} = [2, 6, 8]$，新来 $x = 5$。</p><ul><li><code>lower_bound</code> 找到第一个 $\ge 5$ 的位置是 $index = 1$（值为 6）。</li><li>我们把 6 替换成 5，得到 $\text{tails} = [2, 5, 8]$。</li><li>含义：原本长度为 2 的最优结尾是 6（例如 $[1, 6]$），现在变成了 5（例如 $[2, 5]$ 或 $[1, 5]$）。结尾变小了，未来如果来个 7，就能形成 $[2,5,7]$ 长度为 3；如果还是 6，就无法形成长度为 3 的新序列。</li></ul><hr /><h4 id="重要误区texttails-并不是最终的-lis-序列"><a class="markdownIt-Anchor" href="#重要误区texttails-并不是最终的-lis-序列"></a> 重要误区：$\text{tails}$ 并不是最终的 LIS 序列</h4><p>这是一个极易踩的坑：<strong>$\text{tails}$ 数组里存的元素并不一定构成一个真实存在的递增子序列</strong>。</p><p>例如：对于序列 $[3, 1, 2]$。</p><ul><li>过程：<ul><li>3 来：$\text{tails} = [3]$</li><li>1 来：替换掉 3，$\text{tails} = [1]$（此时末尾变成了 1，但 LIS 不是 $[1]$，而是未来的 $[1, 2]$）</li><li>2 来：追加，$\text{tails} = [1, 2]$</li></ul></li></ul><p>$\text{tails} = [1, 2]$ 恰好是真实序列，但再看一个反例：序列 $[2, 3, 1]$。</p><ul><li>过程：<ul><li>2 来：$[2]$</li><li>3 来：$[2, 3]$</li><li>1 来：替换掉 2，$[1, 3]$</li></ul></li></ul><p>此时 $\text{tails}$ 是 $[1, 3]$，但原序列中 <strong>并不存在 $[1, 3]$</strong>（因为 1 在 3 的后面，不能接上 3）。$[1, 3]$ 只是表示：</p><ul><li>长度为 1 的最小末尾是 1。</li><li>长度为 2 的最小末尾是 3（来自真实的 $[2, 3]$）。</li></ul><p>我们只关心这个数组的<strong>长度</strong>（即牌堆的数量），它恰好等于 LIS 的长度。</p><hr /><h4 id="总结"><a class="markdownIt-Anchor" href="#总结"></a> 总结</h4><ul><li><strong>单调递增</strong> 使得我们可以用 $O(\log N)$ 的二分查找快速定位更新位置。</li><li>每次更新都是用当前元素去尽可能降低某个牌堆的顶部值，为后面的元素创造更多”接上去”的机会。</li><li>这是一种<strong>在线算法</strong>，扫描一遍数据即可，空间仅需维护 $\text{tails}$ 数组。</li></ul><p>如果读者此前接触过树状数组做法（基于值域 DP），那里维护的是”以某个值结尾的最佳长度”，是另一种完全不同的视角（动态规划）。而 Patience Sorting 是从”维护最优末尾集合”的角度出发的贪心，这也是它代码极其精妙的原因。</p><h4 id="代码实现"><a class="markdownIt-Anchor" href="#代码实现"></a> 代码实现</h4><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">lis</span><span class="params">(<span class="type">const</span> vector&lt;<span class="type">int</span>&gt;&amp; a)</span> </span>&#123;</span><br><span class="line">    vector&lt;<span class="type">int</span>&gt; tails; <span class="comment">// tails[k] 表示长度为 k+1 的 LIS 的最小末尾值</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> val : a) &#123;</span><br><span class="line">        <span class="keyword">auto</span> it = <span class="built_in">lower_bound</span>(tails.<span class="built_in">begin</span>(), tails.<span class="built_in">end</span>(), val);</span><br><span class="line">        <span class="keyword">if</span> (it == tails.<span class="built_in">end</span>())</span><br><span class="line">            tails.<span class="built_in">push_back</span>(val);</span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">            *it = val;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> tails.<span class="built_in">size</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>时间复杂度 $O(n \log n)$。</p><h3 id="树状数组优化-dp离散化后-on-log-n"><a class="markdownIt-Anchor" href="#树状数组优化-dp离散化后-on-log-n"></a> 树状数组优化 DP（离散化后 $O(n \log n)$）</h3><p>未完待续…</p>]]>
    </content>
    <id>http://ttzc.github.io/428bd345/</id>
    <link href="http://ttzc.github.io/428bd345/"/>
    <published>2026-08-02T05:59:00.000Z</published>
    <summary>LIS 最长上升子序列的线性 DP 学习笔记，涵盖 O(n²) 朴素动态规划推导、最优子结构与无后效性分析、NOIP 2004 合唱队形例题，以及 O(n log n) 的 Patience Sorting（二分贪心）优化与常见误区。</summary>
    <title>【动态规划】线性 DP 学习笔记</title>
    <updated>2026-08-02T05:59:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>zaochen</name>
    </author>
    <category term="题解" scheme="http://ttzc.github.io/categories/solution/"/>
    <category term="树状数组" scheme="http://ttzc.github.io/tags/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/"/>
    <category term="离散化" scheme="http://ttzc.github.io/tags/%E7%A6%BB%E6%95%A3%E5%8C%96/"/>
    <category term="Luogu" scheme="http://ttzc.github.io/tags/Luogu/"/>
    <content>
      <![CDATA[<h2 id="1-题目数据"><a class="markdownIt-Anchor" href="#1-题目数据"></a> 1. 题目数据</h2><ul><li><strong>题目类型</strong>：传统题</li><li><strong>题目链接</strong>：<a href="https://www.luogu.com.cn/problem/P1637">P1637 三元上升子序列 - 洛谷</a></li><li><strong>时间限制</strong>：1.00s</li><li><strong>内存限制</strong>：128.00MB</li></ul><h2 id="2-题意简述"><a class="markdownIt-Anchor" href="#2-题意简述"></a> 2. 题意简述</h2><p>给定长度为 $N$ 的序列 $a$，统计满足 $i &lt; j &lt; k$ 且 $a[i] &lt; a[j] &lt; a[k]$ 的三元组 $(i, j, k)$ 的个数。</p><ul><li>$1 \le N \le 10^5$</li><li>$1 \le a_i \le 10^9$</li></ul><h2 id="3-朴素解法"><a class="markdownIt-Anchor" href="#3-朴素解法"></a> 3. 朴素解法</h2><p>枚举中间位置 $j$，对每个 $j$ 扫描其左侧和右侧。记左侧满足 $a[i] &lt; a[j]$ 的个数为 $L_j$，右侧满足 $a[k] &gt; a[j]$ 的个数为 $R_j$，则以 $j$ 为中间的三元组个数为 $L_j \times R_j$。对每个 $j$ 扫描两侧需要 $O(N)$，总复杂度 $O(N^2)$，在 $N = 10^5$ 时不可行。</p><p>$$<br />\text{ans} = \sum_{j=1}^{N} L_j \times R_j<br />$$</p><h2 id="4-核心解法"><a class="markdownIt-Anchor" href="#4-核心解法"></a> 4. 核心解法</h2><p>朴素解法的计数框架是正确的——枚举中间位置 $j$，计算左侧小于 $a[j]$ 的个数 $L_j$ 和右侧大于 $a[j]$ 的个数 $R_j$。瓶颈在于对每个 $j$ 都需要 $O(N)$ 时间统计 $L_j$ 和 $R_j$。注意到 $L_j$ 只依赖于 $a[1…j-1]$ 中小于 $a[j]$ 的个数，这是一个典型的前缀计数问题，可以用权值树状数组（Fenwick Tree）在 $O(\log V)$ 时间内完成查询和更新。</p><h3 id="离散化"><a class="markdownIt-Anchor" href="#离散化"></a> 离散化</h3><p>由于 $a_i$ 可达 $10^9$，但 $N \le 10^5$，需要对值域进行离散化。将所有 $a_i$ 排序去重，映射到 $[1, M]$（$M \le N$）。离散化后，树状数组的大小为 $M$，所有操作在 $O(\log M) = O(\log N)$ 内完成。</p><h3 id="计算-l_j"><a class="markdownIt-Anchor" href="#计算-l_j"></a> 计算 $L_j$</h3><p>从左到右遍历，维护一个权值树状数组 $T_L$。遍历到 $j$ 时，$T_L$ 中已加入 $a[1], a[2], \ldots, a[j-1]$。$L_j = T_L.query(\text{rank}(a[j]) - 1)$，即查询值严格小于 $a[j]$ 的元素个数，然后将 $a[j]$ 加入 $T_L$。</p><h3 id="计算-r_j"><a class="markdownIt-Anchor" href="#计算-r_j"></a> 计算 $R_j$</h3><p>从右到左遍历，维护一个权值树状数组 $T_R$。遍历到 $j$ 时，$T_R$ 中已加入 $a[j+1], a[j+2], \ldots, a[N]$。查询 $T_R$ 中值 $\le a[j]$ 的元素个数为 $T_R.query(\text{rank}(a[j]))$（包含刚加入的 $a[j]$ 自身）。位置 $&gt; j$ 的元素总数为 $N - j$，其中值 $\le a[j]$ 的元素有 $T_R.query(\text{rank}(a[j])) - 1$ 个，故右侧严格大于 $a[j]$ 的元素个数为：</p><p>$$<br />R_j = (N - j) - (T_R.query(\text{rank}(a[j])) - 1) = N - j - T_R.query(\text{rank}(a[j])) + 1<br />$$</p><p>然后将 $a[j]$ 加入 $T_R$。</p><h3 id="推导过程"><a class="markdownIt-Anchor" href="#推导过程"></a> 推导过程</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">ans = <span class="number">0</span></span><br><span class="line"><span class="keyword">for</span> j = <span class="number">1</span> to N:</span><br><span class="line">    L_j = TL.query(rank(a[j]) - <span class="number">1</span>)</span><br><span class="line">    TL.add(rank(a[j]), <span class="number">1</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> j = N down to <span class="number">1</span>:</span><br><span class="line">    TR.add(rank(a[j]), <span class="number">1</span>)</span><br><span class="line">    R_j = N - j - TR.query(rank(a[j])) + <span class="number">1</span></span><br><span class="line">    ans += L_j * R_j</span><br></pre></td></tr></table></figure><h2 id="5-正确性证明"><a class="markdownIt-Anchor" href="#5-正确性证明"></a> 5. 正确性证明</h2><h3 id="l_j-的正确性"><a class="markdownIt-Anchor" href="#l_j-的正确性"></a> $L_j$ 的正确性</h3><p>$T_L$ 在计算 $L_j$ 时恰好包含 $a[1…j-1]$。$T_L.query(\text{rank}(a[j])-1)$ 统计的是值域中映射到 $[1, \text{rank}(a[j])-1]$ 的元素个数。由于离散化保持大小关系，rank 越小对应原值越小，因此这恰好是值严格小于 $a[j]$ 的元素个数。$L_j$ 正确。</p><h3 id="r_j-的正确性"><a class="markdownIt-Anchor" href="#r_j-的正确性"></a> $R_j$ 的正确性</h3><p>$T_R$ 在计算 $R_j$ 时恰好包含 $a[j+1…N]$。$T_R.query(\text{rank}(a[j]))$ 统计的是值域中映射到 $[1, \text{rank}(a[j])]$ 的元素个数，包含所有值 $\le a[j]$ 的元素（包括刚加入的 $a[j]$ 自身）。因此，值 $\le a[j]$ 的元素个数（含 $a[j]$）为 $T_R.query(\text{rank}(a[j]))$，其中 $a[j]$ 自身占 1 个，剩余 $T_R.query(\text{rank}(a[j])) - 1$ 个是值 $\le a[j]$ 的其他元素（均在位置 $&gt; j$ 处）。位置 $&gt; j$ 的元素总数为 $N - j$，故值 $&gt; a[j]$ 的元素个数：</p><p>$$<br />R_j = (N - j) - (T_R.query(\text{rank}(a[j])) - 1) = N - j - T_R.query(\text{rank}(a[j])) + 1<br />$$</p><h3 id="乘法原理"><a class="markdownIt-Anchor" href="#乘法原理"></a> 乘法原理</h3><p>对于固定的 $j$，左侧任意一个值 $&lt; a[j]$ 的元素 $a[i]$ 与右侧任意一个值 $&gt; a[j]$ 的元素 $a[k]$ 都能唯一确定一个满足条件的三元组 $(i, j, k)$，且不同 $(i, k)$ 对对应不同的三元组。因此以 $j$ 为中间元素的三元组总数恰为 $L_j \times R_j$。对所有 $j$ 求和即得总数。综上所述，整个计数过程正确。</p><h2 id="6-复杂度分析"><a class="markdownIt-Anchor" href="#6-复杂度分析"></a> 6. 复杂度分析</h2><ul><li><strong>时间复杂度</strong>：$O(N \log N)$。离散化排序 $O(N \log N)$，两次遍历各 $N$ 次 BIT 操作，每次 $O(\log N)$。</li><li><strong>空间复杂度</strong>：$O(N)$，离散化数组 + 树状数组 + $L, R$ 数组。</li></ul><p>$N = 10^5$ 时，$2 \times 10^5$ 次 $\log$ 级别的 BIT 操作完全在时间限制内。</p><h2 id="7-实现细节与避坑指南"><a class="markdownIt-Anchor" href="#7-实现细节与避坑指南"></a> 7. 实现细节与避坑指南</h2><table><thead><tr><th style="text-align:left">坑点</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left"><strong>离散化去重</strong></td><td style="text-align:left"><code>sort</code> + <code>unique</code> 去重，注意 <code>unique</code> 返回的迭代器与原起点的差值为新长度。</td></tr><tr><td style="text-align:left"><strong>下标从 1 开始</strong></td><td style="text-align:left">树状数组下标从 1 开始，离散化后的 rank 也是从 1 开始，注意 <code>query(rank - 1)</code> 时 rank = 1 时 query(0) 返回 0，不越界。</td></tr><tr><td style="text-align:left"><strong>long long</strong></td><td style="text-align:left">$N = 10^5$ 时最坏情况下三元组个数可达 $\binom{10^5}{3} \approx 1.67 \times 10^{14}$，需要 <code>long long</code> 存储答案。</td></tr><tr><td style="text-align:left"><strong>严格不等式</strong></td><td style="text-align:left">左侧用 <code>query(rank - 1)</code>（严格小于），右侧用上述公式（严格大于），相等值不构成三元组。</td></tr></tbody></table><h2 id="8-参考代码"><a class="markdownIt-Anchor" href="#8-参考代码"></a> 8. 参考代码</h2><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// https://www.luogu.com.cn/problem/P1637</span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;bits/stdc++.h&gt;</span></span></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> int long long</span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> n;</span><br><span class="line"><span class="type">int</span> a[<span class="number">100005</span>], b[<span class="number">100005</span>], m;</span><br><span class="line"><span class="type">int</span> c[<span class="number">100005</span>];</span><br><span class="line"><span class="type">int</span> L[<span class="number">100005</span>], R[<span class="number">100005</span>];</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">lowbit</span><span class="params">(<span class="type">int</span> x)</span> </span>&#123; <span class="keyword">return</span> x &amp; (-x); &#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">add</span><span class="params">(<span class="type">int</span> id, <span class="type">int</span> x)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = id; i &lt;= m; i += <span class="built_in">lowbit</span>(i))</span><br><span class="line">        c[i] += x;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">query</span><span class="params">(<span class="type">int</span> id)</span> </span>&#123;</span><br><span class="line">    <span class="type">int</span> ret = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = id; i; i -= <span class="built_in">lowbit</span>(i))</span><br><span class="line">        ret += c[i];</span><br><span class="line">    <span class="keyword">return</span> ret;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">signed</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    ios::<span class="built_in">sync_with_stdio</span>(<span class="number">0</span>);</span><br><span class="line"><span class="meta">#<span class="keyword">ifndef</span> ONLINE_JUDGE</span></span><br><span class="line">    <span class="built_in">freopen</span>(<span class="string">&quot;data.in&quot;</span>, <span class="string">&quot;r&quot;</span>, stdin);</span><br><span class="line">    <span class="built_in">freopen</span>(<span class="string">&quot;data.out&quot;</span>, <span class="string">&quot;w&quot;</span>, stdout);</span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line"></span><br><span class="line">    cin &gt;&gt; n;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= n; i++) &#123;</span><br><span class="line">        cin &gt;&gt; a[i];</span><br><span class="line">        b[i] = a[i];</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">sort</span>(b + <span class="number">1</span>, b + <span class="number">1</span> + n);</span><br><span class="line">    m = <span class="built_in">unique</span>(b + <span class="number">1</span>, b + <span class="number">1</span> + n) - b - <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= n; i++)</span><br><span class="line">        a[i] = <span class="built_in">lower_bound</span>(b + <span class="number">1</span>, b + <span class="number">1</span> + m, a[i]) - b;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 从左到右：计算 L[j]</span></span><br><span class="line">    <span class="built_in">memset</span>(c, <span class="number">0</span>, <span class="keyword">sizeof</span> c);</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> j = <span class="number">1</span>; j &lt;= n; j++) &#123;</span><br><span class="line">        L[j] = <span class="built_in">query</span>(a[j] - <span class="number">1</span>);</span><br><span class="line">        <span class="built_in">add</span>(a[j], <span class="number">1</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 从右到左：计算 R[j]</span></span><br><span class="line">    <span class="built_in">memset</span>(c, <span class="number">0</span>, <span class="keyword">sizeof</span> c);</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> j = n; j &gt;= <span class="number">1</span>; j--) &#123;</span><br><span class="line">        <span class="built_in">add</span>(a[j], <span class="number">1</span>);</span><br><span class="line">        R[j] = n - j - <span class="built_in">query</span>(a[j]) + <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> ans = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> j = <span class="number">1</span>; j &lt;= n; j++)</span><br><span class="line">        ans += L[j] * R[j];</span><br><span class="line"></span><br><span class="line">    cout &lt;&lt; ans &lt;&lt; endl;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>#define int long long</code> 是 OI 中的常用习惯，统一处理可以避免中间过程溢出，本题最坏情况下答案约为 $1.67 \times 10^{14}$，用 <code>long long</code> 类型最稳妥。</p><h2 id="9-补充说明"><a class="markdownIt-Anchor" href="#9-补充说明"></a> 9. 补充说明</h2><ul><li><strong>相关题目</strong>：类似思路可推广到统计满足 $a[i] &lt; a[j] &lt; a[k]$ 或 $a[i] &gt; a[j] &gt; a[k]$ 的三元组，也可用于统计四元组等更高维的组合计数问题。<a href="https://www.luogu.com.cn/problem/P10589">P10589 楼兰图腾 - 洛谷</a> 是同一思路的变体。</li><li><strong>学习笔记</strong>：树状数组基础与权值树状数组详见 <a   href='/41e5d11e/'>【数据结构】树状数组 学习笔记</a></li></ul><p>这个方法在计数类问题中非常经典，思路清晰且巧妙，是树状数组的重要应用之一，值得掌握。</p>]]>
    </content>
    <id>http://ttzc.github.io/7c4f4089/</id>
    <link href="http://ttzc.github.io/7c4f4089/"/>
    <published>2026-07-30T12:23:00.000Z</published>
    <summary>
      <![CDATA[给定长度为 $N$ 的序列，求满足 $i < j < k$ 且 $a[i] < a[j] < a[k]$ 的三元组个数。$N \le 10^5$，$a_i \le 10^9$。通过枚举中间位置 $j$，利用权值树状数组分别统计左右两侧的可行元素个数，$O(N \log N)$ 解决。]]>
    </summary>
    <title>洛谷 P1637 三元上升子序列 - Solution</title>
    <updated>2026-07-30T12:23:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>zaochen</name>
    </author>
    <category term="题解" scheme="http://ttzc.github.io/categories/solution/"/>
    <category term="LeetCode" scheme="http://ttzc.github.io/tags/LeetCode/"/>
    <category term="最短路" scheme="http://ttzc.github.io/tags/%E6%9C%80%E7%9F%AD%E8%B7%AF/"/>
    <category term="Dijkstra" scheme="http://ttzc.github.io/tags/Dijkstra/"/>
    <category term="堆（优先队列）" scheme="http://ttzc.github.io/tags/%E5%A0%86%EF%BC%88%E4%BC%98%E5%85%88%E9%98%9F%E5%88%97%EF%BC%89/"/>
    <content>
      <![CDATA[<h2 id="1-题目数据-problem-metadata"><a class="markdownIt-Anchor" href="#1-题目数据-problem-metadata"></a> 1. 题目数据 (Problem Metadata)</h2><ul><li><strong>题目类型</strong>：传统题</li><li><strong>题目链接</strong>：<a href="https://leetcode.cn/problems/minimum-cost-path-with-alternating-directions-iii/description/">4003. 交替方向的最小路径代价 III - 力扣（LeetCode）</a></li></ul><h2 id="2-题意简述-problem-summary"><a class="markdownIt-Anchor" href="#2-题意简述-problem-summary"></a> 2. 题意简述 (Problem Summary)</h2><p>已知 $m \times n$ 网格（$1 \le m, n \le 10^5$，$2 \le m \cdot n \le 10^5$），每个单元格 $(i,j)$ 有入口代价 $\text{entry}(i,j) = (i+1)(j+1)$ 和罚金 $\text{penalty}[i][j]$（$0 \le \text{penalty}[i][j] \le 10^5$）。从 $(0,0)$ 出发，初始已付入口代价 $1$。第 $k$ 步移动遵循奇偶交替方向规则：$k$ 为奇数时只能向右或向下，$k$ 为偶数时只能向左或向上。若违反规则移动，需额外支付离开格子 $(i,j)$ 的 $\text{penalty}[i][j]$；也可原地等待（步数 $+1$、方向切换）并支付 $\text{penalty}[i][j]$。求从 $(0,0)$ 到达 $(m-1,n-1)$ 的最小总代价。</p><h2 id="3-朴素解法-brute-force"><a class="markdownIt-Anchor" href="#3-朴素解法-brute-force"></a> 3. 朴素解法 (Brute-Force)</h2><p>对每一步做 DFS 枚举四个方向加等待，每条路径的状态数最多为总移动次数。由于网格大小 $m \cdot n \le 10^5$，且方向每步在切换，最坏情况下的路径长度远超网格大小（可能在相邻格子间反复横跳跳板等待），搜索空间指数级增长，无法通过。</p><p>更实际的问题是：每个格子的最优代价不仅取决于位置，还取决于到达该位置时当前步数的奇偶性——奇偶性不同，下一步能走的方向不同，后续代价也不同。朴素 DFS 必须同时记住位置和奇偶性才能避免重复搜索，这自然引出带状态的最短路。</p><h2 id="4-核心解法-main-solution"><a class="markdownIt-Anchor" href="#4-核心解法-main-solution"></a> 4. 核心解法 (Main Solution)</h2><h3 id="特殊性质"><a class="markdownIt-Anchor" href="#特殊性质"></a> 特殊性质</h3><p>本题的核心性质是「到达每个格子时，步数的奇偶性决定后续可走方向」。因此状态不能只用位置 $(i,j)$ 表示——同一格子、不同奇偶性对应两个不同的节点。将奇偶性纳入状态后，转化为标准的单源最短路问题，每条合法移动（含等待）对应一条有向边，边权为入口代价加上可能的罚金，跑 Dijkstra 即可。</p><h3 id="关键突破"><a class="markdownIt-Anchor" href="#关键突破"></a> 关键突破</h3><p>从 DFS 暴搜的瓶颈出发：位置 + 奇偶性恰好构成一个隐式图，节点数为 $2mn \le 2 \times 10^5$，每个节点最多 5 条出边（四个方向 + 等待），总边数 $O(mn)$。在这个规模上 Dijkstra（$O(E \log V)$）完全可行。</p><h3 id="推导过程"><a class="markdownIt-Anchor" href="#推导过程"></a> 推导过程</h3><p><strong>状态设计。</strong> 将节点定义为 $(i, j, \text{oe})$，其中 $\text{oe}$ 为到达 $(i,j)$ 后已完成的移动次数（含进入 $(0,0)$ 算第 $1$ 步）。$\text{oe}$ 为奇数时下一步只能右/下，$\text{oe}$ 为偶数时下一步只能左/上。用 $\text{dist}[i][j][\text{oe}]$ 表示从起点到该状态的最小代价。</p><p><strong>转移。</strong> 从状态 $(i,j,\text{oe})$ 出发，有 $5$ 种选择：</p><ul><li><strong>符合规则的移动</strong>（奇偶方向匹配）：支付目标格子的入口代价 $\text{entry}(i’,j’)$，转移至 $(i’,j’,\text{oe}+1)$。</li><li><strong>违反规则的移动</strong>（奇偶方向不匹配）：除目标入口代价外，还需支付离开格子 $(i,j)$ 的 $\text{penalty}[i][j]$。</li><li><strong>原地等待</strong>：支付 $\text{penalty}[i][j]$，转移至 $(i,j,\text{oe}+1)$。</li></ul><p>四条方向移动是否合法由奇偶性判定：</p><table><thead><tr><th style="text-align:left">$\text{oe}$ 奇偶</th><th style="text-align:left">合法方向</th><th style="text-align:left">违规方向（加罚金）</th></tr></thead><tbody><tr><td style="text-align:left">奇数</td><td style="text-align:left">$(i+1,j)$（下）、$(i,j+1)$（右）</td><td style="text-align:left">$(i-1,j)$（上）、$(i,j-1)$（左）</td></tr><tr><td style="text-align:left">偶数</td><td style="text-align:left">$(i-1,j)$（上）、$(i,j-1)$（左）</td><td style="text-align:left">$(i+1,j)$（下）、$(i,j+1)$（右）</td></tr></tbody></table><p>Dijkstra 从初始状态 $(0,0,1)$（已付入口 $1$）开始松弛，直到首次弹出终点状态 $(m-1,n-1,\text{oe})$ 时返回当前 $\text{dist}$。</p><h2 id="5-正确性证明-proof-of-correctness"><a class="markdownIt-Anchor" href="#5-正确性证明-proof-of-correctness"></a> 5. 正确性证明 (Proof of Correctness)</h2><p><strong>状态空间完整性。</strong> 本题中移动方向完全由已走步数的奇偶性决定，与路径历史中具体经过了哪些格子无关。因此 $(i,j,\text{oe})$ 足以唯一刻画一个节点在未来所有可能转移中的行为，满足无后效性。所有合法移动（含等待、违规移动）均已作为有向边建模，状态空间不重不漏。</p><p><strong>边权非负性。</strong> 入口代价 $(i+1)(j+1) \ge 1$，罚金 $\text{penalty}[i][j] \ge 0$，所有边权均非负。因此在非负权图上运行 Dijkstra 能正确求出单源最短路。</p><p><strong>终点条件。</strong> 题目要求到达 $(m-1,n-1)$，不限制到达时的 $\text{oe}$ 奇偶性。Dijkstra 在首次弹出任意 $\text{oe}$ 的终点状态时，该 $\text{dist}$ 即为所有可能 $\text{oe}$ 中的最小值——因为堆顶总是当前所有未处理节点中 $\text{dist}$ 最小的，而终点状态一旦弹出就不可能被更短的路径更新。</p><p>综上所述，算法正确。</p><h2 id="6-复杂度分析-complexity"><a class="markdownIt-Anchor" href="#6-复杂度分析-complexity"></a> 6. 复杂度分析 (Complexity)</h2><ul><li><strong>时间复杂度</strong>：$O(mn \log(mn))$。节点数 $V = 2mn$，每个节点最多 $5$ 条出边，总边数 $E = O(mn)$。Dijkstra 在堆上的操作为 $O((V+E) \log V) = O(mn \log(mn))$。$mn \le 10^5$ 时约 $10^5 \times \log_2(2\times10^5) \approx 1.8 \times 10^6$ 次堆操作，轻松通过。</li><li><strong>空间复杂度</strong>：$O(mn)$。堆中最多同时容纳 $O(mn)$ 个状态，<code>vis</code> 集合同样 $O(mn)$。$2mn \le 2\times 10^5$，远未超标。</li></ul><h2 id="7-实现细节与避坑指南-implementation-details"><a class="markdownIt-Anchor" href="#7-实现细节与避坑指南-implementation-details"></a> 7. 实现细节与避坑指南 (Implementation Details)</h2><table><thead><tr><th style="text-align:left">坑点</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left"><strong><code>vis</code> 标记的位置</strong></td><td style="text-align:left">必须在 <code>pop()</code> 后标记，不能在 <code>push()</code> 时标记。堆中同一节点可能有多条路径记录，只有首次弹出才是最短距离，后面的直接 <code>continue</code> 跳过。若 <code>push</code> 时就标记，后面更短的路径会被拦下。</td></tr><tr><td style="text-align:left"><strong><code>continue</code> 与终点判断的顺序</strong></td><td style="text-align:left"><code>pop</code> → <code>if (vis) continue</code> → 判断是否到终点 → 扩展邻居。若先判断终点再判重，同一终点可能被多次处理（不同 $\text{oe}$ 第一次弹出即为该 $\text{oe}$ 下的最优，重复弹出会被 <code>continue</code> 安全跳过，但顺序正确更清晰）。</td></tr><tr><td style="text-align:left"><strong>状态必须包含奇偶性</strong></td><td style="text-align:left"><code>vis</code> 的键必须同时编码位置 $(i,j)$ 和奇偶性 $\text{oe}$。我的实现用 <code>hsh</code> 函数将 $(i,j,\text{oe})$ 映射到整数值：<code>n*i + j</code> 编码位置，<code>oe</code> 为奇数时偏移 $+mn$。若只编码位置，不同奇偶性会被错误视为同一节点。</td></tr><tr><td style="text-align:left"><strong>罚金归属</strong></td><td style="text-align:left">$\text{penalty}[i][j]$ 是「从 $(i,j)$ 违规移动」或「在 $(i,j)$ 等待」的代价，应加到当前所在格子，而非目标格子。违规移动的边权 = $\text{entry}(i’,j’) + \text{penalty}[i][j]$，不是 $\text{penalty}[i’][j’]$。</td></tr><tr><td style="text-align:left"><strong>等待状态不能漏</strong></td><td style="text-align:left">原地等待是改变奇偶性的唯一「无位移」手段。当目标方向与当前奇偶性冲突时，等待一步切换方向后出发可能比违规移动代价更低。漏写等待会导致某些路径无法遍历。</td></tr><tr><td style="text-align:left"><strong>整数溢出</strong></td><td style="text-align:left">入口代价 $(i+1)(j+1)$ 最大 $10<sup>5\times10</sup>5 = 10^{10}$，超过 <code>int</code> 上限。<code>val</code> 必须用 <code>long long</code>。我的实现中 <code>f(i,j)</code> 返回 <code>ll</code>，<code>state.val</code> 为 <code>ll</code>，避免隐式类型转换溢出。</td></tr><tr><td style="text-align:left"><strong>堆的比较器</strong></td><td style="text-align:left">C++ <code>priority_queue</code> 默认大顶堆，取 <code>val</code> 最小的需重载 <code>operator&lt;</code> 返回 <code>val &gt; s.val</code>（反直觉但正确：让 <code>val</code> 小的元素排在堆顶）。第一次写时容易写成 <code>val &lt; s.val</code> 变成大顶堆。</td></tr></tbody></table><h2 id="8-参考代码-reference-code"><a class="markdownIt-Anchor" href="#8-参考代码-reference-code"></a> 8. 参考代码 (Reference Code)</h2><p>下面是我赛后的实现，「奇偶性维度编码 + Dijkstra + 等待状态」的思路。用 <code>unordered_set&lt;int&gt;</code> 做 <code>vis</code>，<code>hsh</code> 函数将 $(i,j,\text{oe})$ 映射到一维整数避免手写哈希。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span> &#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">    <span class="keyword">using</span> ll = <span class="type">long</span> <span class="type">long</span>;</span><br><span class="line"></span><br><span class="line">    <span class="function">ll <span class="title">f</span><span class="params">(<span class="type">int</span> i, <span class="type">int</span> j)</span> </span>&#123; <span class="keyword">return</span> ((ll)i + <span class="number">1</span>) * (j + <span class="number">1</span>); &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">struct</span> <span class="title class_">state</span> &#123;</span><br><span class="line">        <span class="type">int</span> i, j;</span><br><span class="line">        ll val, oe; <span class="comment">// oe: 已走步数（进入(0,0)算1），奇偶决定下一步方向</span></span><br><span class="line">        <span class="built_in">state</span>(<span class="type">int</span> _i = <span class="number">0</span>, <span class="type">int</span> _j = <span class="number">0</span>, ll _val = <span class="number">0</span>, ll _oe = <span class="number">0</span>)</span><br><span class="line">            : <span class="built_in">i</span>(_i), <span class="built_in">j</span>(_j), <span class="built_in">val</span>(_val), <span class="built_in">oe</span>(_oe) &#123;&#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="type">int</span> <span class="title">hsh</span><span class="params">(<span class="type">int</span> m, <span class="type">int</span> n)</span> </span>&#123;</span><br><span class="line">            <span class="type">int</span> res = n * i + j;  <span class="comment">// 位置编码到 [0, mn)</span></span><br><span class="line">            <span class="keyword">if</span> (oe &amp; <span class="number">1</span>)</span><br><span class="line">                res += m * n;     <span class="comment">// 奇数oe偏移到 [mn, 2mn)</span></span><br><span class="line">            <span class="keyword">return</span> res;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="type">bool</span> <span class="keyword">operator</span>&lt;(<span class="type">const</span> state&amp; s) <span class="type">const</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> val &gt; s.val;   <span class="comment">// 小顶堆</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">bool</span> <span class="title">check</span><span class="params">(<span class="type">int</span> i, <span class="type">int</span> j, <span class="type">int</span> m, <span class="type">int</span> n)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> i &gt;= <span class="number">0</span> &amp;&amp; j &gt;= <span class="number">0</span> &amp;&amp; i &lt; m &amp;&amp; j &lt; n;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function">ll <span class="title">minCost</span><span class="params">(<span class="type">int</span> m, <span class="type">int</span> n, vector&lt;vector&lt;<span class="type">int</span>&gt;&gt;&amp; penalty)</span> </span>&#123;</span><br><span class="line">        priority_queue&lt;state&gt; q;</span><br><span class="line">        unordered_set&lt;<span class="type">int</span>&gt; vis;</span><br><span class="line"></span><br><span class="line">        q.<span class="built_in">emplace</span>(<span class="number">0</span>, <span class="number">0</span>, <span class="number">1</span>, <span class="number">1</span>); <span class="comment">// 入口(0,0)已付代价1</span></span><br><span class="line">        <span class="keyword">while</span> (!q.<span class="built_in">empty</span>()) &#123;</span><br><span class="line">            state s = q.<span class="built_in">top</span>(); q.<span class="built_in">pop</span>();</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (vis.<span class="built_in">count</span>(s.<span class="built_in">hsh</span>(m, n)))</span><br><span class="line">                <span class="keyword">continue</span>;</span><br><span class="line">            vis.<span class="built_in">insert</span>(s.<span class="built_in">hsh</span>(m, n));</span><br><span class="line"></span><br><span class="line">            <span class="type">int</span> i = s.i, j = s.j;</span><br><span class="line">            ll val = s.val, oe = s.oe;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (i == m - <span class="number">1</span> &amp;&amp; j == n - <span class="number">1</span>)</span><br><span class="line">                <span class="keyword">return</span> val;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 扩展邻居：四方向 + 等待</span></span><br><span class="line">            <span class="comment">// 奇偶性为真（奇数步）：合法→下/右；违规→上/左（加罚）</span></span><br><span class="line">            <span class="comment">// 奇偶性为假（偶数步）：合法→上/左；违规→下/右（加罚）</span></span><br><span class="line">            <span class="type">int</span> dirs[<span class="number">4</span>][<span class="number">2</span>] = &#123;&#123;<span class="number">1</span>,<span class="number">0</span>&#125;, &#123;<span class="number">-1</span>,<span class="number">0</span>&#125;, &#123;<span class="number">0</span>,<span class="number">1</span>&#125;, &#123;<span class="number">0</span>,<span class="number">-1</span>&#125;&#125;;</span><br><span class="line">            <span class="type">bool</span> legal[<span class="number">4</span>]; <span class="comment">// legal[k] = 该方向是否合法</span></span><br><span class="line">            <span class="keyword">if</span> (oe &amp; <span class="number">1</span>) &#123;</span><br><span class="line">                <span class="comment">// 奇数步：下(0)、右(2) 合法；上(1)、左(3) 违规</span></span><br><span class="line">                legal[<span class="number">0</span>] = legal[<span class="number">2</span>] = <span class="literal">true</span>;</span><br><span class="line">                legal[<span class="number">1</span>] = legal[<span class="number">3</span>] = <span class="literal">false</span>;</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                <span class="comment">// 偶数步：上(1)、左(3) 合法；下(0)、右(2) 违规</span></span><br><span class="line">                legal[<span class="number">1</span>] = legal[<span class="number">3</span>] = <span class="literal">true</span>;</span><br><span class="line">                legal[<span class="number">0</span>] = legal[<span class="number">2</span>] = <span class="literal">false</span>;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">for</span> (<span class="type">int</span> k = <span class="number">0</span>; k &lt; <span class="number">4</span>; k++) &#123;</span><br><span class="line">                <span class="type">int</span> ni = i + dirs[k][<span class="number">0</span>], nj = j + dirs[k][<span class="number">1</span>];</span><br><span class="line">                <span class="keyword">if</span> (!<span class="built_in">check</span>(ni, nj, m, n)) <span class="keyword">continue</span>;</span><br><span class="line">                ll cost = val + <span class="built_in">f</span>(ni, nj);</span><br><span class="line">                <span class="keyword">if</span> (!legal[k]) cost += penalty[i][j]; <span class="comment">// 违规加罚金</span></span><br><span class="line">                <span class="function">state <span class="title">nxt</span><span class="params">(ni, nj, cost, oe + <span class="number">1</span>)</span></span>;</span><br><span class="line">                <span class="keyword">if</span> (!vis.<span class="built_in">count</span>(nxt.<span class="built_in">hsh</span>(m, n)))</span><br><span class="line">                    q.<span class="built_in">push</span>(nxt);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 原地等待</span></span><br><span class="line">            <span class="function">state <span class="title">wait</span><span class="params">(i, j, val + penalty[i][j], oe + <span class="number">1</span>)</span></span>;</span><br><span class="line">            <span class="keyword">if</span> (!vis.<span class="built_in">count</span>(wait.<span class="built_in">hsh</span>(m, n)))</span><br><span class="line">                q.<span class="built_in">push</span>(wait);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="number">-1</span>; <span class="comment">// 不应到达</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="9-补充说明-additional-notes"><a class="markdownIt-Anchor" href="#9-补充说明-additional-notes"></a> 9. 补充说明 (Additional Notes)</h2><ul><li><strong>题目定位</strong>：本题是一道比较板的 Dijkstra 应用题，核心在于将「奇偶性决定方向」编码为状态维度。类似技巧在竞赛中常见：当移动规则由步数/层数/颜色等属性决定时，把该属性纳入状态空间即可转化为标准最短路。其实相当于把每个位置根据奇偶性拆点转化为分层图最短路，我们接下来详细讨论一下这个等价建模，但是状态转移的写法更直观。可以用 Dijkstra 的核心依据是边权非负。</li><li><strong>等价建模</strong>：本题本质上是一个<strong>分层图最短路</strong>。将状态拆分为第 0 层（奇数步）和第 1 层（偶数步）。奇数层：只能向“下/右”走到偶数层。偶数层：只能向“上/左”走到奇数层。等待边：从本层连向另一层，边权为罚金。</li><li><strong>Dijkstra 通用注意</strong>：<code>vis</code> 必须在 <code>pop</code> 后标记、<code>continue</code> 在扩展前执行、堆比较器需反转——这几个坑在写 Dijkstra 时反复出现。我在 §7 中逐一标注了它们的位置和原因，后面遇到类似的 Dijkstra 题直接套这个框架就行。赛时因为这些细节反复提交错误代码，比较可惜。</li><li><strong>vis 用 set vs. 数组</strong>：本题 $2mn \le 2 \times 10^5$，用 <code>unordered_set</code> 或 <code>vector&lt;bool&gt; dist</code> 均可。我选 <code>unordered_set</code> 是因为 <code>hsh</code> 映射到整数后直接 <code>.count()</code> 很自然；若用 <code>dist</code> 数组，需要一维大小 $2mn$ 的 <code>vector&lt;bool&gt;</code> 或者 <code>bitset</code>，空间略大但常数更优，写法也很清晰。</li><li><strong>原版实现</strong>：我比赛时八个方向逐条展开写了一遍（奇数步和偶数步各写四条 <code>if</code>），逻辑冗长但最直观。上面 §8 的版本用 <code>dirs</code> 数组 + <code>legal</code> 布尔数组做了一次重构，将方向合法性判断集中到奇偶性分支中，代码行数从 60 缩到 20。两种写法的思路完全一致，重构版同样直观，思路清晰且巧妙。</li></ul>]]>
    </content>
    <id>http://ttzc.github.io/94489eec/</id>
    <link href="http://ttzc.github.io/94489eec/"/>
    <published>2026-07-30T07:00:00.000Z</published>
    <summary>本题给出 $m \times n$ 网格，每个格子有入口代价和罚金。从 $(0,0)$ 出发，第 $k$ 步移动方向由 $k$ 的奇偶性决定（奇数步只能右/下，偶数步只能左/上），违反规则或原地等待需支付罚金。分析指出朴素 DFS 因方向奇偶交替导致搜索空间巨大，进而将「位置 + 步数奇偶性」纳入状态，转化为 $2mn$ 个节点的隐式图，每条合法移动（含等待）建有权边，跑 Dijkstra 即可求解。文章详细推导了状态设计、转移规则，给出了 C++ 参考实现，并总结了 vis 标记时机、罚金归属、整数溢出等避坑要点。</summary>
    <title>LeetCode 4003 交替方向的最小路径代价 III - Solution</title>
    <updated>2026-07-30T07:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>zaochen</name>
    </author>
    <category term="题解" scheme="http://ttzc.github.io/categories/solution/"/>
    <category term="组合数学" scheme="http://ttzc.github.io/tags/%E7%BB%84%E5%90%88%E6%95%B0%E5%AD%A6/"/>
    <category term="康托展开" scheme="http://ttzc.github.io/tags/%E5%BA%B7%E6%89%98%E5%B1%95%E5%BC%80/"/>
    <category term="LeetCode" scheme="http://ttzc.github.io/tags/LeetCode/"/>
    <category term="可重集的排列组合" scheme="http://ttzc.github.io/tags/%E5%8F%AF%E9%87%8D%E9%9B%86%E7%9A%84%E6%8E%92%E5%88%97%E7%BB%84%E5%90%88/"/>
    <content>
      <![CDATA[<h2 id="1-题目数据"><a class="markdownIt-Anchor" href="#1-题目数据"></a> 1. 题目数据</h2><ul><li><strong>题目类型</strong>：传统题</li><li><strong>题目链接</strong>：<a href="https://leetcode.cn/problems/smallest-palindrome-ii/">LeetCode 3518. 最小回文排列 II</a></li></ul><h2 id="2-题意简述"><a class="markdownIt-Anchor" href="#2-题意简述"></a> 2. 题意简述</h2><p>已知长度为 $n$（$1 \le n \le 10^4$）的回文字符串 $s$（由小写英文字母组成）和整数 $k$（$1 \le k \le 10^6$），求 $s$ 的所有<strong>不同</strong>回文排列按字典序排序后的第 $k$ 个排列；若排列总数不足 $k$，返回空字符串。</p><h2 id="3-朴素解法"><a class="markdownIt-Anchor" href="#3-朴素解法"></a> 3. 朴素解法</h2><p>直接生成所有回文排列、去重、排序，取第 $k$ 个。排列数最坏情况为 $\frac{(n/2)!}{\prod cnt[i]!}$，$n=10^4$ 时完全不可行。</p><h2 id="4-核心解法"><a class="markdownIt-Anchor" href="#4-核心解法"></a> 4. 核心解法</h2><h3 id="关键观察回文串的左半边决定整体"><a class="markdownIt-Anchor" href="#关键观察回文串的左半边决定整体"></a> 关键观察：回文串的左半边决定整体</h3><p>对于任意回文串，右半边完全由左半边镜像得到，中间字符（若存在）固定不变。因此，构造回文排列等价于<strong>构造左半边的字符排列</strong>。问题就此降维：<strong>求可重集合的第 $k$ 小排列</strong>。</p><p>本题是 <a href="https://leetcode.cn/problems/smallest-palindromic-rearrangement-i/?envType=daily-question&amp;envId=2026-07-29">LeetCode 3517. 最小回文排列 I</a> 的升级版。I 版只要求字典序<strong>最小</strong>的回文排列，没有 $k$ 参数：既然左半边决定整体，只需把左半边字符按字典序从小到大排即可。II 版升级为求第 $k$ 小排列，正是本章后续要讨论的可重集合逆康托展开问题。</p><h3 id="阶段一与标准逆康托展开对比建立算法大框架"><a class="markdownIt-Anchor" href="#阶段一与标准逆康托展开对比建立算法大框架"></a> 阶段一：与标准逆康托展开对比，建立算法大框架</h3><p>在继续之前，有必要明确本题与<strong>标准逆康托展开</strong>（见 <a   href='/df80b11b/'>【组合数学】康托展开 学习笔记</a>）的关键差异。</p><p>标准逆康托展开（元素互异）的逐位决策流程是：</p><ol><li>维护一个有序的剩余数字集合 $S$（初始为 $1,2,\dots,n$）；</li><li>计算<strong>权重</strong> $t = \left\lfloor \dfrac{k}{(n-i)!} \right\rfloor$；</li><li>用数据结构（线段树 / 树状数组 / 平衡树）在 $S$ 中查第 $t+1$ 小的元素作为当前位；</li><li>更新 $k \gets k \bmod (n-i)!$，从 $S$ 中移除已选元素。</li></ol><p>核心开销在于维护动态有序集合并支持&quot;第 $K$ 小&quot;查询，因此需要 $O(\log n)$ 的数据结构。</p><p>而本题的可重集合逆康托展开，利用了两个关键性质，将流程大幅简化：</p><table><thead><tr><th style="text-align:left">维度</th><th style="text-align:left">标准逆康托（元素互异）</th><th style="text-align:left">可重集逆康托（本题）</th></tr></thead><tbody><tr><td style="text-align:left"><strong>剩余元素的组织方式</strong></td><td style="text-align:left">有序集合 $S$，需动态维护</td><td style="text-align:left">频次数组 $cnt[0…25]$，固定大小</td></tr><tr><td style="text-align:left"><strong>&quot;第 $K$ 小&quot;的获取方式</strong></td><td style="text-align:left">计算权重 $t$，用数据结构查第 $t+1$ 小</td><td style="text-align:left"><strong>直接枚举</strong> $c \in [\texttt{‘a’},\texttt{‘z’}]$，跳过 $cnt[c]=0$ 的字符</td></tr><tr><td style="text-align:left"><strong>当前分支的排列增加量</strong></td><td style="text-align:left">$(n-i)!$（全排列）</td><td style="text-align:left">$\displaystyle \binom{rest}{cnt_0}\binom{rest-cnt_0}{cnt_1}\cdots$（多重集排列）</td></tr></tbody></table><p>简言之，因为本题的&quot;值域&quot;只有 $26$ 个字母，<strong>枚举代替了查第 $K$ 小</strong>，不再需要线段树或树状数组；因为元素可重，<strong>组合数连乘代替了阶乘</strong>，作为每个分支的排列增加量。整个算法变成纯粹的&quot;枚举 + 组合计数 + 试填决策&quot;。</p><h3 id="阶段二分析两种-adds-的计算方法"><a class="markdownIt-Anchor" href="#阶段二分析两种-adds-的计算方法"></a> 阶段二：分析两种 adds 的计算方法</h3><p>有了上述框架，我们就可以把标准逆康托的&quot;计算权重 $t$“替换为本题的&quot;逐字母枚举 + 组合计数”。具体来说，对每一位：</p><ol><li>维护当前各字符的剩余频次 $cnt[0…25]$。</li><li>对每一位，从小到大枚举可选的字母 $c$。</li><li>计算&quot;若该位填 $c$，剩余 $rest$ 个位置能产生多少种合法排列&quot;，记为 $adds$。</li><li>若当前偏移量 $prk + adds \ge k$，说明第 $k$ 小排列在此分支内，选定 $c$。</li><li>否则，说明第 $k$ 小排列排在当前分支之后，执行 $prk \gets prk + adds$（跳过该分支），继续枚举下一个字母。</li></ol><p>这里 $adds$ 的含义与标准逆康托中的 $(n-i)!$ 完全相同——都是&quot;当前分支有多少种排列&quot;——但计算方法因可重集合而不同。</p><p><strong>标准逆康托（元素互异）</strong>：剩余 $rest$ 个互异元素的全排列数为 $rest!$。</p><p><strong>本题（可重集合）</strong>：给定剩余频次 $cnt[i]$ 和剩余位置 $rest = \sum cnt[i]$，多重集排列数为：</p><p>$$ adds = \frac{rest!}{\prod_{i=0}^{25} cnt[i]!} $$</p><p>直接计算阶乘不现实（$rest \le 5000$），我们将其转化为组合数连乘：</p><p>$$ adds = \binom{rest}{cnt_0} \times \binom{rest - cnt_0}{cnt_1} \times \cdots $$</p><p>每次调用时，若中间结果超过 $k$，立即返回 $k+1$（因为我们已经知道该分支的排列数足够大，不需要精确值）。</p><h3 id="阶段三确定组合数细节"><a class="markdownIt-Anchor" href="#阶段三确定组合数细节"></a> 阶段三：确定组合数细节</h3><p>现在问题归结为高效计算 $C(n, m)$，且只需判断是否超过 $k$（$k \le 10^6$）。我们使用递推公式：</p><p>$$ C(n, m) = C(n, m-1) \times \frac{n - m + 1}{m} $$</p><p>这个公式的组合意义是&quot;先乘后除&quot;：先从 $n-m+1$ 个&quot;局外人&quot;中选一个加入现有 $m-1$ 人队伍，再除以 $m$ 消除因添加顺序造成的重复计数。每一步的中间结果都是整数。</p><p>在 <code>comb</code> 函数的实现中，我们利用对称性 $m = \min(m, n-m)$ 减少循环次数，并在中间结果超过 $k$ 时立即返回 $k+1$。这既防止了 <code>long long</code> 溢出，又将常数压到最小（因为 $k \le 10^6$，循环最多 20 余次）。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">ll <span class="title">comb</span><span class="params">(ll n, ll m, <span class="type">int</span> k)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    m = <span class="built_in">min</span>(m, n - m);</span><br><span class="line">    ll res = <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= m; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        res = res * (n - i + <span class="number">1</span>) / i;</span><br><span class="line">        <span class="keyword">if</span> (res &gt; k) <span class="keyword">return</span> k + <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> res;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="5-正确性证明"><a class="markdownIt-Anchor" href="#5-正确性证明"></a> 5. 正确性证明</h2><p>需证三点：试填法的贪心决策正确、$adds$ 计算正确、截断不影响最终结果。</p><p><strong>1. 试填法的贪心决策正确。</strong> 在每一步，我们将所有字典序比当前选择小的排列按首字符分块，$adds$ 恰好是当前字符对应的块大小。若 $k$ 落在块内（即 $prk + adds \ge k$），则锁定该字符；否则跳过整个块（累加 $prk$）。由于排列按字典序连续分布，此过程最终精确锁定第 $k$ 小排列。</p><p><strong>2. $adds$ 计算正确。</strong> 给定剩余频次 $cnt[i]$ 和剩余位置 $rest$，排列总数为多重集排列公式 $adds = \dfrac{rest!}{\prod cnt[i]!}$。将其转化为组合数连乘 $adds = \binom{rest}{cnt_0} \binom{rest-cnt_0}{cnt_1} \cdots$，数学上等价，且避免了直接计算阶乘。</p><p><strong>3. 截断不影响最终结果。</strong> 我们只关心 $adds$ 是否 $\ge k$。<code>m = min(m, n - m)</code> 将 $m$ 映射到 $[0, n/2]$，由杨辉三角的对称性与单调性，$C(n, m)$ 在此区间单调递增，因此中间结果只增不减。一旦超过 $k$，后续步骤不可能回落到 $k$ 以下，截断返回 $k+1$ 不影响 $prk + adds \ge k$ 的判断。<code>calc_adds</code> 的逐项连乘截断同理。</p><p>综上所述，算法正确。</p><h2 id="6-复杂度分析"><a class="markdownIt-Anchor" href="#6-复杂度分析"></a> 6. 复杂度分析</h2><ul><li><strong>时间复杂度</strong>：$O(n \cdot |\Sigma|^2 \cdot \log k)$，其中 $|\Sigma| = 26$ 为字母表大小，$\log k \le 20$ 为 <code>comb</code> 因提前截断的实际迭代次数。由于 $|\Sigma|$ 与 $\log k$ 均为常数，可简化为 $O(n)$。左半长 $L = \lfloor n/2 \rfloor \le 5000$，每层枚举 26 个字母，每次 <code>calc_adds</code> 遍历 26 个字母并调用 <code>comb</code>（最多约 20 次）。总操作量约 $26 \times L \times 26 \times 20 \approx 2.7 \times 10^7$，在时限内。</li><li><strong>空间复杂度</strong>：$O(26)$ 存储频次，$O(1)$ 额外空间。</li></ul><h2 id="7-实现细节与避坑指南"><a class="markdownIt-Anchor" href="#7-实现细节与避坑指南"></a> 7. 实现细节与避坑指南</h2><ul><li><strong>组合数提前截断</strong>：<code>comb(n, m, k)</code> 在中间结果超过 $k$ 时立即返回 $k+1$。这既防止了 <code>long long</code> 溢出，又将常数压到最小（因为 $k \le 10^6$，循环最多 20 余次）。</li><li><strong>先乘后除</strong>：<code>C(n,m)</code> 的递推实现中，<code>res = res * (n-i+1) / i</code> 保证每一步都是整数，避免浮点误差。</li><li><strong>0-index 与 1-index</strong>：字符串预处理时加前导空格转为 1-indexed，简化中间字符的下标计算 <code>(n+1)/2</code>。</li><li><strong>无解判断</strong>：若左半边未填满（<code>res.length() &lt; n/2</code>），说明排列总数不足 $k$，直接返回空串。</li><li><strong>中间字符固定</strong>：回文排列的中间字符（若存在，即原字符串长度是奇数）必须与原字符串一致，代码中直接取 <code>s[(n+1)/2]</code>。</li></ul><h2 id="8-参考代码"><a class="markdownIt-Anchor" href="#8-参考代码"></a> 8. 参考代码</h2><p>我提交时使用的版本，采用「试填法 + 组合数截断」的思路，在 LeetCode 上已通过。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span> &#123;</span><br><span class="line">    <span class="keyword">using</span> ll = <span class="type">long</span> <span class="type">long</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 计算 C(n, m)，若结果 &gt; k 则提前返回 k + 1</span></span><br><span class="line">    <span class="function">ll <span class="title">comb</span><span class="params">(ll n, ll m, <span class="type">int</span> k)</span></span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        m = <span class="built_in">min</span>(m, n - m);</span><br><span class="line">        ll res = <span class="number">1</span>;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= m; i++)</span><br><span class="line">        &#123;</span><br><span class="line">            res = res * (n - i + <span class="number">1</span>) / i;</span><br><span class="line">            <span class="keyword">if</span> (res &gt; k) <span class="keyword">return</span> k + <span class="number">1</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> res;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">    <span class="function">string <span class="title">smallestPalindrome</span><span class="params">(string s, <span class="type">int</span> k)</span></span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="type">int</span> n = s.<span class="built_in">length</span>();</span><br><span class="line">        <span class="function">vector&lt;<span class="type">int</span>&gt; <span class="title">cnt</span><span class="params">(<span class="number">26</span>, <span class="number">0</span>)</span></span>;</span><br><span class="line">        s = <span class="string">&quot; &quot;</span> + s; <span class="comment">// 转为 1-indexed，方便取中间字符</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">// 仅统计左半边的字符频次（回文串右半边由左半边镜像决定）</span></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= n / <span class="number">2</span>; i++)</span><br><span class="line">            cnt[s[i] - <span class="string">&#x27;a&#x27;</span>]++;</span><br><span class="line"></span><br><span class="line">        string res = <span class="string">&quot;&quot;</span>;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 计算在当前频次下，剩余 rest 个位置能产生的排列数</span></span><br><span class="line">        <span class="keyword">auto</span> calc_adds = [&amp;](<span class="type">int</span> rest) -&gt; ll</span><br><span class="line">        &#123;</span><br><span class="line">            ll adds = <span class="number">1</span>;</span><br><span class="line">            <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; <span class="number">26</span>; i++)</span><br><span class="line">            &#123;</span><br><span class="line">                adds *= <span class="built_in">comb</span>(rest, cnt[i], k);</span><br><span class="line">                <span class="keyword">if</span> (adds &gt; k) <span class="keyword">return</span> k + <span class="number">1LL</span>;</span><br><span class="line">                rest -= cnt[i];</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> adds;</span><br><span class="line">        &#125;;</span><br><span class="line"></span><br><span class="line">        ll prk = <span class="number">0</span>; <span class="comment">// 偏移量：前面已经跳过的排列总数</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">// 逐位试填左半边</span></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= n / <span class="number">2</span>; i++)</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">for</span> (<span class="type">int</span> j = <span class="number">0</span>; j &lt; <span class="number">26</span>; j++)</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="keyword">if</span> (!cnt[j]) <span class="keyword">continue</span>;</span><br><span class="line"></span><br><span class="line">                cnt[j]--;</span><br><span class="line">                ll adds = <span class="built_in">calc_adds</span>(n / <span class="number">2</span> - i);</span><br><span class="line"></span><br><span class="line">                <span class="comment">// 若当前偏移 + 该分支的排列数 &gt;= k，说明第 k 小排列在此分支内</span></span><br><span class="line">                <span class="keyword">if</span> (prk + adds &gt;= k)</span><br><span class="line">                &#123;</span><br><span class="line">                    res += <span class="built_in">char</span>(<span class="string">&#x27;a&#x27;</span> + j);</span><br><span class="line">                    <span class="keyword">break</span>;</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                cnt[j]++;</span><br><span class="line">                prk += adds; <span class="comment">// 跳过当前分支，累加偏移量</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 左半边未填满，说明排列总数不足 k</span></span><br><span class="line">        <span class="keyword">if</span> (res.<span class="built_in">length</span>() &lt; n / <span class="number">2</span>) <span class="keyword">return</span> <span class="string">&quot;&quot;</span>;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 奇数长度需补中间字符，再镜像右半边</span></span><br><span class="line">        <span class="keyword">if</span> (n &amp; <span class="number">1</span>)</span><br><span class="line">            <span class="keyword">return</span> res + s[(n + <span class="number">1</span>) / <span class="number">2</span>] + <span class="built_in">string</span>(res.<span class="built_in">rbegin</span>(), res.<span class="built_in">rend</span>());</span><br><span class="line">        <span class="keyword">return</span> res + <span class="built_in">string</span>(res.<span class="built_in">rbegin</span>(), res.<span class="built_in">rend</span>());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="9-补充说明"><a class="markdownIt-Anchor" href="#9-补充说明"></a> 9. 补充说明</h2><p>本题是<strong>可重集合逆康托展开</strong>的经典应用。更一般地，若题目要求&quot;求第 $k$ 小排列&quot;且元素可重，均可用此框架：逐位枚举、组合计数、试填决策。方法经典，具有学习的价值。</p><p>我在写这篇题解时，正是先把可重集合的排列公式 $\frac{rest!}{\prod cnt[i]!}$ 拆成组合数连乘，才真正理解了为什么官方题解用 <code>comb</code> 来计算 $adds$——这个转化是整个算法复杂度正确的基础。</p><p>本题是 LeetCode 每日一题，思路清晰且实现友好。试填法的框架一旦建立，代码量很小，主要工作量在组合数的截断优化上。</p>]]>
    </content>
    <id>http://ttzc.github.io/26daeb5a/</id>
    <link href="http://ttzc.github.io/26daeb5a/"/>
    <published>2026-07-29T12:45:00.000Z</published>
    <summary>利用康托展开 + 可重集排列计数，在 O(n·26·log n) 内求回文串所有不同排列按字典序排序后的第 k 个，通过剪枝提前判断无解情况。</summary>
    <title>LeetCode 3518 最小回文排列 II - Solution</title>
    <updated>2026-07-29T12:45:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>zaochen</name>
    </author>
    <category term="学习笔记" scheme="http://ttzc.github.io/categories/study-notes/"/>
    <category term="组合数学" scheme="http://ttzc.github.io/tags/%E7%BB%84%E5%90%88%E6%95%B0%E5%AD%A6/"/>
    <category term="康托展开" scheme="http://ttzc.github.io/tags/%E5%BA%B7%E6%89%98%E5%B1%95%E5%BC%80/"/>
    <category term="树状数组" scheme="http://ttzc.github.io/tags/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/"/>
    <category term="数学" scheme="http://ttzc.github.io/tags/%E6%95%B0%E5%AD%A6/"/>
    <content>
      <![CDATA[<h2 id="康托展开"><a class="markdownIt-Anchor" href="#康托展开"></a> 康托展开</h2><p>将 $1…n$ 的所有排列按照字典序进行排序，某个排列的排名可以通过<strong>康托展开</strong>的方法求出。</p><p>观察排列 $2,3,1,4$ 和 $2,3,4,1$，发现第一个不同的位置是第三位，而且第一个排列的第三位比第二个小，根据字典序的性质，第一个排列的排名在第二个之前。</p><p>从这里我们也可以发现判断某个排列排名之前的排列数量的方法。对于 $2,3,4,1$ 这个排列，我们逐位分析：</p><ul><li>第一位：$2$，我们根据分类加法计数原理对所有排列进行分类：<ul><li>如果一个排列的第一位是 $1$，则后面三位可以任意排列，有 $3!$ 种情况。</li><li>如果一个排列的第一位是 $3,4$，显然后面如何排列都不满足条件。</li><li>如果一个排列的第一位是 $2$，分为下一类。</li></ul></li><li>第二位：$3$，我们再对第一位是 $2$ 的排列进行分类：<ul><li>第二位是 $1$，后面的两个数可以任意排列，有 $2!$ 种情况。</li><li>第二位是 $4$，显然后面如何排列都不满足条件。</li><li>第二位是 $3$，分为下一类。</li></ul></li><li>第三位：$4$：我们对前两位是 $2,3$ 的排列进行分类：<ul><li>第三位是 $1$ 对答案产生 $1!$ 的贡献。</li><li>第三位是 $4$ 则是排列 $2,3,4,1$ 本身。</li></ul></li></ul><p>所以，最终的答案就是 $1<em>3! + 1</em>2! + 1*1! = 9$，有 $9$ 个排列的字典序比这个排列小，所以这个排列的排名是 $10$。</p><p>由此可得比某排列的字典序小的排列数量：<br />$$\sum_{i=1}^{n}(c_{a[i]} \times (n-i)!)$$</p><p>其中，$c_{a[i]}=\sum_{j=i}^n [a[j]&lt;a[i]]$，表示第 $i$ 个数后面比 $a[i]$ 小的数，可以用树状数组计算，见 <a   href='/41e5d11e/#树状数组与逆序对'>【数据结构】树状数组 学习笔记</a>。把所有字典序比给定排列小的排列按 $i$ 进行分类，$(c_{a[i]} \times (n-i)!)$ 表示排列的前 $i-1$ 项和给定排列完全相同时，字典序小的排列的数目。这样分类是不重不漏的，因此求和便得到答案。</p><p>参考代码：</p><p><a   href='/acd7b679/'>代码模板-康托展开</a></p><h2 id="逆康托展开"><a class="markdownIt-Anchor" href="#逆康托展开"></a> 逆康托展开</h2><p>由排名反推原排列，这就是<strong>逆康托展开</strong>要解决的问题。它与康托展开互为逆运算，本质上是<strong>分类加法计数原理</strong>的逆向使用——我们已知被“跳过”的排列总数，现在要反推出每一位具体是什么数字。</p><p>我们继续沿用排列 $2,3,4,1$（$n=4$）这个例子。已知它的排名是 $10$，即比它小的排列有 $9$ 个（记 $k=9$）。现在我们手里只有 $k=9$，要还原出 $2,3,4,1$，依然采用逐位分析：</p><ul><li><p>第一位：对于 $1,2,3,4$ 这 $4$ 个数，以每个数字开头的排列各有 $3! = 6$ 种。</p><ul><li>我们用 $k = 9$ 除以 $3!$，得到商 $1$，余数 $3$。</li><li>这个商 $1$ 表示：第一位被跳过了 1 个数字（即数字 $1$ 开头的所有 $6$ 种情况），所以第一位应该是初始数字中的第 $2$ 小，即 $2$。选定第一位后，令 $k$ 更新为余数 $3$。</li><li>后续分析只需在剩余数字 ${1,3,4}$ 中进行</li></ul></li><li><p>第二位：固定第一位后，在剩余数字中，以某个数作为第二位的排列各有 $2! = 2$ 种。</p><ul><li>用 $k = 3$ 除以 $2!$，得到商 $1$，余数 $1$。</li><li>商 $1$ 表示第二位的选择跳过了 $1$ 个数字（即跳过最小的数字 $1$），所以第二位应取剩余数字中的第 $2$ 小，即 $3$。更新 $k = 1$。</li><li>后续分析只需在剩余数字 ${1,4}$ 中进行</li></ul></li><li><p>第三位：在剩余数字 ${1,4}$ 中，固定第三位后，第四位只有 $1! = 1$ 种排法。</p><ul><li>用 $k = 1$ 除以 $1!$，得到商 $1$，余数 $0$。</li><li>商 $1$ 表示第三位的选择跳过了 $1$ 个数字（即跳过最小的数字 $1$），所以第三位取剩余数字中的第 $2$ 小，即 $4$。更新 $k = 0$。</li></ul></li><li><p>第四位：此时只剩数字 $1$，且 $k=0$ ，所以应当取用 $1$，还原完毕，得到排列 $2,3,4,1$。</p></li></ul><hr /><p>由此，我们可以总结出逆康托展开的通用的递推步骤：</p><ol><li>给定排名，求得 $k$（即“比它小的排列个数”）。</li><li>维护一个有序的剩余数字集合 $S$（初始为 $1, 2, …, n$）。</li><li>从第 $1$ 位到第 $n$ 位依次确定：<ul><li>计算当前剩余数字的排列数，即 $(n-i)!$。</li><li>计算 <strong>权重</strong> $t = \left\lfloor \dfrac{k}{(n-i)!} \right\rfloor$。</li><li>在有序集合 $S$ 中，选取第 $t+1$ 小的数字作为当前位的 $a[i]$，因为 $t$ 表示跳过了 $t$ 个比它小的数字），所以排列的排名（比他小的排列的个数）会增加 $t*(n-i)!$。</li><li>若选择更大（小）的数字，则以此为前缀的任意排列的字典序都比最早要求的 $k$ 更小（大）。</li><li>从 $S$ 中移除 $a[i]$。</li><li>更新 $k = k \bmod (n-i)!$（即余数留给后续位使用）。</li></ul></li></ol><p>这个步骤本质上是在反复做<strong>带余除法</strong>：每一位的商 $t$ 指明了该位在剩余可选数字中的“偏移量”，而余数则传递给下一位继续分解。最终，$k$ 会被分解为唯一的阶乘数系表示（即阶乘进制），而这个进制表示恰好对应着原排列的每一位选择。</p><p>在算法实现中，维护动态有序集合 $S$ 并快速查询第 $t+1$ 小元素，通常可以使用<strong>线段树二分</strong>、<strong>树状数组倍增</strong>或<strong>平衡树</strong>来完成，从而在 $O(n \log n)$ 的时间内还原出完整的排列。</p><p>参考代码：还没写。</p><h2 id="参考资料-推荐题目"><a class="markdownIt-Anchor" href="#参考资料-推荐题目"></a> 参考资料 &amp;&amp; 推荐题目</h2><ol><li><a href="https://oi-wiki.org/math/permutation/#%E6%8E%92%E5%90%8D">置换和排列 - OI Wiki</a></li></ol>]]>
    </content>
    <id>http://ttzc.github.io/df80b11b/</id>
    <link href="http://ttzc.github.io/df80b11b/"/>
    <published>2026-07-29T06:38:13.000Z</published>
    <summary>介绍康托展开与逆康托展开的原理、公式推导和代码实现，涵盖排列排名计算、第 k 个排列生成，以及配合树状数组优化的 O(n log n) 解法。</summary>
    <title>【组合数学】康托展开 学习笔记</title>
    <updated>2026-07-29T06:38:13.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>zaochen</name>
    </author>
    <category term="代码模板" scheme="http://ttzc.github.io/categories/code-template/"/>
    <category term="代码模板" scheme="http://ttzc.github.io/tags/%E4%BB%A3%E7%A0%81%E6%A8%A1%E6%9D%BF/"/>
    <category term="组合数学" scheme="http://ttzc.github.io/tags/%E7%BB%84%E5%90%88%E6%95%B0%E5%AD%A6/"/>
    <category term="康托展开" scheme="http://ttzc.github.io/tags/%E5%BA%B7%E6%89%98%E5%B1%95%E5%BC%80/"/>
    <category term="树状数组" scheme="http://ttzc.github.io/tags/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/"/>
    <content>
      <![CDATA[<h2 id="康托展开"><a class="markdownIt-Anchor" href="#康托展开"></a> 康托展开</h2><p>康托展开用于求 $1 \sim n$ 的排列在所有排列中的字典序排名，用树状数组维护未使用过的数中比当前数小的个数。预处理阶乘 $O(n)$，单次展开 $O(n \log n)$。</p><hr /><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> int long long</span></span><br><span class="line"><span class="type">const</span> <span class="type">int</span> N = <span class="number">1e6</span> + <span class="number">5</span>, mod = <span class="number">998244353</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> n, a[N];</span><br><span class="line"><span class="type">int</span> jc[N];      <span class="comment">// 阶乘表</span></span><br><span class="line"><span class="type">int</span> t[N];       <span class="comment">// 树状数组</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">add</span><span class="params">(<span class="type">int</span> id)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = id; i &lt;= n; i += (i &amp; (-i))) t[i]++;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">query</span><span class="params">(<span class="type">int</span> id)</span> </span>&#123;</span><br><span class="line">    <span class="type">int</span> ret = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = id; i; i -= (i &amp; (-i))) ret += t[i];</span><br><span class="line">    <span class="keyword">return</span> ret;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">cantor</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 预处理阶乘</span></span><br><span class="line">    jc[<span class="number">0</span>] = <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= n; i++) jc[i] = jc[i - <span class="number">1</span>] * i % mod;</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> res = <span class="number">0</span>;</span><br><span class="line">    <span class="comment">// 倒序枚举，统计 a[i] 后方比它小的数的个数</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = n; i; i--) &#123;</span><br><span class="line">        <span class="type">int</span> ca = <span class="built_in">query</span>(a[i]);           <span class="comment">// 已经出现（即已在后方）且比 a[i] 小的个数</span></span><br><span class="line">        res = (res + ca * jc[n - i]) % mod;</span><br><span class="line">        <span class="built_in">add</span>(a[i]);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// res 是比当前排列小的排列数，排名为 res + 1</span></span><br><span class="line">    <span class="keyword">return</span> res + <span class="number">1</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><div class="callout" data-callout="tip"><div class="callout-title"><div class="callout-title-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-flame"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></svg></div><div class="callout-title-inner">提示</div></div><div class="callout-content"><p>树状数组维护的是值域上每个数是否已经出现（从后往前扫描），<code>query(a[i])</code> 统计的是已经扫过的数中比 $a[i]$ 小的个数，即原公式中的 $c_{a[i]}$。</p></div></div><p>相关笔记：<a   href='/df80b11b/#康托展开'>【组合数学】康托展开 学习笔记</a></p>]]>
    </content>
    <id>http://ttzc.github.io/acd7b679/</id>
    <link href="http://ttzc.github.io/acd7b679/"/>
    <published>2026-07-28T16:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="康托展开"><a class="markdownIt-Anchor" href="#康托展开"></a> 康托展开</h2>
<p>康托展开用于求 $1 \sim n$ 的排列在所有排列中的字典序排名，用树状数组维护未使用过的数中比当前数小的个数。预处理阶乘 $O]]>
    </summary>
    <title>代码模板-康托展开</title>
    <updated>2026-07-28T16:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>zaochen</name>
    </author>
    <category term="题解" scheme="http://ttzc.github.io/categories/solution/"/>
    <category term="离散化" scheme="http://ttzc.github.io/tags/%E7%A6%BB%E6%95%A3%E5%8C%96/"/>
    <category term="贪心" scheme="http://ttzc.github.io/tags/%E8%B4%AA%E5%BF%83/"/>
    <category term="Codeforces" scheme="http://ttzc.github.io/tags/Codeforces/"/>
    <content>
      <![CDATA[<h2 id="1-题目数据-problem-metadata"><a class="markdownIt-Anchor" href="#1-题目数据-problem-metadata"></a> 1. 题目数据 (Problem Metadata)</h2><ul><li><strong>题目类型</strong>：传统题</li><li><strong>题目链接</strong>：<a href="https://codeforces.com/problemset/problem/670/C">Problem - 670C - Codeforces</a></li><li><strong>时间限制</strong>：2 秒</li><li><strong>内存限制</strong>：256 MB</li></ul><h2 id="2-题意简述-problem-summary"><a class="markdownIt-Anchor" href="#2-题意简述-problem-summary"></a> 2. 题意简述 (Problem Summary)</h2><p>给定 $n$（$1 \le n \le 2 \times 10^5$）位科学家的语言 $a_i$（$1 \le a_i \le 10^9$）及 $m$（$1 \le m \le 2 \times 10^5$）场电影，每场电影 $j$ 有配音语言 $b_j$ 与字幕语言 $c_j$（$1 \le b_j, c_j \le 10^9$，且 $b_j \neq c_j$）。所有科学家必须看同一场电影：能听懂配音者&quot;愉悦&quot;，能看懂字幕者&quot;满意&quot;。定义 $f_j = |{i : a_i = b_j}|$ 为看了场 $j$ 会愉悦的人数，$s_j = |{i : a_i = c_j}|$ 为会满意的人数。求使 $(f_j, s_j)$ 在字典序下最大（先最大化 $f_j$，再最大化 $s_j$）的电影编号 $j$（1-indexed）。</p><h2 id="3-朴素解法-brute-force"><a class="markdownIt-Anchor" href="#3-朴素解法-brute-force"></a> 3. 朴素解法 (Brute-Force)</h2><p>对每场电影 $j$，遍历全部 $n$ 位科学家逐一统计 $f_j$ 与 $s_j$，再取字典序最大。复杂度 $O(nm)$，当 $n = m = 2 \times 10^5$ 时约 $4 \times 10^{10}$ 次运算，远超时限。瓶颈在于：每评估一场电影都重复扫描全体科学家，同一语言的人数被反复统计。</p><h2 id="4-核心解法-main-solution"><a class="markdownIt-Anchor" href="#4-核心解法-main-solution"></a> 4. 核心解法 (Main Solution)</h2><ul><li><strong>特殊性质</strong>：愉悦人数 $f_j$ 与满意人数 $s_j$ 只依赖于&quot;有多少科学家会说语言 $b_j$（或 $c_j$）&quot;，与科学家的排列顺序无关。因此可以预先统计每种语言的人数，之后每场电影的评估降为 $O(1)$ 查表。</li><li><strong>关键突破</strong>：语言编号高达 $10^9$，无法直接开下标数组。将所有出现过的语言（来自 $a_i, b_j, c_j$）收集、排序、去重，建立&quot;语言 $\to$ 离散编号&quot;的映射，把值域从 $10^9$ 压到 $O(n + m)$ 的连续区间。随后用 <code>cnt[离散编号]</code> 即可 $O(1)$ 查询任意语言的人数。</li><li><strong>推导过程</strong>：设离散化后语言 $x$ 对应的科学家人数为 $\text{cnt}[x]$。对每场电影 $j$，计算 $f_j = \text{cnt}[b_j]$、$s_j = \text{cnt}[c_j]$，在 $m$ 场电影中维护 $(f_j, s_j)$ 的字典序最大值即为答案。</li></ul><h2 id="5-正确性证明-proof-of-correctness"><a class="markdownIt-Anchor" href="#5-正确性证明-proof-of-correctness"></a> 5. 正确性证明 (Proof of Correctness)</h2><p>算法分两步：离散化计数 + 字典序比较。</p><p><strong>离散化的正确性</strong>：排序去重后，每个不同的原语言唯一映射到一个连续的离散编号，且 <code>query</code> 对该语言的二分查询稳定返回同一编号。因此 $\text{cnt}\left[\text{disc}(x)\right]$ 恰好等于会说语言 $x$ 的科学家人数，与直接用原编号做哈希计数完全等价。</p><p><strong>比较的正确性</strong>：遍历全部 $m$ 场电影，维护当前最优答案 $\text{ans}$。对每场电影 $i$，仅当 $(f_i, s_i)$ 在字典序意义下严格优于当前最优 $(f_{\text{ans}}, s_{\text{ans}})$ 时才更新。由于遍历了所有候选，且每次更新都保证新答案不劣于旧答案，最终 $\text{ans}$ 必为全局字典序最大。</p><p>通俗地，这就像先按&quot;愉悦人数&quot;排座次、同座次再按&quot;满意人数&quot;排座次，扫一遍把排第一的那场选出来即可，不重不漏。综上所述，算法正确。</p><h2 id="6-复杂度分析-complexity"><a class="markdownIt-Anchor" href="#6-复杂度分析-complexity"></a> 6. 复杂度分析 (Complexity)</h2><ul><li><strong>时间复杂度</strong>：离散化排序 $O((n + m) \log(n + m))$；计数与选优阶段共 $n + 2m$ 次 <code>query</code>（每次 <code>lower_bound</code> 查询 $O(\log(n + m))$）。总计 $O((n + m) \log(n + m))$。当 $n + m = 4 \times 10^5$ 时约 $10^7$ 量级，$2$ 秒时限充裕。</li><li><strong>空间复杂度</strong>：$O(n + m)$，存储语言数组、离散化数组与计数数组，远低于 256 MB。</li></ul><h2 id="7-实现细节与避坑指南-implementation-details"><a class="markdownIt-Anchor" href="#7-实现细节与避坑指南-implementation-details"></a> 7. 实现细节与避坑指南 (Implementation Details)</h2><ul><li><strong>整数范围</strong>：语言编号 $\le 10^9$，<code>int</code>（上界约 $2.1 \times 10^9$）即可容纳，计数 $\le n \le 2 \times 10^5$ 更无溢出风险，无需 <code>long long</code>。</li><li><strong>离散化去重</strong>：推荐写法 <code>if (i == 1 || lang[i] != lang[i-1])</code> 显式处理 $i = 1$（见 §8），比依赖 <code>lang[0]</code> 恰好为 $0$ 更稳妥；若改用 <code>std::unique</code> 也必须先 <code>sort</code>，且注意 <code>unique</code> 只去相邻重复。离散化后，也可以一口气把所有语言数据全部替换，减小反复二分带来的复杂度。</li><li><strong>数组大小</strong>：所有语言编号收集到 <code>lang[]</code> 共 $n + 2m$ 个，上界 $6 \times 10^5$，开 <code>3 * N</code> 即可；<code>uniq[]</code>、<code>cnt[]</code> 同规模。</li><li><strong><code>query</code> 的边界</strong>：<code>query</code> 只会被 $a_i, b_j, c_j$ 调用，而这些值都已进入离散化数组，必能查到，无需处理&quot;查不到&quot;的情况。</li><li><strong>字典序比较</strong>：严格按&quot;先比 $f$，相等再比 $s$&quot;实现，用 <code>happy &gt; bestHappy || (happy == bestHappy &amp;&amp; sat &gt; bestSat)</code> 即可；若缓存了当前最优的 <code>bestHappy/bestSat</code>，可避免每轮重复对 <code>b[ans], c[ans]</code> 做二分。</li></ul><h2 id="8-参考代码-reference-code"><a class="markdownIt-Anchor" href="#8-参考代码-reference-code"></a> 8. 参考代码 (Reference Code)</h2><p>下面给出离散化做法，直接在我很久之前写的版本上改写：保留&quot;收集语言 + 排序去重 + <code>query()</code> 二分&quot;的框架，把变量名改为有意义的形式，去重循环改用模板里的 <code>if (i == 1 || lang[i] != lang[i-1])</code> 以显式处理首元素，并在选优时缓存 <code>bestHappy/bestSat</code> 减少冗余二分。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;bits/stdc++.h&gt;</span></span></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std;</span><br><span class="line"></span><br><span class="line"><span class="type">const</span> <span class="type">int</span> N = <span class="number">2e5</span> + <span class="number">5</span>;</span><br><span class="line"><span class="type">const</span> <span class="type">int</span> M = <span class="number">3</span> * N;  <span class="comment">// 最多 n + 2m 种不同语言</span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> n, m;</span><br><span class="line"><span class="type">int</span> a[N], b[N], c[N];   <span class="comment">// a[]: 科学家语言; b[]/c[]: 电影配音/字幕语言</span></span><br><span class="line"><span class="type">int</span> lang[M];            <span class="comment">// 所有出现过的语言编号</span></span><br><span class="line"><span class="type">int</span> uniq[M];            <span class="comment">// 去重后的离散化数组</span></span><br><span class="line"><span class="type">int</span> cnt[M];             <span class="comment">// 每种语言对应的科学家人数</span></span><br><span class="line"><span class="type">int</span> d;                  <span class="comment">// uniq 的有效长度</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 查询原语言 x 的离散编号（1-indexed）</span></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">query</span><span class="params">(<span class="type">int</span> x)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">lower_bound</span>(uniq + <span class="number">1</span>, uniq + d + <span class="number">1</span>, x) - uniq;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    ios::<span class="built_in">sync_with_stdio</span>(<span class="literal">false</span>);</span><br><span class="line">    cin.<span class="built_in">tie</span>(<span class="literal">nullptr</span>);</span><br><span class="line"></span><br><span class="line">    cin &gt;&gt; n;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= n; i++) &#123;</span><br><span class="line">        cin &gt;&gt; a[i];</span><br><span class="line">        lang[i] = a[i];</span><br><span class="line">    &#125;</span><br><span class="line">    cin &gt;&gt; m;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= m; i++) &#123;</span><br><span class="line">        cin &gt;&gt; b[i];</span><br><span class="line">        lang[n + i] = b[i];</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= m; i++) &#123;</span><br><span class="line">        cin &gt;&gt; c[i];</span><br><span class="line">        lang[n + m + i] = c[i];</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 离散化：排序 + 去重</span></span><br><span class="line">    <span class="type">int</span> total = n + <span class="number">2</span> * m;</span><br><span class="line">    <span class="built_in">sort</span>(lang + <span class="number">1</span>, lang + total + <span class="number">1</span>);</span><br><span class="line">    d = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= total; i++) &#123;</span><br><span class="line">        <span class="keyword">if</span> (i == <span class="number">1</span> || lang[i] != lang[i - <span class="number">1</span>]) uniq[++d] = lang[i];</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 统计每种语言的科学家人数</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= n; i++) &#123;</span><br><span class="line">        cnt[<span class="built_in">query</span>(a[i])]++;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 选出 (愉悦人数, 满意人数) 字典序最大的电影</span></span><br><span class="line">    <span class="type">int</span> ans = <span class="number">1</span>;</span><br><span class="line">    <span class="type">int</span> bestHappy = cnt[<span class="built_in">query</span>(b[<span class="number">1</span>])], bestSat = cnt[<span class="built_in">query</span>(c[<span class="number">1</span>])];</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">2</span>; i &lt;= m; i++) &#123;</span><br><span class="line">        <span class="type">int</span> happy = cnt[<span class="built_in">query</span>(b[i])], sat = cnt[<span class="built_in">query</span>(c[i])];</span><br><span class="line">        <span class="keyword">if</span> (happy &gt; bestHappy || (happy == bestHappy &amp;&amp; sat &gt; bestSat)) &#123;</span><br><span class="line">            ans = i;</span><br><span class="line">            bestHappy = happy;</span><br><span class="line">            bestSat = sat;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    cout &lt;&lt; ans &lt;&lt; endl;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="9-补充说明-additional-notes"><a class="markdownIt-Anchor" href="#9-补充说明-additional-notes"></a> 9. 补充说明 (Additional Notes)</h2><ul><li><strong>其他数据结构</strong>：除手写离散化外，本题也能用 <code>std::map&lt;int, int&gt;</code> 统计人数（单次操作 $O(\log(n + m))$，复杂度与离散化 + <code>lower_bound</code> 相同，但省去排序去重样板），或用 <code>unordered_map</code> 降到期望 $O(n + m)$。手写离散化的优势在于常数更小、对值域压缩更显式，作为离散化套路的学习样本更合适，这也是本题被收录进离散化笔记的原因。</li><li><strong>问题定位</strong>：本题是&quot;离散化 + 计数&quot;最基础的应用，核心思想是把大值域、小数据量问题通过排序去重映射到连续下标，从而用数组替代哈希表。这是 OI 中的通用套路，值得熟练掌握。</li></ul>]]>
    </content>
    <id>http://ttzc.github.io/a9a1b994/</id>
    <link href="http://ttzc.github.io/a9a1b994/"/>
    <published>2026-07-28T04:08:28.000Z</published>
    <summary>
      <![CDATA[<h2 id="1-题目数据-problem-metadata"><a class="markdownIt-Anchor" href="#1-题目数据-problem-metadata"></a> 1. 题目数据 (Problem Metadata)</h2>
<ul>
<li>]]>
    </summary>
    <title>Codeforces 670C Cinema - Solution</title>
    <updated>2026-07-28T04:08:28.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>zaochen</name>
    </author>
    <category term="代码模板" scheme="http://ttzc.github.io/categories/code-template/"/>
    <category term="代码模板" scheme="http://ttzc.github.io/tags/%E4%BB%A3%E7%A0%81%E6%A8%A1%E6%9D%BF/"/>
    <category term="STL" scheme="http://ttzc.github.io/tags/STL/"/>
    <category term="基础算法" scheme="http://ttzc.github.io/tags/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95/"/>
    <category term="排序" scheme="http://ttzc.github.io/tags/%E6%8E%92%E5%BA%8F/"/>
    <category term="离散化" scheme="http://ttzc.github.io/tags/%E7%A6%BB%E6%95%A3%E5%8C%96/"/>
    <content>
      <![CDATA[<h2 id="数组写法"><a class="markdownIt-Anchor" href="#数组写法"></a> 数组写法</h2><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">const</span> <span class="type">int</span> N = <span class="number">1e5</span> + <span class="number">5</span>;</span><br><span class="line"><span class="type">int</span> n, m;</span><br><span class="line"><span class="type">int</span> a[N], b[N]; <span class="comment">// a[]为原数组，b[]是离散化后的数组，下标范围分别是 [1, n]，[1, m]</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">init</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="built_in">sort</span>(a + <span class="number">1</span>, a + <span class="number">1</span> + n); <span class="comment">// 先对数组排序</span></span><br><span class="line">    m = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= n; i++)</span><br><span class="line">        <span class="keyword">if</span> (i == <span class="number">1</span> || a[i] != a[i - <span class="number">1</span>])</span><br><span class="line">            b[++m] = a[i];</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 或STL: m = unique(a + 1, a + 1 + n) - a; 这种写法会丢失原数组</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">query</span><span class="params">(<span class="type">int</span> x)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">lower_bound</span>(b + <span class="number">1</span>, b + m + <span class="number">1</span>, x) - b;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="vector-写法"><a class="markdownIt-Anchor" href="#vector-写法"></a> vector 写法</h2><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// std::vector&lt;int&gt; arr;</span></span><br><span class="line"><span class="function">std::vector&lt;<span class="type">int</span>&gt; <span class="title">tmp</span><span class="params">(arr)</span></span>;  <span class="comment">// tmp 是 arr 的一个副本</span></span><br><span class="line">std::<span class="built_in">sort</span>(tmp.<span class="built_in">begin</span>(), tmp.<span class="built_in">end</span>());</span><br><span class="line">tmp.<span class="built_in">erase</span>(std::<span class="built_in">unique</span>(tmp.<span class="built_in">begin</span>(), tmp.<span class="built_in">end</span>()), tmp.<span class="built_in">end</span>());</span><br><span class="line"><span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; n; ++i)</span><br><span class="line">  arr[i] = std::<span class="built_in">lower_bound</span>(tmp.<span class="built_in">begin</span>(), tmp.<span class="built_in">end</span>(), arr[i]) - tmp.<span class="built_in">begin</span>();</span><br></pre></td></tr></table></figure><h2 id="参考资料"><a class="markdownIt-Anchor" href="#参考资料"></a> 参考资料</h2><ul><li>《算法竞赛进阶指南》，李煜东</li><li><a href="https://oi-wiki.org/misc/discrete/">离散化 - OI Wiki</a></li></ul>]]>
    </content>
    <id>http://ttzc.github.io/33b4e9d9/</id>
    <link href="http://ttzc.github.io/33b4e9d9/"/>
    <published>2026-07-28T00:36:25.743Z</published>
    <summary>提供离散化的数组与 vector 两种代码模板，包含排序去重、lower_bound 查询映射关系，适用于值域压缩场景。</summary>
    <title>代码模板-离散化</title>
    <updated>2026-07-28T00:36:25.743Z</updated>
  </entry>
  <entry>
    <author>
      <name>zaochen</name>
    </author>
    <category term="学习笔记" scheme="http://ttzc.github.io/categories/study-notes/"/>
    <category term="基础算法" scheme="http://ttzc.github.io/tags/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95/"/>
    <category term="排序" scheme="http://ttzc.github.io/tags/%E6%8E%92%E5%BA%8F/"/>
    <category term="离散化" scheme="http://ttzc.github.io/tags/%E7%A6%BB%E6%95%A3%E5%8C%96/"/>
    <category term="线性降维" scheme="http://ttzc.github.io/tags/%E7%BA%BF%E6%80%A7%E9%99%8D%E7%BB%B4/"/>
    <content>
      <![CDATA[<p>离散化是排序算法对于线性降维的一个重要应用。通常情况下，如果一组数值域很大，但是只考虑他们的大小关系，则可以通过离散化的方式将值域降维到不重复元素个数（和数组长度同数量级）。<br />比如：$[1,20,300,4234,51234,64321,114514,1919810]$ 这组数，如果只考虑他们的大小关系，则和 $[1,2,3,4,5,6,7,8]$ 等价。将数组排序后就可以用元素所在的位置代表这个元素。</p><h2 id="代码实现"><a class="markdownIt-Anchor" href="#代码实现"></a> 代码实现</h2><p><a   href='/33b4e9d9/'>代码模板-离散化</a></p><h2 id="例题"><a class="markdownIt-Anchor" href="#例题"></a> 例题</h2><ul><li><strong>Codeforces 670C Cinema</strong>：已知 $n$（$1≤n≤2×10^5$）位科学家的语言 $a_i$（$1≤a_i≤10^9$）及 m（$1≤m≤2×10^5$）场电影，每场电影 $j$ 有配音语言 $b_j$ 与字幕语言 $c_j$（$1≤b_j,c_j≤10^9$ 且 $b_j \neq c_j$），定义 $f_j = |{i : a_i = b_j}|$、$s_j = |{i : a_i = c_j}|$，求使 $(f_j, s_j)$ 字典序最大的电影编号 $j$。题解：<a   href='/a9a1b994/'>Codeforces 670C Cinema - Solution</a>.</li><li><strong>洛谷 P1955 程序自动分析</strong>：给定 $t$（$1\le t\le 10$）组相互独立的判定问题，每组含 $n$（$1\le n\le 10^5$）条形如 $x_i=x_j$（$e=1$）或 $x_i\neq x_j$（$e=0$）的约束，其中变量下标 $i,j$ 可达 $10^9$，求是否存在对变量的赋值使该组全部约束同时成立，对每组输出 <code>YES</code> 或 <code>NO</code>。题解：<a   href='/26fd5b5/'>洛谷 P1955 程序自动分析 - Solution</a></li></ul>]]>
    </content>
    <id>http://ttzc.github.io/4dac1417/</id>
    <link href="http://ttzc.github.io/4dac1417/"/>
    <published>2026-07-28T00:32:15.358Z</published>
    <summary>
      <![CDATA[<p>离散化是排序算法对于线性降维的一个重要应用。通常情况下，如果一组数值域很大，但是只考虑他们的大小关系，则可以通过离散化的方式将值域降维到不重复元素个数（和数组长度同数量级）。<br />
比如：$[1,20,300,4234,51234,64321,114514,19198]]>
    </summary>
    <title>【基础算法】离散化 学习笔记</title>
    <updated>2026-07-28T00:32:15.358Z</updated>
  </entry>
  <entry>
    <author>
      <name>zaochen</name>
    </author>
    <category term="题解" scheme="http://ttzc.github.io/categories/solution/"/>
    <category term="并查集" scheme="http://ttzc.github.io/tags/%E5%B9%B6%E6%9F%A5%E9%9B%86/"/>
    <category term="离散化" scheme="http://ttzc.github.io/tags/%E7%A6%BB%E6%95%A3%E5%8C%96/"/>
    <category term="Luogu" scheme="http://ttzc.github.io/tags/Luogu/"/>
    <category term="NOI" scheme="http://ttzc.github.io/tags/NOI/"/>
    <content>
      <![CDATA[<h2 id="1-题目数据-problem-metadata"><a class="markdownIt-Anchor" href="#1-题目数据-problem-metadata"></a> 1. 题目数据 (Problem Metadata)</h2><ul><li><strong>题目类型</strong>：传统题</li><li><strong>题目链接</strong>：<a href="https://www.luogu.com.cn/problem/P1955">P1955 [NOI2015] 程序自动分析 - 洛谷</a></li><li><strong>时间限制</strong>：2.00s</li><li><strong>内存限制</strong>：512.00MB</li></ul><h2 id="2-题意简述-problem-summary"><a class="markdownIt-Anchor" href="#2-题意简述-problem-summary"></a> 2. 题意简述 (Problem Summary)</h2><p>给定 $t$ 个相互独立的约束满足问题。每个问题包含 $n$ 条形如 $(i, j, e)$ 的约束：$e = 1$ 表示 $x_i = x_j$，$e = 0$ 表示 $x_i \neq x_j$。变量编号 $i, j$ 的范围为 $1 \le i, j \le 10^9$。对每个问题，判断是否存在一种变量赋值，使得所有约束同时满足。</p><p>输入：$t$（$1 \le t \le 10$），随后每组数据 $n$（$1 \le n \le 10^5$）及 $n$ 条约束。<br />输出：对每组数据输出一行，<code>YES</code> 表示可满足，<code>NO</code> 表示不可满足。</p><h2 id="3-朴素解法-brute-force"><a class="markdownIt-Anchor" href="#3-朴素解法-brute-force"></a> 3. 朴素解法 (Brute-Force)</h2><p>最直接的想法是枚举所有变量的赋值。但变量编号可达 $10^9$，且约束数 $n$ 最大为 $10^5$，变量个数最多可达 $2 \times 10^5$，完全枚举不可行。</p><p>另一种思路是建立约束图，用 DFS 染色检测相等关系的连通性，再逐一检查不等关系。但变量编号过大，无法直接开数组建图，且需要处理传递性，实现起来并不比并查集更简单。</p><h2 id="4-核心解法-main-solution"><a class="markdownIt-Anchor" href="#4-核心解法-main-solution"></a> 4. 核心解法 (Main Solution)</h2><h3 id="特殊性质"><a class="markdownIt-Anchor" href="#特殊性质"></a> 特殊性质</h3><p>相等关系具有传递性：若 $x_i = x_j$ 且 $x_j = x_k$，则必有 $x_i = x_k$。因此，所有相等的变量在逻辑上属于同一个等价类。不等关系则要求两个变量必须落在不同的等价类中。</p><h3 id="关键突破"><a class="markdownIt-Anchor" href="#关键突破"></a> 关键突破</h3><p>利用传递性，我们可以先用并查集（Union-Find）把所有相等约束合并，再逐一检查不等约束。若某条不等约束的两个变量已被合并到同一集合，则矛盾出现，问题不可满足。</p><p>变量编号 $i, j$ 最大为 $10^9$，无法直接作为并查集数组下标，因此需要<strong>离散化</strong>：收集每组数据中出现过的所有编号，排序后映射为 $0 \sim m-1$（或 $1 \sim m$），其中 $m \le 2n$。</p><h3 id="推导过程"><a class="markdownIt-Anchor" href="#推导过程"></a> 推导过程</h3><p>算法流程如下：</p><ol><li><strong>收集与离散化</strong>：读入所有约束，将出现的 $i, j$ 存入数组，排序去重后建立映射 $\text{id}[x]$。</li><li><strong>合并相等关系</strong>：遍历所有 $e = 1$ 的约束，将 $\text{id}[i]$ 与 $\text{id}[j]$ 合并。</li><li><strong>检查不等关系</strong>：遍历所有 $e = 0$ 的约束，若 $\text{find}(\text{id}[i]) = \text{find}(\text{id}[j])$，则矛盾，输出 <code>NO</code>。</li><li>若所有不等约束均不矛盾，输出 <code>YES</code>。</li></ol><p>下面我们证明这个流程的正确性。</p><h2 id="5-正确性证明-proof-of-correctness"><a class="markdownIt-Anchor" href="#5-正确性证明-proof-of-correctness"></a> 5. 正确性证明 (Proof of Correctness)</h2><p><strong>引理 1（并查集合并的正确性）</strong>：若 $x_i = x_j$，则经过并查集合并后，$x_i$ 与 $x_j$ 属于同一集合。并查集的合并操作满足等价关系的自反性、对称性和传递性，因此所有通过相等关系传递可达的变量最终都会被合并到同一集合。</p><p><strong>引理 2（检查的充分性）</strong>：若存在 $e = 0$ 的约束 $x_i \neq x_j$，但 $\text{find}(\text{id}[i]) = \text{find}(\text{id}[j])$，则问题无解。因为并查集合并了所有相等关系，若 $x_i$ 与 $x_j$ 在同一集合，说明 $x_i = x_j$ 可由已有相等约束推导得出，与 $x_i \neq x_j$ 矛盾。</p><p><strong>定理（算法正确）</strong>：上述算法输出 <code>YES</code> 当且仅当约束可满足。</p><p><em>证明</em>：若算法输出 <code>NO</code>，则由引理 2 可知存在矛盾，故不可满足。若算法输出 <code>YES</code>，则所有不等约束的两个变量均在不同集合。此时为每个集合分配一个唯一值（如集合的根节点编号），相等约束因同集合而自动满足，不等约束因不同集合而自动满足。因此存在可行赋值，约束可满足。综上所述，算法正确。</p><h2 id="6-复杂度分析-complexity"><a class="markdownIt-Anchor" href="#6-复杂度分析-complexity"></a> 6. 复杂度分析 (Complexity)</h2><ul><li><strong>时间复杂度</strong>：$O(n \log n)$。每组数据中，离散化排序 $O(n \log n)$，并查集操作 $O(n \alpha(m))$，其中 $\alpha$ 为反阿克曼函数，可视为常数。$n \le 10^5$ 时，$n \log n \approx 1.7 \times 10^6$，2s 内可轻松通过。</li><li><strong>空间复杂度</strong>：$O(n)$。离散化数组与并查集数组各 $O(n)$，总空间约几 MB，远低于 512 MB 限制。所以数组略微开大一点也没关系。</li></ul><h2 id="7-实现细节与避坑指南-implementation-details"><a class="markdownIt-Anchor" href="#7-实现细节与避坑指南-implementation-details"></a> 7. 实现细节与避坑指南 (Implementation Details)</h2><table><thead><tr><th style="text-align:left">坑点</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left"><strong>离散化映射</strong></td><td style="text-align:left">变量编号最大 $10^9$，必须离散化。收集时去重，映射时用 <code>lower_bound</code> 或哈希表。$m \le 2n$。</td></tr><tr><td style="text-align:left"><strong>多组数据清空</strong></td><td style="text-align:left">$t \le 10$，每组数据独立。离散化数组、并查集父数组、约束存储数组均需在每组数据开始时重新初始化。</td></tr><tr><td style="text-align:left"><strong>合并顺序</strong></td><td style="text-align:left">必须先合并所有相等关系，再检查不等关系。若先检查不等关系，会漏掉经由相等传递才产生的矛盾。</td></tr><tr><td style="text-align:left"><strong>数组下标</strong></td><td style="text-align:left">离散化后下标从 $0$ 或 $1$ 开始均可，但并查集数组大小要相应调整开大一点（约两倍），避免越界。</td></tr></tbody></table><h2 id="8-参考代码-reference-code"><a class="markdownIt-Anchor" href="#8-参考代码-reference-code"></a> 8. 参考代码 (Reference Code)</h2><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;algorithm&gt;</span></span></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std;</span><br><span class="line"></span><br><span class="line"><span class="type">const</span> <span class="type">int</span> MAXN = <span class="number">1e6</span> + <span class="number">5</span>;  <span class="comment">// 保险起见，直接开十倍</span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> t, n;</span><br><span class="line"><span class="type">int</span> fa[MAXN];</span><br><span class="line"></span><br><span class="line"><span class="comment">// z = 1 对应相等约束，存入 i1/j1，计数 a1</span></span><br><span class="line"><span class="comment">// z = 0 对应不等约束，存入 i0/j0，计数 a0</span></span><br><span class="line"><span class="type">int</span> i1[MAXN], j1[MAXN], a1;</span><br><span class="line"><span class="type">int</span> i0[MAXN], j0[MAXN], a0;</span><br><span class="line"><span class="type">int</span> a[MAXN], b[MAXN], m;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">get</span><span class="params">(<span class="type">int</span> x)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">return</span> fa[x] == x ? x : fa[x] = <span class="built_in">get</span>(fa[x]);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">query</span><span class="params">(<span class="type">int</span> x)</span> <span class="comment">// 离散化：把变量值映射为下标</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">lower_bound</span>(b, b + m, x) - b;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    ios::<span class="built_in">sync_with_stdio</span>(<span class="number">0</span>);</span><br><span class="line">    cin.<span class="built_in">tie</span>(<span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">    cin &gt;&gt; t; <span class="comment">// 多组测试数据</span></span><br><span class="line">    <span class="keyword">while</span> (t--)</span><br><span class="line">    &#123;</span><br><span class="line">        cin &gt;&gt; n;</span><br><span class="line">        m = a1 = a0 = <span class="number">0</span>; <span class="comment">// 百年 OI 一场空，不清多测见祖宗</span></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= <span class="number">2</span> * n; i++)</span><br><span class="line">            fa[i] = i;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= n; i++)</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="type">int</span> x, y, z;</span><br><span class="line">            cin &gt;&gt; x &gt;&gt; y &gt;&gt; z;</span><br><span class="line">            <span class="keyword">if</span> (z == <span class="number">0</span>)</span><br><span class="line">            &#123;</span><br><span class="line">                a0++;</span><br><span class="line">                i0[a0] = x;</span><br><span class="line">                j0[a0] = y;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> (z == <span class="number">1</span>)</span><br><span class="line">            &#123;</span><br><span class="line">                a1++;</span><br><span class="line">                i1[a1] = x;</span><br><span class="line">                j1[a1] = y;</span><br><span class="line">            &#125;</span><br><span class="line">            a[<span class="number">2</span> * i - <span class="number">1</span>] = x;</span><br><span class="line">            a[<span class="number">2</span> * i] = y;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="built_in">sort</span>(a + <span class="number">1</span>, a + <span class="number">1</span> + <span class="number">2</span> * n);</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= <span class="number">2</span> * n; i++)</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">if</span> (i == <span class="number">1</span> || a[i] != a[i - <span class="number">1</span>])</span><br><span class="line">                b[++m] = a[i];</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 先合并相等关系（a1 中存储的是 z=1 的约束）</span></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= a1; i++)</span><br><span class="line">            fa[<span class="built_in">get</span>(<span class="built_in">query</span>(i1[i]))] = <span class="built_in">get</span>(<span class="built_in">query</span>(j1[i]));</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 再检查不等关系（a0 中存储的是 z=0 的约束）</span></span><br><span class="line">        <span class="type">bool</span> flag = <span class="number">1</span>;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= a0 &amp;&amp; flag; i++)</span><br><span class="line">            flag &amp;= (<span class="built_in">get</span>(<span class="built_in">query</span>(i0[i])) != <span class="built_in">get</span>(<span class="built_in">query</span>(j0[i])));</span><br><span class="line"></span><br><span class="line">        cout &lt;&lt; (flag ? <span class="string">&quot;YES&quot;</span> : <span class="string">&quot;NO&quot;</span>) &lt;&lt; <span class="string">&#x27;\n&#x27;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="9-补充说明-additional-notes"><a class="markdownIt-Anchor" href="#9-补充说明-additional-notes"></a> 9. 补充说明 (Additional Notes)</h2><ul><li>本题来自 NOI 2015 决赛，是第一天第一题，也是近年来比较简单的一道签到题。</li><li>本题是<strong>约束满足问题</strong>（Constraint Satisfaction Problem, CSP）的最简形式，只含相等与不等两类二元约束。更一般的 CSP 允许任意定义域与任意关系约束，通常是 NP-Complete 的；但本题由于约束结构极简单，仅靠并查集的合并与查找即可在多项式时间内判定。</li><li>若把&quot;相等&quot;视为无向边、&quot;不等&quot;视为不能连通的判定，这题本质上是在问：由相等关系生成的等价类图，是否与不等关系冲突。这个视角也可以扩展到带权约束（如 $x_i - x_j \ge c$）或多元约束，那时需要改用带权并查集、差分约束或 SAT 求解器。</li><li>若约束中还包含 $x_i &lt; x_j$ 等偏序关系，则需要更复杂的数据结构（如带权并查集），但本题无需考虑。</li></ul><h3 id="其他版本"><a class="markdownIt-Anchor" href="#其他版本"></a> 其他版本</h3><p>本题的写法有很多变体。一种常见的写法是用结构体数组存储所有约束，读入时统一离散化，再分两遍处理。时间复杂度和空间复杂度与上述版本相同。</p>]]>
    </content>
    <id>http://ttzc.github.io/26fd5b5/</id>
    <link href="http://ttzc.github.io/26fd5b5/"/>
    <published>2026-07-27T14:38:38.000Z</published>
    <summary>NOI 2015 约束满足问题，利用并查集维护相等关系的传递性，离散化压缩变量编号后判定不等约束是否冲突，时间复杂度 O(n α(n))。</summary>
    <title>洛谷 P1955 程序自动分析 - Solution</title>
    <updated>2026-07-27T14:38:38.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>zaochen</name>
    </author>
    <category term="题解" scheme="http://ttzc.github.io/categories/solution/"/>
    <category term="前缀和" scheme="http://ttzc.github.io/tags/%E5%89%8D%E7%BC%80%E5%92%8C/"/>
    <category term="数学" scheme="http://ttzc.github.io/tags/%E6%95%B0%E5%AD%A6/"/>
    <category term="AtCoder" scheme="http://ttzc.github.io/tags/AtCoder/"/>
    <category term="贡献拆分" scheme="http://ttzc.github.io/tags/%E8%B4%A1%E7%8C%AE%E6%8B%86%E5%88%86/"/>
    <category term="模逆元" scheme="http://ttzc.github.io/tags/%E6%A8%A1%E9%80%86%E5%85%83/"/>
    <content>
      <![CDATA[<h2 id="1-题目数据-problem-metadata"><a class="markdownIt-Anchor" href="#1-题目数据-problem-metadata"></a> 1. 题目数据 (Problem Metadata)</h2><ul><li><strong>题目类型</strong>：传统题</li><li><strong>题目链接</strong>：<a href="https://atcoder.jp/contests/abc468/tasks/abc468_e">E - Sum of Average - AtCoder Beginner Contest 468</a></li><li><strong>时间限制</strong>：2 秒</li><li><strong>内存限制</strong>：1024 MB</li></ul><h2 id="2-题意简述-problem-summary"><a class="markdownIt-Anchor" href="#2-题意简述-problem-summary"></a> 2. 题意简述 (Problem Summary)</h2><p>给定正整数 $N$ 和长度-$N$ 的整数序列 $A=(A_1,A_2,\dots,A_N)$，定义子数组 $A_l,A_{l+1},\dots,A_r$ 的算术平均值为 $f(l,r)=\frac{1}{r-l+1}\sum_{i=l}^{r}A_i$。要求计算</p><p>$$\text{Ans}=\sum_{1\le l\le r\le N} f(l,r) \pmod{998244353}$$</p><p>其中模 $998244353$ 下的分数 $\frac{P}{Q}$（$Q\not\equiv0\pmod{998244353}$）转化为 $P\cdot Q^{-1}\pmod{998244353}$ 输出，其中 $Q^{-1}$ 表示在模意义下的逆元 。$1\le N\le 5\times10^5$，$0\le A_i&lt;998244353$。</p><h2 id="3-朴素解法-brute-force"><a class="markdownIt-Anchor" href="#3-朴素解法-brute-force"></a> 3. 朴素解法 (Brute-Force)</h2><p>枚举所有 $O(N^2)$ 个子数组，对每个子数组计算平均值并累加。$N=5\times10^5$ 时子数组数量级为 $10^{11}$，完全无法在规定时间内完成。</p><p>瓶颈在于重复计算平均值的分母。所有子数组的平均值之和可以交换求和顺序，拆分为每个元素的独立贡献，从而将问题降维。</p><h2 id="4-核心解法-main-solution"><a class="markdownIt-Anchor" href="#4-核心解法-main-solution"></a> 4. 核心解法 (Main Solution)</h2><h3 id="特殊性质"><a class="markdownIt-Anchor" href="#特殊性质"></a> 特殊性质</h3><p>平均值之和可以交换求和顺序，拆分为每个元素的独立贡献。位置 $i$ 的贡献仅由 $i$ 与 $N$ 的相对关系决定，与 $A_i$ 的具体值无关。通过逆元，平均数求和操作可以转化为对取模有分配律的加法和乘法。</p><h3 id="关键突破"><a class="markdownIt-Anchor" href="#关键突破"></a> 关键突破</h3><p>从 $O(N^2)$ 的瓶颈出发，利用&quot;每个元素的贡献可分离&quot;这一性质，将问题转化为对每个位置计算一个仅依赖下标的系数 $W_i$，最终答案为 $\text{Ans}=\sum_{i=1}^N A_i\cdot W_i$。</p><h3 id="推导过程"><a class="markdownIt-Anchor" href="#推导过程"></a> 推导过程</h3><p><strong>第 1 步：枚举区间长度，合并分子求和。</strong><br />对每个固定的长度 $k$，先计算所有长度为 $k$ 的子数组的分子之和，再乘以 $k^{-1}$ 得到该长度下所有子数组的平均值之和：</p><p>$$\frac1k\text{res}(k)=\frac{1}{k}\sum_{l=1}<sup>{N-k+1}\sum_{i=l}</sup>{l+k-1}A_i$$<br /><strong>第 2 步：固定位置 $i$，分析包含它的子数组。</strong> 对固定的 $i$ 与 $k$，满足 $l\le i\le l+k-1$ 的左端点个数为</p><p>$$cnt(i,k)=\min(k,;i,;N-i+1,;N-k+1)$$<br />该公式可由区间 $l\in[\max(1,i-k+1),,\min(i,N-k+1)]$ 的长度直接得到。</p><p>交换求和顺序，固定位置 $i$，统计 $i$ 在多少个长度为 $k$ 的子数组中出现：<br />$$\frac{1}{k}\sum_{i=1}^{N}A_i\cdot \min(k,i,N-i+1,N-k+1)$$<br />对所有 $k$ 累加即得最终答案：<br />$$\text{Ans}=\sum_{k=1}<sup>{N}\frac{1}{k}\sum_{i=1}</sup>{N}A_i\cdot \min(k,i,N-i+1,N-k+1)$$<br />再交换内外层求和，拆出每个位置 $i$ 的独立贡献 $A_i\cdot W_i$：</p><p>$$\text{Ans}=\sum_{i=1}^{N}A_i\cdot \sum_{k=1}^{N}\frac{\min(k,i,N-i+1,N-k+1)}{k}$$</p><p><strong>第 3 步：定义系数 $W_i$。</strong><br />$$W_i=\sum_{k=1}^{N}\frac{cnt(i,k)}{k}$$</p><p><strong>第 4 步：对称性简化。</strong> 注意到 $W_i=W_{N-i+1}$，且 $cnt(i,k)$ 关于 $i$ 与 $k$ 均呈&quot;三段线性&quot;结构。通过枚举 $k$，令 $tp=\min(k,N-k+1)$，可将 $W_i$ 的求和转化为</p><p>$$W_i=\sum_{k=1}^{N}\frac{\min(i,N-i+1,,tp,,N-tp+1)}{k}$$</p><p><strong>第 5 步：前缀和优化。</strong> 对枚举变量 $k$，分子部分 $\sum_{j=1}^{N}\min(j,N-j+1,tp,N-tp+1)\cdot A_j$ 可按三段拆分为：</p><ul><li>左段 $[1,tp]$：系数为 $j$，即 $\sum_{j=1}^{tp}j\cdot A_j$</li><li>中段 $[tp+1,N-tp]$：系数为 $tp$，即 $tp\cdot\sum_{j=tp+1}^{N-tp}A_j$</li><li>右段 $[N-tp+1,N]$：系数为 $N-j+1$，即 $\sum_{j=N-tp+1}^{N}(N-j+1)\cdot A_j$</li></ul><p>预处理三个前缀和数组 $f$（普通前缀和）、$g$（左段加权和）、$h$（右段加权和），即可在 $O(1)$ 内算出当前 $k$ 对应的分子值 $\text{res}(k)$。对于 $\sum_{j=1}^{tp}j\cdot A_j$ 这种加权和，我们在使用差分树状数组实现区间修改区间查询时见过，详见：<a   href='/41e5d11e/#树状数组与差分'>【数据结构】树状数组 学习笔记</a>。最终答案：<br />$$\text{Ans}=\sum_{k=1}^{N}\text{res}(k)\cdot k^{-1}\pmod{998244353}$$</p><h2 id="5-正确性证明-proof-of-correctness"><a class="markdownIt-Anchor" href="#5-正确性证明-proof-of-correctness"></a> 5. 正确性证明 (Proof of Correctness)</h2><p><strong>交换求和顺序</strong>：所有项均为有限值，且模意义下分母 $Q\not\equiv0\pmod{998244353}$，故逆元存在。由求和交换律，先对 $i$ 求和或先对 $l,r$ 求和结果相同。</p><p><strong>$cnt(i,k)$ 公式</strong>：对固定 $i,k$，左端点 $l$ 需满足 $l\le i\le l+k-1$ 且 $1\le l\le N-k+1$，即 $l\in[\max(1,i-k+1),,\min(i,N-k+1)]$。该区间长度恰为 $\min(k,i,N-i+1,N-k+1)$，证毕。</p><p><strong>前缀和公式</strong>：对枚举变量 $k$，令 $tp=\min(k,N-k+1)$。将数组按 $tp$ 分为左、中、右三段，各段系数分别为下标 $j$、$tp$、$N-j+1$。因此 $\text{res}(k)=\sum_{j=1}^{N}c_j\cdot A_j$，其中</p><p>$$c_j=\begin{cases}j &amp; j\le tp\tp &amp; tp&lt;j\le N-tp\N-j+1 &amp; j&gt;N-tp\end{cases}$$</p><p>该系数恰好等于 $\min(j,N-j+1,tp,N-tp+1)$。预处理 $g$ 与 $h$ 后，$\text{res}(k)$ 的每一段均可 $O(1)$ 合并。综上所述，整个算法正确。</p><p>通俗地，每个位置 $i$ 的贡献权重 $W_i$ 可以看作&quot;以 $i$ 为中心的对称衰减&quot;，而前缀和 $g$ 和 $h$ 分别维护了从左到右和从右到左的加权累积，使得每一轮 $k$ 的分子计算只需三次数组访问。</p><h2 id="6-复杂度分析-complexity"><a class="markdownIt-Anchor" href="#6-复杂度分析-complexity"></a> 6. 复杂度分析 (Complexity)</h2><ul><li><strong>时间复杂度</strong>：预处理三个前缀和数组 $O(N)$。主循环 $N$ 次，每次需 $O(\log MOD)$ 计算模逆元（快速幂），总计 $O(N \log MOD)$。$\log_2 998244353 \approx 30$，$N=5\times10^5$ 时约 $1.5\times10^7$ 次运算，可轻松通过。若预处理逆元数组，可降至严格 $O(N)$。</li><li><strong>空间复杂度</strong>：$O(N)$。存储原数组及三个前缀和数组，每个数组长度 $N+5$，总计约 $4\times 5\times10^5\times 8\text{ bytes}\approx 16\text{ MB}$，远低于内存限制。</li></ul><h2 id="7-实现细节与避坑指南-implementation-details"><a class="markdownIt-Anchor" href="#7-实现细节与避坑指南-implementation-details"></a> 7. 实现细节与避坑指南 (Implementation Details)</h2><ul><li><strong>模逆元</strong>：使用快速幂计算 $k^{-1}\equiv k^{998244353-2}\pmod{998244353}$。$998244353$ 是素数，逆元恒存在。</li><li><strong>整数范围</strong>：$f$ 数组存储真实前缀和（未取模），其差值 $f[N-tp]-f[tp]$ 最大约为 $N\cdot 998244353\approx 5\times10^{14}$，取模后小于 $998244353$，再乘以 $tp\le 5\times10^5$，乘积仍在 $64$ 位整数范围内，不会溢出。</li><li><strong>非负性</strong>：由于 $tp=\min(k,N-k+1)$，总有 $N-tp\ge tp$，故 $f[N-tp]-f[tp]\ge0$，无需额外处理负数取模问题。这也是我对原始前缀和不取模，并计算这个 $tp$ 的原因。</li><li><strong>奇偶分类</strong>：<ul><li>$N$ 为偶数：中间位置不存在，所有 $k$ 统一使用左段+右段+中段公式；当 $tp=N/2$ 时中段为空，跳过中间段累加。</li><li>$N$ 为奇数：中间位置 $mid=(N+1)/2$ 需要特殊处理，因为此时中段为空，需拆分为左段前缀 $g[mid-1]$ + 右段后缀 $h[mid+1]$ + 中间元素 $A_{mid}\cdot mid$。</li></ul></li><li><strong>下标习惯</strong>：数组下标从 $1$ 开始，与题目描述一致，避免 $0$ 下标带来的边界混淆。</li></ul><table><thead><tr><th style="text-align:left">坑点</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left"><strong>模逆元计算</strong></td><td style="text-align:left">快速幂每次 $O(\log MOD)$，主循环 $N$ 次，总复杂度 $O(N \log MOD)$。$\log_2 998244353 \approx 30$，$N=5\times10^5$ 时约 $1.5\times10^7$ 次运算，可轻松通过。若需严格 $O(N)$，可预处理逆元数组 <code>inv[i] = mod - mod/i * inv[mod%i] % mod</code>（$i=1…N$），$O(N)$ 初始化后每次 $O(1)$ 查询。</td></tr><tr><td style="text-align:left"><strong>中间位置处理</strong></td><td style="text-align:left">$N$ 为奇数时，$tp=(N+1)/2$ 对应中间元素，此时中段 $[tp+1,N-tp]$ 为空。代码中通过 <code>if (tp != (n+1)/2)</code> 分支避免访问空区间，否则会重复计入中间元素。</td></tr><tr><td style="text-align:left"><strong>负数取模</strong></td><td style="text-align:left"><code>f[n-tp]-f[tp]</code> 理论上非负，但 C++ 中 <code>%</code> 对负数的行为取决于编译器。为保险起见，可以在参考代码中加了 <code>if (ans &lt; 0) ans += MOD;</code> 和 <code>(pref[n - tp] - pref[tp] + MOD) % MOD</code>。实际上由于 $n-tp \ge tp$，该差值始终非负，不加也正确。</td></tr></tbody></table><h2 id="8-参考代码-reference-code"><a class="markdownIt-Anchor" href="#8-参考代码-reference-code"></a> 8. 参考代码 (Reference Code)</h2><p>基本上是我最早实现的赛时代码，采用「前缀和 $f,g,h$ + 分类讨论 $N$ 奇偶」的思路。思路完全正确，已通过 AtCoder 评测。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;bits/stdc++.h&gt;</span></span></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std;</span><br><span class="line"><span class="meta">#<span class="keyword">define</span> int long long</span></span><br><span class="line"></span><br><span class="line"><span class="type">const</span> <span class="type">int</span> N = <span class="number">5e5</span> + <span class="number">5</span>, mod = <span class="number">998244353</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> n;</span><br><span class="line"><span class="type">int</span> a[N], f[N], g[N], h[N];</span><br><span class="line"><span class="type">int</span> ans = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 快速幂求模逆元：计算 base^(mod-2) % mod</span></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">qmi</span><span class="params">(<span class="type">int</span> p, <span class="type">int</span> r)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="type">int</span> res = <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">while</span> (r)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (r &amp; <span class="number">1</span>)</span><br><span class="line">            res = res * p % mod;</span><br><span class="line">        p = p * p % mod;</span><br><span class="line">        r &gt;&gt;= <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> res;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">signed</span> <span class="title">main</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    ios::<span class="built_in">sync_with_stdio</span>(<span class="literal">false</span>);</span><br><span class="line"><span class="meta">#<span class="keyword">ifdef</span> DEBUG</span></span><br><span class="line">    <span class="type">clock_t</span> t0 = <span class="built_in">clock</span>();</span><br><span class="line">    <span class="built_in">freopen</span>(<span class="string">&quot;data.in&quot;</span>, <span class="string">&quot;r&quot;</span>, stdin);</span><br><span class="line">    <span class="built_in">freopen</span>(<span class="string">&quot;data.out&quot;</span>, <span class="string">&quot;w&quot;</span>, stdout);</span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// Don&#x27;t stop. Don&#x27;t hide. Follow the light, and you&#x27;ll find tomorrow.</span></span><br><span class="line"></span><br><span class="line">    cin &gt;&gt; n;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= n; i++)</span><br><span class="line">        cin &gt;&gt; a[i], f[i] = f[i - <span class="number">1</span>] + a[i];       <span class="comment">// 真实前缀和（未取模，避免相减后产生负数）</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= n; i++)</span><br><span class="line">        g[i] = (g[i - <span class="number">1</span>] + i * a[i]) % mod;         <span class="comment">// 左段加权前缀和</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = n; i; i--)</span><br><span class="line">        h[i] = (h[i + <span class="number">1</span>] + (n - i + <span class="number">1</span>) * a[i]) % mod; <span class="comment">// 右段加权后缀和</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> ((n &amp; <span class="number">1</span>) == <span class="number">0</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="comment">// N 为偶数：遍历所有区间长度 k，计算当前长度的贡献</span></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> k = <span class="number">1</span>; k &lt;= n; k++)</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="type">int</span> inv_k = <span class="built_in">qmi</span>(k, mod - <span class="number">2</span>);            <span class="comment">// 当前 k 的模逆元，即 1/k</span></span><br><span class="line">            <span class="type">int</span> tp = <span class="built_in">min</span>(k, n - k + <span class="number">1</span>);              <span class="comment">// tp = min(k, n-k+1)，确定当前对称段的边界</span></span><br><span class="line">            <span class="type">int</span> res = (g[tp] + h[n - tp + <span class="number">1</span>]) % mod; <span class="comment">// 左段 + 右段贡献</span></span><br><span class="line"></span><br><span class="line">            <span class="comment">// 中段贡献：当 tp &lt; n/2 时中段非空，否则中段为空跳过</span></span><br><span class="line">            <span class="keyword">if</span> (tp &lt; n / <span class="number">2</span>)</span><br><span class="line">                res = (res + (f[n - tp] - f[tp]) % mod * tp) % mod;</span><br><span class="line"></span><br><span class="line">            ans = (ans + res * inv_k) % mod;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="comment">// N 为奇数：中间位置需要单独处理</span></span><br><span class="line">        <span class="type">int</span> mid = (n + <span class="number">1</span>) / <span class="number">2</span>;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> k = <span class="number">1</span>; k &lt;= n; k++)</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="type">int</span> inv_k = <span class="built_in">qmi</span>(k, mod - <span class="number">2</span>);</span><br><span class="line">            <span class="type">int</span> tp = <span class="built_in">min</span>(k, n - k + <span class="number">1</span>);</span><br><span class="line">            <span class="type">int</span> res = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (tp == mid)</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="comment">// tp 恰好等于 mid：中段为空，拆分为左段前缀 + 右段后缀 + 中间元素</span></span><br><span class="line">                res = (g[tp - <span class="number">1</span>] + h[tp + <span class="number">1</span>]) % mod;</span><br><span class="line">                res = (res + a[tp] * tp) % mod;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">else</span></span><br><span class="line">            &#123;</span><br><span class="line">                <span class="comment">// 一般情况：左段 + 右段 + 中段</span></span><br><span class="line">                res = (g[tp] + h[n - tp + <span class="number">1</span>]) % mod;</span><br><span class="line">                res = (res + (f[n - tp] - f[tp]) % mod * tp) % mod;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            ans = (ans + res * inv_k) % mod;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    cout &lt;&lt; ans &lt;&lt; endl;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="9-补充说明-additional-notes"><a class="markdownIt-Anchor" href="#9-补充说明-additional-notes"></a> 9. 补充说明 (Additional Notes)</h2><ul><li>本题是<strong>子数组平均值和</strong>（Sum of Subarray Averages）的经典变种，核心技巧是&quot;贡献拆分 + 前缀和优化&quot;，将 $O(N^2)$ 的枚举压缩至 $O(N)$。方法经典，具有学习的价值。</li><li>若 $N$ 更大（如 $10^6$），此 $O(N)$ 算法依然高效。模逆元部分可进一步用线性预处理优化常数，但非必需。</li><li>赛时代码采用全局数组与 <code>#define int long long</code> 的典型竞赛写法，思路清晰且已通过验证。全局数组是 OI 赛制下的常见习惯，本题中主要是计算前（后）缀和利用了头（尾）默认值为 $0$；<code>#define int long long</code> 统一处理溢出问题，也是算法竞赛中常用的写法。参考代码相比赛时代码，调整了变量命名与边界条件的可读性，添加了详细注释，算法本质完全一致，也提交 AtCoder 进行测试。</li><li>本题与 <a href="https://atcoder.jp/contests/abc268/tasks/abc268_f">AtCoder ABC268 F - SST</a> 等&quot;子数组贡献拆分&quot;类题目思路一脉相承，核心都是将 $O(N^2)$ 的枚举转化为每个元素的独立系数计算。本题独特的点是需要使用具有对称性的前缀和分段实现区间求和。</li></ul>]]>
    </content>
    <id>http://ttzc.github.io/a12f0831/</id>
    <link href="http://ttzc.github.io/a12f0831/"/>
    <published>2026-07-25T16:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="1-题目数据-problem-metadata"><a class="markdownIt-Anchor" href="#1-题目数据-problem-metadata"></a> 1. 题目数据 (Problem Metadata)</h2>
<ul>
<li>]]>
    </summary>
    <title>AtCoder ABC468 E - Sum of Average - Solution</title>
    <updated>2026-07-25T16:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>zaochen</name>
    </author>
    <category term="题解" scheme="http://ttzc.github.io/categories/solution/"/>
    <category term="贪心" scheme="http://ttzc.github.io/tags/%E8%B4%AA%E5%BF%83/"/>
    <category term="字符串" scheme="http://ttzc.github.io/tags/%E5%AD%97%E7%AC%A6%E4%B8%B2/"/>
    <category term="LeetCode" scheme="http://ttzc.github.io/tags/LeetCode/"/>
    <content>
      <![CDATA[<h2 id="1-题目数据"><a class="markdownIt-Anchor" href="#1-题目数据"></a> 1. 题目数据</h2><ul><li><strong>题目类型</strong>：传统题</li><li><strong>题目链接</strong>：<a href="https://leetcode.cn/problems/count-valid-sequences/description/">4002. 统计有效序列数目 - 力扣（LeetCode）</a></li></ul><h2 id="2-题意简述"><a class="markdownIt-Anchor" href="#2-题意简述"></a> 2. 题意简述</h2><p>给定正整数 $n$ 与 $k$，求长度为 $k$ 的正整数序列 $(a_1, a_2, \dots, a_k)$ 的个数，满足 $\sum_{i=1}^k a_i = n$ 且 $\prod_{i=1}^k a_i$ 为偶数，答案对 $10^9+7$ 取模。两个序列在任意位置不同即视为不同。</p><h2 id="3-朴素解法"><a class="markdownIt-Anchor" href="#3-朴素解法"></a> 3. 朴素解法</h2><p>最直接的想法是枚举所有长度为 $k$ 的正整数序列，验证和与乘积条件。序列空间大小为 $n^k$，即使 $n, k$ 仅几十也完全不可接受。</p><h2 id="4-核心解法"><a class="markdownIt-Anchor" href="#4-核心解法"></a> 4. 核心解法</h2><p><strong>特殊性质</strong>：乘积为偶数当且仅当至少有一个元素为偶数。因此可以用&quot;总序列数&quot;减去&quot;全奇序列数&quot;。</p><p><strong>关键突破</strong>：两类计数均可通过隔板法闭式求解，无需枚举。</p><p><strong>推导过程</strong>：<br />正整数序列满足 $\sum_{i=1}^k a_i = n$ 的个数是隔板法的标准模型。将 $n$ 个不可区分的球放入 $k$ 个有标号盒子且每个盒子至少一个，等价于在 $n-1$ 个间隙中选 $k-1$ 个放隔板，共 $\binom{n-1}{k-1}$ 种方案。</p><p>接下来计算全奇序列数。若每个 $a_i$ 均为奇数，令 $a_i = 2b_i - 1$（$b_i \ge 1$），则<br />$$\sum_{i=1}^k (2b_i - 1) = n \implies \sum_{i=1}^k b_i = \frac{n+k}{2}$$<br />此方程有非负整数解当且仅当 $n+k$ 为偶数。此时再用隔板法，方案数为 $\binom{\frac{n+k}{2} - 1}{k - 1}$。若 $n+k$ 为奇数，则不存在全奇序列，对应项为 $0$。</p><p>由容斥原理，最终答案为<br />$$\text{ans} = \binom{n-1}{k-1} - \begin{cases} \binom{\frac{n+k}{2} - 1}{k-1} &amp; n \equiv k \pmod{2} \ 0 &amp; \text{otherwise} \end{cases}$$</p><h2 id="5-正确性证明"><a class="markdownIt-Anchor" href="#5-正确性证明"></a> 5. 正确性证明</h2><p><strong>总序列计数</strong>：隔板法证明 $\binom{n-1}{k-1}$ 是正整数解总数，无遗漏无重复。</p><p><strong>全奇序列计数</strong>：变换 $a_i = 2b_i - 1$ 是正整数奇序列与正整数序列之间的双射。方程 $\sum b_i = \frac{n+k}{2}$ 有解当且仅当 $n+k$ 为偶数，此时组合数给出精确计数；若无解则计数为 $0$。</p><p><strong>容斥（正难则反）</strong>：总序列可划分为&quot;至少有一个偶数&quot;和&quot;全奇数&quot;两个互斥类。由于全奇序列是总序列的子集，因此总数不小于全奇数，相减结果非负。</p><h2 id="6-复杂度分析"><a class="markdownIt-Anchor" href="#6-复杂度分析"></a> 6. 复杂度分析</h2><ul><li><strong>时间复杂度</strong>：$O(k)$ 或 $O(1)$。若预计算阶乘与逆元，单次询问可在 $O(1)$ 内完成。</li><li><strong>空间复杂度</strong>：$O(n)$ 预计算阶乘表，或 $O(1)$ 若使用 <code>math.comb</code> 等内置函数。</li></ul><h2 id="7-实现细节与避坑指南"><a class="markdownIt-Anchor" href="#7-实现细节与避坑指南"></a> 7. 实现细节与避坑指南</h2><ul><li>注意及时取模，同时注意本题答案分两类，两类都要取模。</li></ul><h2 id="8-参考代码"><a class="markdownIt-Anchor" href="#8-参考代码"></a> 8. 参考代码</h2><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>:</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">countValidSequences</span>(<span class="params">self, n: <span class="built_in">int</span>, k: <span class="built_in">int</span></span>) -&gt; <span class="built_in">int</span>:</span><br><span class="line">        MOD = <span class="number">10</span>**<span class="number">9</span> + <span class="number">7</span></span><br><span class="line">        total = math.comb(n - <span class="number">1</span>, k - <span class="number">1</span>)</span><br><span class="line">        <span class="keyword">if</span> n % <span class="number">2</span> != k % <span class="number">2</span>:</span><br><span class="line">            <span class="keyword">return</span> total % MOD</span><br><span class="line">        odd = math.comb((n + k) // <span class="number">2</span> - <span class="number">1</span>, k - <span class="number">1</span>)</span><br><span class="line">        <span class="keyword">return</span> (total - odd) % MOD</span><br></pre></td></tr></table></figure><h2 id="9-补充说明"><a class="markdownIt-Anchor" href="#9-补充说明"></a> 9. 补充说明</h2><ul><li>本题是隔板法与容斥原理的经典组合应用，思路简洁但需要细心处理奇偶性条件。</li><li>若需处理多组询问，可预处理阶乘与逆元将单次查询降至 $O(1)$。</li><li>为展现核心逻辑，代码中 <code>math.comb</code> 在 Python 3.8+ 可用，LeetCode 环境已预置该模块。</li></ul>]]>
    </content>
    <id>http://ttzc.github.io/fba1a986/</id>
    <link href="http://ttzc.github.io/fba1a986/"/>
    <published>2026-07-25T16:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="1-题目数据"><a class="markdownIt-Anchor" href="#1-题目数据"></a> 1. 题目数据</h2>
<ul>
<li><strong>题目类型</strong>：传统题</li>
<li><strong>题目链接</stro]]>
    </summary>
    <title>LeetCode 4002 统计有效序列数目 - Solution</title>
    <updated>2026-07-25T16:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>zaochen</name>
    </author>
    <category term="建站笔记" scheme="http://ttzc.github.io/categories/web-notes/"/>
    <category term="Obsidian" scheme="http://ttzc.github.io/tags/Obsidian/"/>
    <category term="Hexo" scheme="http://ttzc.github.io/tags/Hexo/"/>
    <category term="GitHub_Pages" scheme="http://ttzc.github.io/tags/GitHub-Pages/"/>
    <category term="stellar" scheme="http://ttzc.github.io/tags/stellar/"/>
    <category term="KaTeX" scheme="http://ttzc.github.io/tags/KaTeX/"/>
    <content>
      <![CDATA[<p>本文记录了我把 Obsidian 笔记发布为 Hexo 静态博客的完整过程——从 7 月 23 日晚到 24 日上午，大约 12 小时，经历了三次架构迭代，踩了一堆坑，最终落地为一个简洁的方案。</p><p>如果你也想&quot;在 Obsidian 内闭环写博客&quot;，这篇文章应该能帮你少走一些弯路。</p><h2 id="起点与目标"><a class="markdownIt-Anchor" href="#起点与目标"></a> 起点与目标</h2><p>我的需求很明确：</p><ul><li><strong>Obsidian 里写</strong>，笔记带 <code>[[双链]]</code> 和 $\LaTeX$ 公式</li><li><strong>Hexo 渲染</strong>，双链变成站内链接，公式正常显示</li><li><strong>GitHub Pages 部署</strong>，一个 <code>git push</code> 上线</li><li><strong>短 URL</strong>，不要一长串日期路径</li></ul><p>已有的 Hexo 项目在 <code>D:/web_study/hexo_blog/zaochen_blog</code>，Obsidian 笔记在 <code>D:\programming_contest\cp_blog</code>，22 篇算法竞赛笔记，希望发布到 <code>_posts/算法竞赛/</code> 子文件夹。</p><h2 id="阶段一hexo-integration-pathmapping-注入"><a class="markdownIt-Anchor" href="#阶段一hexo-integration-pathmapping-注入"></a> 阶段一：Hexo Integration + pathMapping 注入</h2><h3 id="思路"><a class="markdownIt-Anchor" href="#思路"></a> 思路</h3><p>用 Obsidian 的 <a href="https://github.com/nanjo712/obsidian-hexo-integration">Hexo Integration</a> 插件做发布工具。但它的发布路径是<strong>硬编码</strong> <code>source/_posts</code>，没有子目录设置。这一阶段的主要思路来源于<a href="https://lankeren035.github.io/2026/04/04/experience/obsidian/obsidian_hexo/">这篇文章</a>。</p><p>好在 <code>data.json</code> 里有个 <code>pathMapping</code> 字段，可以把&quot;仓库内相对路径 → _posts 下的目标路径&quot;做映射。于是我写了个 <code>sync_pathmapping.py</code> 脚本，幂等地为 22 篇笔记注入 pathMapping，路由到 <code>算法竞赛/&lt;文件名&gt;.md</code>。</p><h3 id="踩坑"><a class="markdownIt-Anchor" href="#踩坑"></a> 踩坑</h3><p><strong>坑 1：hexo 命令不在 PATH</strong></p><p>Hexo Integration 插件用 <code>child_process.spawn(&quot;hexo&quot;, ...)</code> 调用 hexo，但 <code>yarn global add hexo-cli</code> 后全局 bin 目录 <code>C:\Users\ftc20\AppData\Local\Yarn\bin</code> 不在 PATH 里。追加到用户级 PATH 后解决，但需要<strong>重启 Obsidian</strong> 让 Electron 应用重新继承环境变量。</p><p><strong>坑 2：<code>[[双链]]</code> 空格截断</strong></p><p><code>hexo g</code> 直接报错：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Error: Post not found: post_link 数据结构-ST.</span><br></pre></td></tr></table></figure><p>这是个双重 bug：</p><ol><li>Hexo Integration 把 <code>[[数据结构-ST 表]]</code> 转成 <code>&#123;% post_link 数据结构-ST 表 &quot;数据结构-ST 表&quot; %&#125;</code>——slug 含空格，Nunjucks 按空格拆参数，slug 被截断成 <code>数据结构-ST</code></li><li>即使加引号，slug 仍不匹配——Hexo 实际 slug 是 <code>算法竞赛/数据结构-ST 表</code>（含子目录前缀），而插件只用了文件名</li></ol><p>我改了 <code>node_modules/hexo-backlink/index.js</code>，给 slug 加引号 + 按文件名重写路径，勉强跑通了。</p><h3 id="为什么放弃"><a class="markdownIt-Anchor" href="#为什么放弃"></a> 为什么放弃</h3><p>Hexo Integration 的 Convert/Publish 流程 + pathMapping 脚本 + hexo-backlink patch，三层 hack 叠在一起，每次新增笔记都要重跑脚本，维护成本太高。</p><h2 id="阶段二尝试-hexo-link-obsidian"><a class="markdownIt-Anchor" href="#阶段二尝试-hexo-link-obsidian"></a> 阶段二：尝试 hexo-link-obsidian</h2><h3 id="思路-2"><a class="markdownIt-Anchor" href="#思路-2"></a> 思路</h3><p>推倒重来。vault 直接设在 <code>source/_posts/</code>，写完即发布，不再需要 Convert/Publish 步骤。换用 <a href="https://github.com/moelody/hexo-link-obsidian">hexo-link-obsidian</a> 插件替代 hexo-backlink——它原生支持文件名空格、<code>![[图片]]</code> 嵌入、块引用 <code>[[笔记#标题]]</code>，听起来很完美。</p><h3 id="踩坑-2"><a class="markdownIt-Anchor" href="#踩坑-2"></a> 踩坑</h3><p><strong>致命约束</strong>：这个插件依赖 Obsidian 运行时。它通过 <code>link-info-server</code> 插件（端口 3333）向 Obsidian 请求链接解析结果。也就是说：</p><ul><li><code>hexo g</code> 时 <strong>Obsidian 必须开着</strong></li><li><code>link-info-server</code> 插件必须正常运行</li><li><code>link-to-server</code> 插件加载失败（反复尝试未解决）</li></ul><p>这让构建链路绑定了 Obsidian 进程，无法独立运行。一个静态站点生成器不应该依赖某个 GUI 应用，所以放弃了。</p><h2 id="阶段三最终架构"><a class="markdownIt-Anchor" href="#阶段三最终架构"></a> 阶段三：最终架构</h2><h3 id="关键突破"><a class="markdownIt-Anchor" href="#关键突破"></a> 关键突破</h3><p>找到了 <a href="https://github.com/airemu/hexo-filter-titlebased-link">hexo-filter-titlebased-link</a>——纯静态按<strong>文件名</strong>匹配 <code>[[双链]]</code>，<strong>不需要 Obsidian 运行时</strong>。这就是我一直想要的。</p><h3 id="技术栈"><a class="markdownIt-Anchor" href="#技术栈"></a> 技术栈</h3><table><thead><tr><th>层级</th><th>组件</th><th>作用</th></tr></thead><tbody><tr><td>写作</td><td>Obsidian（vault = <code>source/_posts/</code>）</td><td>写完即发布</td></tr><tr><td>渲染</td><td><code>hexo-renderer-markdown-it-plus</code></td><td>替代默认 marked，支持 callout / 图片尺寸 / 任务列表</td></tr><tr><td>双链</td><td><code>hexo-filter-titlebased-link</code></td><td><code>[[文件名]]</code> → 文章链接</td></tr><tr><td>URL</td><td><code>hexo-abbrlink</code></td><td>CRC32 短哈希 permalink（如 <code>/d566f658/</code>）</td></tr><tr><td>公式</td><td>KaTeX</td><td>CSS 经 stellar 主题 <code>plugins.katex.inject</code> 注入</td></tr><tr><td>主题</td><td>stellar v1.33.1</td><td></td></tr><tr><td>部署</td><td><code>hexo-deployer-git</code></td><td>→ GitHub Pages</td></tr><tr><td>统计</td><td>不蒜子 v3.6.9</td><td>注入 footer</td></tr></tbody></table><h3 id="核心配置"><a class="markdownIt-Anchor" href="#核心配置"></a> 核心配置</h3><p><code>_config.yml</code> 关键部分：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">permalink:</span> <span class="string">:abbrlink/</span></span><br><span class="line"><span class="attr">abbrlink:</span></span><br><span class="line">  <span class="attr">alg:</span> <span class="string">crc32</span></span><br><span class="line">  <span class="attr">rep:</span> <span class="string">hex</span></span><br><span class="line"></span><br><span class="line"><span class="attr">post_asset_folder:</span> <span class="literal">true</span></span><br><span class="line"><span class="attr">exclude:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">&quot;**/.obsidian/**&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">theme:</span> <span class="string">stellar</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 双链插件必须显式开启</span></span><br><span class="line"><span class="attr">titlebased_link:</span></span><br><span class="line">  <span class="attr">enable:</span> <span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># markdown-it-plus + 4 个 Obsidian 语法插件</span></span><br><span class="line"><span class="attr">markdown_it_plus:</span></span><br><span class="line">  <span class="attr">plugins:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">plugin:</span></span><br><span class="line">        <span class="attr">name:</span> <span class="string">markdown-it-task-lists</span></span><br><span class="line">        <span class="attr">enable:</span> <span class="literal">true</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">plugin:</span></span><br><span class="line">        <span class="attr">name:</span> <span class="string">markdown-it-obsidian-callouts</span></span><br><span class="line">        <span class="attr">enable:</span> <span class="literal">true</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">plugin:</span></span><br><span class="line">        <span class="attr">name:</span> <span class="string">markdown-it-obsidian-imgsize</span></span><br><span class="line">        <span class="attr">enable:</span> <span class="literal">true</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">plugin:</span></span><br><span class="line">        <span class="attr">name:</span> <span class="string">markdown-it-obsidian-images</span></span><br><span class="line">        <span class="attr">enable:</span> <span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="attr">deploy:</span></span><br><span class="line">  <span class="attr">type:</span> <span class="string">git</span></span><br><span class="line">  <span class="attr">repo:</span> <span class="string">git@github.com:ttzc/ttzc.github.io.git</span></span><br><span class="line">  <span class="attr">branch:</span> <span class="string">gh-pages</span></span><br><span class="line">  <span class="attr">message:</span> <span class="string">&quot;Site updated at <span class="template-variable">&#123;&#123; now(&#x27;yyyy-MM-dd HH:mm:ss&#x27;) &#125;&#125;</span>&quot;</span></span><br></pre></td></tr></table></figure><p><code>_config.stellar.yml</code> 的 KaTeX 配置：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">plugins:</span></span><br><span class="line">  <span class="attr">katex:</span></span><br><span class="line">    <span class="attr">enable:</span> <span class="literal">true</span></span><br><span class="line">    <span class="attr">inject:</span> <span class="string">|</span></span><br><span class="line">      <span class="string">&lt;link</span> <span class="string">rel=&quot;stylesheet&quot;</span> <span class="string">href=&quot;https://gcore.jsdelivr.net/npm/katex@0.16/dist/katex.min.css&quot;&gt;</span></span><br></pre></td></tr></table></figure><h3 id="收尾阶段踩的坑"><a class="markdownIt-Anchor" href="#收尾阶段踩的坑"></a> 收尾阶段踩的坑</h3><p><strong>坑 3：双链不渲染</strong></p><p><code>hexo-filter-titlebased-link</code> 默认 <code>enable: false</code>，必须在 <code>_config.yml</code> 里显式写 <code>titlebased_link: enable: true</code>。看了源码才发现这个默认值。</p><p><strong>坑 4：KaTeX 公式显示两遍</strong></p><p>每一篇算法笔记，公式既显示了渲染结果又显示了原始 $\LaTeX$ 源码。两个原因叠加：</p><ol><li>stellar 主题的 <code>katex</code> 配置要放在 <code>plugins:</code> 父级下，不能放顶层</li><li><code>inject</code> 里的 <code>integrity</code> 哈希与 CDN 文件不匹配，浏览器<strong>静默拒绝</strong>加载 CSS，导致 <code>.katex-mathml</code> 没被隐藏</li></ol><p>去掉 <code>integrity</code> 和 <code>crossorigin</code> 属性后解决。SRI 失效是 CSS 静默不加载的常见原因，调试时优先怀疑。</p><p><strong>坑 5：deploy 提交信息乱码</strong></p><p><code>hexo d</code> 的提交信息里日期变成了字面量 <code>YYYY-MM-DD</code>。原因是 Hexo 用 luxon 格式化日期，luxon 不识别大写 <code>YYYY-MM-DD</code>，改为小写 <code>yyyy-MM-dd HH:mm:ss</code> 后正常。</p><p><strong>坑 6：不蒜子标签过时</strong></p><p>旧教程里的不蒜子标签是 <code>busuanzi_value_site_pv</code>，但新版（v3.6.9）已经改成了 <code>busuanzi_site_pv</code>，CDN 也从 <code>busuanzi.ibruce.info</code> 换成了 <code>cdn.busuanzi.cc</code>。照着旧教程抄就不会显示数字。</p><p><strong>坑 7：LeanCloud 停服</strong></p><p>stellar 主题默认的访问量统计用 LeanCloud，但 LeanCloud 国际版 2027-01-12 要停服了，所以改用不蒜子——纯前端 JS，不需要后端。</p><h2 id="日常发布流程"><a class="markdownIt-Anchor" href="#日常发布流程"></a> 日常发布流程</h2><figure class="highlight bat"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> /d D:\web_study\hexo_blog\zaochen_blog</span><br><span class="line">hexo clean &amp;&amp; hexo g &amp;&amp; hexo d    :: 部署站点</span><br><span class="line">git add -A &amp;&amp; git commit &amp;&amp; git push  :: 提交源码</span><br></pre></td></tr></table></figure><p>站点地址：<a href="https://ttzc.github.io">https://ttzc.github.io</a></p><h2 id="复盘哪些弯路值得走"><a class="markdownIt-Anchor" href="#复盘哪些弯路值得走"></a> 复盘：哪些弯路值得走</h2><p>回头看，阶段一和阶段二都走了弯路，但我不觉得是浪费时间：</p><ul><li><strong>阶段一</strong>让我理解了 Hexo 的 <code>post_link</code> 机制和 Nunjucks 模板引擎的参数解析逻辑</li><li><strong>阶段二</strong>让我明确了&quot;构建链路不应该依赖 GUI 应用&quot;这个原则</li><li><strong>阶段三</strong>的最终方案简洁到只有一层配置，正是因为前两个阶段排除了错误选项</li></ul><div class="callout" data-callout="tip"><div class="callout-title"><div class="callout-title-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-flame"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></svg></div><div class="callout-title-inner">经验</div></div><div class="callout-content"><p>遇到插件不工作时，先看源码再看文档。<code>hexo-filter-titlebased-link</code> 的 <code>enable: false</code> 默认值、hexo-backlink 的空格截断 bug，都是看源码才发现的。</p></div></div><h2 id="待办"><a class="markdownIt-Anchor" href="#待办"></a> 待办</h2><ul class="contains-task-list"><li class="task-list-item"><input class="task-list-item-checkbox" checked="" disabled="" type="checkbox"> Callout CSS（<code>&gt; [!note]</code> 样式）</li><li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox"> 考虑加 Giscus 评论系统</li></ul>]]>
    </content>
    <id>http://ttzc.github.io/d566f658/</id>
    <link href="http://ttzc.github.io/d566f658/"/>
    <published>2026-07-23T16:00:00.000Z</published>
    <summary>记录从零搭建 Hexo + Stellar 博客的完整过程，涵盖三次架构迭代、Obsidian 双链渲染、KaTeX 公式、favicon 配置、busuanzi 访问统计等踩坑经验与最终落地方案。</summary>
    <title>从 Obsidian 到 Hexo：本博客网站搭建复盘</title>
    <updated>2026-07-26T16:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>zaochen</name>
    </author>
    <category term="题解" scheme="http://ttzc.github.io/categories/solution/"/>
    <category term="ST表" scheme="http://ttzc.github.io/tags/ST%E8%A1%A8/"/>
    <category term="贪心" scheme="http://ttzc.github.io/tags/%E8%B4%AA%E5%BF%83/"/>
    <category term="字符串" scheme="http://ttzc.github.io/tags/%E5%AD%97%E7%AC%A6%E4%B8%B2/"/>
    <category term="LeetCode" scheme="http://ttzc.github.io/tags/LeetCode/"/>
    <content>
      <![CDATA[<h2 id="1-题目数据-problem-metadata"><a class="markdownIt-Anchor" href="#1-题目数据-problem-metadata"></a> 1. 题目数据 (Problem Metadata)</h2><ul><li><strong>题目类型</strong>：传统题</li><li><strong>题目链接</strong>：<a href="https://leetcode.cn/problems/maximize-active-section-with-trade-ii/">3501. 操作后最大活跃区段数 II - 力扣（LeetCode）</a></li></ul><h2 id="2-题意简述-problem-summary"><a class="markdownIt-Anchor" href="#2-题意简述-problem-summary"></a> 2. 题意简述 (Problem Summary)</h2><p>给定长度为 $n$（$1 \le n \le 10^5$）的二进制字符串 $s$，$s[i] \in {\texttt{‘0’}, \texttt{‘1’}}$，以及 $q$ 个查询（$1 \le q \le 10^5$），每个查询给出区间 $[l_i, r_i]$。对每个查询，将子串 $s[l_i…r_i]$ 两端各补一个 $\texttt{‘1’}$ 得到 $t = \texttt{‘1’} + s[l_i…r_i] + \texttt{‘1’}$，在 $t$ 上最多执行一次「交易」：</p><ol><li>选一个被 $\texttt{‘0’}$ 包围的连续 $\texttt{‘1’}$ 块，整体变 $\texttt{‘0’}$；</li><li>再选一个被 $\texttt{‘1’}$ 包围的连续 $\texttt{‘0’}$ 块，整体变 $\texttt{‘1’}$。</li></ol><p>求交易后 $s$ <strong>全体</strong>中 $\texttt{‘1’}$ 的最大数量。各查询独立，两端补的 $\texttt{‘1’}$ 不计入答案。</p><p>由 <a href="https://leetcode.cn/problems/maximize-active-section-with-trade-i/">I 版（3499）</a> 的结论，一次交易的净效果是「选两个相邻的、被 $\texttt{1}$ 包围的 $\texttt{0}$ 块，连同中间的 $\texttt{1}$ 块一起变成 $\texttt{1}$」，增量恰为两个 $\texttt{0}$ 块长度之和。因此每个查询的答案为：</p><p>$$<br />\text{ans}<em>i = \text{cnt1} + \max</em>{\text{valid pairs in } [l_i, r_i]} \big(z_L.\text{len} + z_R.\text{len}\big)<br />$$</p><p>其中 $\text{cnt1}$ 表示 $s$ 中 $\texttt{‘1’}$ 的总数。关键在于：两端的虚拟 $\texttt{‘1’}$ 使得查询边界处的 $\texttt{0}$ 块（即使被截断）也能参与交易。</p><h2 id="3-朴素解法-brute-force"><a class="markdownIt-Anchor" href="#3-朴素解法-brute-force"></a> 3. 朴素解法 (Brute-Force)</h2><p>对每个查询，提取子串 $s[l_i…r_i]$，两端补 $\texttt{‘1’}$ 后分段，枚举所有相邻 $\texttt{0}$ 块对取最大值。单次查询 $O(r_i - l_i + 1)$，总复杂度 $O(nq)$。$n = q = 10^5$ 时约 $10^{10}$ 次运算，远超时限。</p><p>瓶颈在于每个查询都重复分段和枚举。所有查询共享同一个字符串，$\texttt{0}$ 块分布是固定的，可以预处理分段信息后用区间最值数据结构加速查询。</p><h2 id="4-核心解法-main-solution"><a class="markdownIt-Anchor" href="#4-核心解法-main-solution"></a> 4. 核心解法 (Main Solution)</h2><h3 id="特殊性质"><a class="markdownIt-Anchor" href="#特殊性质"></a> 特殊性质</h3><p>交易增益只依赖两个相邻 $\texttt{0}$ 块的长度之和。查询边界的虚拟 $\texttt{‘1’}$ 允许被截断的 $\texttt{0}$ 块也能作为交易的 $\texttt{0}$ 块——虚拟 $\texttt{‘1’}$ 充当其外侧边界。因此只需在预处理好的 $\texttt{0}$ 块上做区间最值查询。</p><h3 id="关键突破"><a class="markdownIt-Anchor" href="#关键突破"></a> 关键突破</h3><p>从 $s$ 中提取所有 $\texttt{0}$ 块 $z_1, z_2, \dots, z_k$（按出现顺序），对相邻对预计算 $w_i = z_i.\text{len} + z_{i+1}.\text{len}$（$i = 1, \dots, k-1$），用稀疏表（Sparse Table）维护 $w_i$ 的区间最大值。对每个查询，二分定位查询边界处的 $\texttt{0}$ 块，分四种情况计算最大增益。</p><h3 id="推导过程"><a class="markdownIt-Anchor" href="#推导过程"></a> 推导过程</h3><p><strong>第 1 步：预处理。</strong> 将 $s$ 按连续相同字符分段，提取所有 $\texttt{0}$ 段，记为 $z_1, \dots, z_k$。每个 $z_i$ 记录起始位置 $z_i.l$、结束位置 $z_i.r$ 和长度 $z_i.\text{len}$。对相邻对建立数组 $w[1…k-1]$：</p><p>$$<br />w[i] = z_i.\text{len} + z_{i+1}.\text{len}<br />$$</p><p>对 $w$ 建立 Sparse Table 支持 $O(1)$ 区间最大值查询。</p><p><strong>第 2 步：查询定位。</strong> 对查询 $[l, r]$（转为 1-based），二分找到：</p><ul><li>$\text{fst}$：第一个 $z_i.l \ge l$ 的 $\texttt{0}$ 块；</li><li>$\text{lst}$：最后一个 $z_i.r \le r$ 的 $\texttt{0}$ 块。</li></ul><p><strong>第 3 步：分类计算增益。</strong> 定义：</p><ul><li>$\text{left}<em>0 = f_0[z</em>{\text{fst}}.l - 1] - f_0[l - 1]$，即 $[l, z_{\text{fst}}.l - 1]$ 中的 $\texttt{0}$ 数（左侧被截断的 $\texttt{0}$ 块在查询范围内的部分）；</li><li>$\text{right}<em>0 = f_0[r] - f_0[z</em>{\text{lst}}.r]$，即 $[z_{\text{lst}}.r + 1, r]$ 中的 $\texttt{0}$ 数（右侧被截断部分）。</li></ul><p>增益来源有四种（取适用的最大值）：</p><table><thead><tr><th style="text-align:left">情况</th><th style="text-align:left">条件</th><th style="text-align:left">增益公式</th><th style="text-align:left">含义</th></tr></thead><tbody><tr><td style="text-align:left">① 左侧合并</td><td style="text-align:left">$z_{\text{fst}}$ 完全在 $[l, r]$ 内，且 $\text{left}_0 &gt; 0$</td><td style="text-align:left">$\text{left}<em>0 + z</em>{\text{fst}}.\text{len}$</td><td style="text-align:left">左截断 $\texttt{0}$ + 第一个完整 $\texttt{0}$ 块</td></tr><tr><td style="text-align:left">② 右侧合并</td><td style="text-align:left">$z_{\text{lst}}$ 完全在 $[l, r]$ 内，且 $\text{right}_0 &gt; 0$</td><td style="text-align:left">$\text{right}<em>0 + z</em>{\text{lst}}.\text{len}$</td><td style="text-align:left">最后一个完整 $\texttt{0}$ 块 + 右截断 $\texttt{0}$</td></tr><tr><td style="text-align:left">③ 两侧合并</td><td style="text-align:left">$\text{left}_0, \text{right}_0 &gt; 0$ 且 $\text{left}_0 + \text{right}_0 + (区间内\ \texttt{1}\ 数) = r - l + 1$</td><td style="text-align:left">$\text{left}_0 + \text{right}_0$</td><td style="text-align:left">左右两个截断 $\texttt{0}$ 合并（无完整 $\texttt{0}$ 块）</td></tr><tr><td style="text-align:left">④ 中间合并</td><td style="text-align:left">$z_{\text{fst}}$ 和 $z_{\text{lst}}$ 均完全在区间内，且 $\text{fst} &lt; \text{lst}$</td><td style="text-align:left">$\text{ST_query}(\text{fst}, \text{lst} - 1)$</td><td style="text-align:left">中间所有完整 $\texttt{0}$ 块中相邻对之和的最大值</td></tr></tbody></table><p>最终答案 $= \text{cnt1} + \max(0, \text{四种增益的最大值})$。</p><h2 id="5-正确性证明-proof-of-correctness"><a class="markdownIt-Anchor" href="#5-正确性证明-proof-of-correctness"></a> 5. 正确性证明 (Proof of Correctness)</h2><p>需证四种情况不重不漏地覆盖了所有可能的交易。</p><p>在 $t = \texttt{‘1’} + s[l…r] + \texttt{‘1’}$ 中，一次交易选两个被 $\texttt{1}$ 包围的相邻 $\texttt{0}$ 块。这两个 $\texttt{0}$ 块在查询范围内的位置关系有以下几种：</p><p><strong>两个 $\texttt{0}$ 块都完全在 $[l, r]$ 内。</strong> 它们对应 $z_{\text{fst}}, \dots, z_{\text{lst}}$ 中的某对相邻 $\texttt{0}$ 块，增益由 Sparse Table 覆盖（情况 ④）。</p><p><strong>左 $\texttt{0}$ 块被截断（起始 $\lt l$），右 $\texttt{0}$ 块完全在区间内。</strong> 左 $\texttt{0}$ 块在区间内的部分为 $\text{left}<em>0$，虚拟 $\texttt{‘1’}$ 在其左侧充当中间 $\texttt{1}$ 块的备选边界。左截断 $\texttt{0}$ 块必定结束于 $z</em>{\text{fst}}.l - 1$ 之前（否则与 $z_{\text{fst}}$ 合并为同一块），且 $z_{\text{fst}}.l - 1$ 位置是 $\texttt{‘1’}$。因此 $\text{left}<em>0$ 个 $\texttt{0}$ 与 $z</em>{\text{fst}}$ 中的 $\texttt{0}$ 被至少一个 $\texttt{‘1’}$ 隔开，满足交易条件。增益为 $\text{left}<em>0 + z</em>{\text{fst}}.\text{len}$（情况 ①）。</p><p><strong>右 $\texttt{0}$ 块被截断，左 $\texttt{0}$ 块完全在区间内。</strong> 与上对称（情况 ②）。</p><p><strong>两个 $\texttt{0}$ 块都被截断。</strong> 此时查询区间内没有完整的 $\texttt{0}$ 块（即 $\text{fst} &gt; \text{lst}$），所有 $\texttt{0}$ 来自左右截断部分。区间内容恰好由 $\text{left}_0$ 个 $\texttt{0}$、一段 $\texttt{1}$、$\text{right}_0$ 个 $\texttt{0}$ 组成——条件 $\text{left}_0 + \text{right}_0 + (区间内\ \texttt{1}\ 数) = r - l + 1$ 恰好刻画了这一情况。若等式不成立，说明区间内存在完整 $\texttt{0}$ 块，应归入前面三种情况之一。增益为 $\text{left}_0 + \text{right}_0$（情况 ③）。</p><p>四种情况互斥（由 $\texttt{0}$ 块在区间内的位置关系决定）且完备（覆盖两个 $\texttt{0}$ 块的所有位置关系），不重不漏。最终答案在所有适用情况中取最大值加上 $\text{cnt1}$。综上所述，算法正确。</p><h2 id="6-复杂度分析-complexity"><a class="markdownIt-Anchor" href="#6-复杂度分析-complexity"></a> 6. 复杂度分析 (Complexity)</h2><ul><li><strong>时间复杂度</strong>：预处理 $O(n + k\log k)$（分段 $O(n)$，ST 表 $O(k\log k)$，$k$ 为 $\texttt{0}$ 块数，$k \le n$）。每次查询 $O(\log k)$（二分）+ $O(1)$（ST 查询）。总复杂度 $O(n \log n + q \log n)$，$n = q = 10^5$ 时约 $2 \times 10^6$ 次运算，轻松通过。</li><li><strong>空间复杂度</strong>：$O(n \log n)$（ST 表 $O(k\log k)$ + 前缀和与分段数组 $O(k)$），未超典型内存限制。</li><li><strong>常数优化</strong>：ST 表查询 $O(1)$ 已是最优；$\lg$ 数组只初始化到 $k-1$ 而非 $N$，在多次测试用例场景中可减少不必要计算。线段树替代 ST 表会多一个 $\log$ 因子，常数也更大，不必引入。</li></ul><h2 id="7-实现细节与避坑指南-implementation-details"><a class="markdownIt-Anchor" href="#7-实现细节与避坑指南-implementation-details"></a> 7. 实现细节与避坑指南 (Implementation Details)</h2><table><thead><tr><th style="text-align:left">坑点</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left"><strong>1-based 索引</strong></td><td style="text-align:left">代码在 $s$ 前补空格（<code>s = &quot; &quot; + s</code>），数组下标从 $1$ 开始。查询传入的 0-based 下标需 $+1$ 转换（<code>ql = q[0] + 1, qr = q[1] + 1</code>）。前缀和数组 <code>f0[0]</code>、<code>f1[0]</code> 为全局变量自动初始化为 $0$。</td></tr><tr><td style="text-align:left"><strong>二分边界处理</strong></td><td style="text-align:left">当 $z_r = 1$ 且唯一 $\texttt{0}$ 块不完全在 $[l, r]$ 内时，二分不执行（$cl = cr$），<code>fst_chk</code> / <code>lst_chk</code> 可能指向不在区间内的 $\texttt{0}$ 块。后续的 <code>valid_fst</code> / <code>valid_lst</code> 检查（判断该块是否完全被 $[l, r]$ 包含）会过滤此情况，确保不会错误触发增益计算。</td></tr><tr><td style="text-align:left"><strong>$z_r = 0$ 的无 $\texttt{0}$ 情况</strong></td><td style="text-align:left">当 $s$ 全为 $\texttt{‘1’}$ 时 $z_r = 0$（无 $\texttt{0}$ 块），代码未显式提前返回。此时二分搜索 $cr = 0$，$cl = cr$ 不执行循环，<code>fst_chk = 1, lst_chk = 0</code>。<code>zeros[1]</code> 为全局零初始化（<code>l = r = 0</code>），后续 <code>valid_fst</code>、<code>valid_lst</code> 均为 <code>false</code>，所有增益条件均不满足，答案退化为 <code>f1[n] = n</code>。虽然结果正确但 <code>f0[-1]</code> 一次越界访问属于 UB，建议在开头加 <code>if (zr == 0) return vector&lt;int&gt;(queries.size(), n);</code> 提前返回。首次写代码时忽略了这个问题，但是仍然通过了所有测试数据。</td></tr><tr><td style="text-align:left"><strong>ST 表建表循环边界</strong></td><td style="text-align:left">相邻对数组 $w$ 有 $k-1$ 个元素（下标 $1…k-1$）。建表时 <code>i &lt; zr</code> 恰好覆盖 $i = 1…k-1$；<code>pow2(j) &lt; zr</code> 确保 $2^j \le k-1$。查询条件 <code>fst_chk &lt; lst_chk</code> 保证 $l \le r$（<code>ST_query(fst_chk, lst_chk - 1)</code> 中 $l \le r$）。</td></tr><tr><td style="text-align:left"><strong>$\lg$ 数组初始化范围</strong></td><td style="text-align:left"><code>lg[1] = 0; for i=2..zr-1</code> 只初始化到 $k-1$。ST 查询的最大区间长度为 $k-1$，$lg[k-1]$ 已包含。标准写法可初始化到 $N$，但当前范围已足够且避免多余计算。</td></tr><tr><td style="text-align:left"><strong>前缀和数组的 <code>f0</code> 与 <code>f1</code></strong></td><td style="text-align:left"><code>f0[i]</code> = $s[1…i]$ 中 $\texttt{‘0’}$ 的个数，<code>f1[i]</code> = $\texttt{‘1’}$ 的个数。区间内 $\texttt{1}$ 数 = <code>f1[r] - f1[l-1]</code>，区间内 $\texttt{0}$ 数 = <code>f0[r] - f0[l-1]</code>。<code>f0</code> 同时用于计算截断部分的 $\texttt{0}$ 数。</td></tr><tr><td style="text-align:left"><strong>多测试用例</strong></td><td style="text-align:left">LeetCode 上全局数组不会自动清零，但本代码会覆盖所有使用到的下标（<code>f0[0..n]</code>、<code>chks[1..chk]</code>、<code>zeros[1..zr]</code>、<code>st[1..zr-1][*]</code>、<code>lg[1..zr-1]</code>），无需手动清空。事实上，在写本题前，我不太了解 Leetcode 的多测机制，调试时发现把 <code>chk</code> 和 <code>zr</code> 两个全局变量放到函数中就可以实现清零的效果。</td></tr></tbody></table><h2 id="8-参考代码-reference-code"><a class="markdownIt-Anchor" href="#8-参考代码-reference-code"></a> 8. 参考代码 (Reference Code)</h2><p>我提交时使用的版本，采用「分段 + Sparse Table + 二分定位 + 四分类增益」的思路。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> pow2(x) (1 &lt;&lt; (x))</span></span><br><span class="line"></span><br><span class="line"><span class="type">const</span> <span class="type">int</span> N = <span class="number">1e5</span> + <span class="number">5</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> f0[N], f1[N]; <span class="comment">// 前缀和：0 和 1 的个数</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">chunk</span> &#123;</span><br><span class="line">    <span class="type">bool</span> type;</span><br><span class="line">    <span class="type">int</span> l, r;</span><br><span class="line">    <span class="built_in">chunk</span>() &#123;&#125;;</span><br><span class="line">    <span class="built_in">chunk</span>(<span class="type">bool</span> _type, <span class="type">int</span> _l, <span class="type">int</span> _r) &#123;</span><br><span class="line">        type = _type;</span><br><span class="line">        l = _l;</span><br><span class="line">        r = _r;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="type">int</span> <span class="title">length</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> r - l + <span class="number">1</span>; &#125;</span><br><span class="line">&#125; chks[N], zeros[N]; <span class="comment">// 全部分段 / 仅 0 段</span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> st[N][<span class="number">20</span>], <span class="comment">// st[i][0] = zeros[i].length() + zeros[i + 1].length()</span></span><br><span class="line">    lg[N];</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">ST_query</span><span class="params">(<span class="type">int</span> l, <span class="type">int</span> r)</span> </span>&#123;</span><br><span class="line">    <span class="type">int</span> k = lg[r - l + <span class="number">1</span>];</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">max</span>(st[l][k], st[r - <span class="built_in">pow2</span>(k) + <span class="number">1</span>][k]);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span> &#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">    <span class="function">vector&lt;<span class="type">int</span>&gt; <span class="title">maxActiveSectionsAfterTrade</span><span class="params">(string s,</span></span></span><br><span class="line"><span class="params"><span class="function">                                            vector&lt;vector&lt;<span class="type">int</span>&gt;&gt;&amp; queries)</span> </span>&#123;</span><br><span class="line">        <span class="type">int</span> chk = <span class="number">0</span>, zr = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">        <span class="type">int</span> n = s.<span class="built_in">length</span>();</span><br><span class="line">        s = <span class="string">&quot; &quot;</span> + s; <span class="comment">// 转换为 1-based</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">// 预处理前缀和与分段</span></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= n; i++) &#123;</span><br><span class="line">            f0[i] = f0[i - <span class="number">1</span>] + (s[i] == <span class="string">&#x27;0&#x27;</span>);</span><br><span class="line">            f1[i] = f1[i - <span class="number">1</span>] + (s[i] == <span class="string">&#x27;1&#x27;</span>);</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (s[i] != s[i - <span class="number">1</span>])</span><br><span class="line">                chks[++chk] = <span class="built_in">chunk</span>(s[i] - <span class="string">&#x27;0&#x27;</span>, i, i);</span><br><span class="line">            <span class="keyword">else</span></span><br><span class="line">                ++chks[chk].r;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 提取 0 段</span></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= chk; i++)</span><br><span class="line">            <span class="keyword">if</span> (!chks[i].type)</span><br><span class="line">                zeros[++zr] = chks[i];</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 特判：无 0 段时没有交易可做</span></span><br><span class="line">        <span class="keyword">if</span> (zr == <span class="number">0</span>)</span><br><span class="line">            <span class="keyword">return</span> <span class="built_in">vector</span>&lt;<span class="type">int</span>&gt;(queries.<span class="built_in">size</span>(), n);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 初始化 Sparse Table</span></span><br><span class="line">        lg[<span class="number">1</span>] = <span class="number">0</span>;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">2</span>; i &lt; zr; i++)</span><br><span class="line">            lg[i] = lg[i &gt;&gt; <span class="number">1</span>] + <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt; zr; i++)</span><br><span class="line">            st[i][<span class="number">0</span>] = zeros[i].<span class="built_in">length</span>() + zeros[i + <span class="number">1</span>].<span class="built_in">length</span>();</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> j = <span class="number">1</span>; <span class="built_in">pow2</span>(j) &lt; zr; j++)</span><br><span class="line">            <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i + <span class="built_in">pow2</span>(j) - <span class="number">1</span> &lt; zr; i++)</span><br><span class="line">                st[i][j] = <span class="built_in">max</span>(st[i][j - <span class="number">1</span>],</span><br><span class="line">                               st[i + <span class="built_in">pow2</span>(j - <span class="number">1</span>)][j - <span class="number">1</span>]);</span><br><span class="line"></span><br><span class="line">        vector&lt;<span class="type">int</span>&gt; answers;</span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">auto</span>&amp; q : queries) &#123;</span><br><span class="line">            <span class="type">int</span> ql = q[<span class="number">0</span>] + <span class="number">1</span>, qr = q[<span class="number">1</span>] + <span class="number">1</span>; <span class="comment">// 转为 1-based</span></span><br><span class="line">            <span class="type">int</span> cl = <span class="number">1</span>, cr = zr;</span><br><span class="line">            <span class="type">int</span> fst_chk, lst_chk;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 二分：第一个 l &gt;= ql 的 0 段</span></span><br><span class="line">            <span class="keyword">while</span> (cl &lt; cr) &#123;</span><br><span class="line">                <span class="type">int</span> mid = (cl + cr) &gt;&gt; <span class="number">1</span>;</span><br><span class="line">                <span class="keyword">if</span> (zeros[mid].l &gt;= ql)</span><br><span class="line">                    cr = mid;</span><br><span class="line">                <span class="keyword">else</span></span><br><span class="line">                    cl = mid + <span class="number">1</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            fst_chk = cl;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 二分：最后一个 r &lt;= qr 的 0 段</span></span><br><span class="line">            cl = <span class="number">1</span>, cr = zr;</span><br><span class="line">            <span class="keyword">while</span> (cl &lt; cr) &#123;</span><br><span class="line">                <span class="type">int</span> mid = (cl + cr + <span class="number">1</span>) &gt;&gt; <span class="number">1</span>;</span><br><span class="line">                <span class="keyword">if</span> (zeros[mid].r &lt;= qr)</span><br><span class="line">                    cl = mid;</span><br><span class="line">                <span class="keyword">else</span></span><br><span class="line">                    cr = mid - <span class="number">1</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            lst_chk = cl;</span><br><span class="line"></span><br><span class="line">            <span class="type">int</span> ans = f1[n]; <span class="comment">// 基线：不交易</span></span><br><span class="line"></span><br><span class="line">            <span class="type">int</span> valid_l0 = (f0[zeros[fst_chk].l - <span class="number">1</span>] - f0[ql - <span class="number">1</span>]),</span><br><span class="line">                valid_r0 = (f0[qr] - f0[zeros[lst_chk].r]);</span><br><span class="line"></span><br><span class="line">            <span class="type">bool</span> valid_fst = zeros[fst_chk].r &lt;= qr</span><br><span class="line">                          &amp;&amp; zeros[fst_chk].l &gt;= ql,</span><br><span class="line">                 valid_lst = zeros[lst_chk].l &gt;= ql</span><br><span class="line">                          &amp;&amp; zeros[lst_chk].r &lt;= qr;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// ① 左侧截断 + 第一个完整 0 段</span></span><br><span class="line">            <span class="keyword">if</span> (valid_fst &amp;&amp; valid_l0)</span><br><span class="line">                ans = <span class="built_in">max</span>(ans, f1[n] + valid_l0</span><br><span class="line">                                 + zeros[fst_chk].<span class="built_in">length</span>());</span><br><span class="line"></span><br><span class="line">            <span class="comment">// ② 最后一个完整 0 段 + 右侧截断</span></span><br><span class="line">            <span class="keyword">if</span> (valid_lst &amp;&amp; valid_r0)</span><br><span class="line">                ans = <span class="built_in">max</span>(ans, f1[n] + valid_r0</span><br><span class="line">                                 + zeros[lst_chk].<span class="built_in">length</span>());</span><br><span class="line"></span><br><span class="line">            <span class="comment">// ③ 两侧截断合并（区间内无完整 0 段）</span></span><br><span class="line">            <span class="keyword">if</span> (valid_l0 &gt; <span class="number">0</span> &amp;&amp; valid_r0 &gt; <span class="number">0</span> &amp;&amp;</span><br><span class="line">                (f1[qr] - f1[ql - <span class="number">1</span>] + valid_l0 + valid_r0)</span><br><span class="line">                    == (qr - ql + <span class="number">1</span>))</span><br><span class="line">                ans = <span class="built_in">max</span>(ans, f1[n] + valid_l0 + valid_r0);</span><br><span class="line"></span><br><span class="line">            <span class="comment">// ④ 中间完整相邻 0 段对</span></span><br><span class="line">            <span class="keyword">if</span> (valid_fst &amp;&amp; valid_lst &amp;&amp; fst_chk &lt; lst_chk)</span><br><span class="line">                ans = <span class="built_in">max</span>(ans, f1[n]</span><br><span class="line">                              + <span class="built_in">ST_query</span>(fst_chk, lst_chk - <span class="number">1</span>));</span><br><span class="line"></span><br><span class="line">            answers.<span class="built_in">push_back</span>(ans);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> answers;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="9-补充说明-additional-notes"><a class="markdownIt-Anchor" href="#9-补充说明-additional-notes"></a> 9. 补充说明 (Additional Notes)</h2><ul><li><strong>题目背景</strong>：本题是 LeetCode 3501（难度分 2941，Hard），与 <a href="https://leetcode.cn/problems/maximize-active-section-with-trade-i/">I 版 3499</a> 同属「操作后最大活跃区段数」系列。I 版是单次查询 $O(n)$ 线性扫描；II 版引入 $q \le 10^5$ 个查询，需要 ST 表（或线段树）将单次查询降至 $O(\log n)$。两者的核心思想一脉相承：交易的净效果是合并两个相邻 $\texttt{0}$ 块。</li><li><strong>ST 表 vs 线段树</strong>：本题无修改操作，ST 表 $O(1)$ 查询优于线段树 $O(\log n)$，且常数更小、实现更短。当 $q = 10^5$ 时，ST 表的优势更加明显。线段树实现可作为替代方案，思路相同：维护区间内相邻 $\texttt{0}$ 块长度之和的最大值，处理 RMQ。如果对 ST 表不够熟悉的读者，可以参考我的 ST 表笔记：<a   href='/draft_post/'>【数据结构】ST 表 学习笔记</a>。</li><li><strong>官方 Hint</strong>：此题官方给出了 5 条 Hint 作为解题引导——分段编号、答案公式为相邻段长度和、$\texttt{0}$ 段定义 $ans[i] = 0$、三段均需完全在区间内、用线段树做区间最值并单独处理首尾。我上面的实现正是这条路径的完整版本，只是我觉得用 ST 表处理本题更合适。</li><li><strong>码风说明</strong>：1-based 索引（字符串前补空格）、全局数组、<code>pow2</code> 宏是 OI 赛制下的常见习惯，避免频繁传参和重复计算。在 LeetCode 上使用全局数组时只需注意：每次调用会覆盖使用到的下标区域，无需额外清空。</li></ul>]]>
    </content>
    <id>http://ttzc.github.io/122ef01b/</id>
    <link href="http://ttzc.github.io/122ef01b/"/>
    <published>2026-07-22T16:00:00.000Z</published>
    <summary>LeetCode 3501「操作后最大活跃区段数 II」题解，基于 I 版结论将交易转化为相邻 0 块合并，用 Sparse Table 预处理相邻 0 块长度和的区间最大值，支持 O(log n) 单次查询。</summary>
    <title>LeetCode 3501 操作后最大活跃区段数 II  - Solution</title>
    <updated>2026-07-22T16:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>zaochen</name>
    </author>
    <category term="题解" scheme="http://ttzc.github.io/categories/solution/"/>
    <category term="排序" scheme="http://ttzc.github.io/tags/%E6%8E%92%E5%BA%8F/"/>
    <category term="贪心" scheme="http://ttzc.github.io/tags/%E8%B4%AA%E5%BF%83/"/>
    <category term="Luogu" scheme="http://ttzc.github.io/tags/Luogu/"/>
    <category term="CSP-S" scheme="http://ttzc.github.io/tags/CSP-S/"/>
    <category term="反悔贪心" scheme="http://ttzc.github.io/tags/%E5%8F%8D%E6%82%94%E8%B4%AA%E5%BF%83/"/>
    <content>
      <![CDATA[<h2 id="1-题目数据-problem-metadata"><a class="markdownIt-Anchor" href="#1-题目数据-problem-metadata"></a> 1. 题目数据 (Problem Metadata)</h2><ul><li><strong>题目类型</strong>：传统题</li><li><strong>题目链接</strong>：<a href="https://www.luogu.com.cn/problem/P14361">P14361 [CSP-S 2025] 社团招新 - 洛谷</a></li><li><strong>时间限制</strong>：1.00s</li><li><strong>内存限制</strong>：512.00MB</li></ul><h2 id="2-题意简述-problem-summary"><a class="markdownIt-Anchor" href="#2-题意简述-problem-summary"></a> 2. 题意简述 (Problem Summary)</h2><p>给定 $n$（$n$ 为偶数，$2 \le n \le 10^5$）个新成员，每人对 $3$ 个部门的满意度 $a_{i,j} \in [0, 2 \times 10^4]$。为每个成员选择恰好一个部门 $d_i \in {1,2,3}$，最大化满意度之和 $\sum_{i=1}^{n} a_{i,d_i}$，且每个部门分配人数不超过 $\frac{n}{2}$。多组测试数据，$t \le 5$。</p><h2 id="3-朴素解法-brute-force"><a class="markdownIt-Anchor" href="#3-朴素解法-brute-force"></a> 3. 朴素解法 (Brute-Force)</h2><p>最直接的想法是枚举每个成员的部门选择，共 $3^n$ 种方案，对每种方案检查容量约束并计算满意度。$n = 10$ 时约 $6 \times 10^4$ 种，可过测试点 $1 \sim 4$；$n \ge 30$ 时 $3^{30} \approx 2 \times 10^{14}$，完全不可行。</p><p>另一种思路是 DP：设 $dp[i][c_1][c_2]$ 表示前 $i$ 人中部门 $1$ 有 $c_1$ 人、部门 $2$ 有 $c_2$ 人时的最大满意度（部门 $3$ 人数为 $i - c_1 - c_2$）。状态数 $O(n^3)$，$n = 10^5$ 时约 $10^{15}$，同样不可行。瓶颈在于容量约束使得状态空间与 $n$ 的幂次绑定，无法线性递推。</p><h2 id="4-核心解法-main-solution"><a class="markdownIt-Anchor" href="#4-核心解法-main-solution"></a> 4. 核心解法 (Main Solution)</h2><h3 id="特殊性质"><a class="markdownIt-Anchor" href="#特殊性质"></a> 特殊性质</h3><p>本题的关键观察在于 $3$ 个部门的容量约束具有特殊的结构。设 $\text{cnt}_j$ 为部门 $j$ 分配到的人数，则 $\text{cnt}_1 + \text{cnt}_2 + \text{cnt}_3 = n$，每个 $\text{cnt}_j \le \frac{n}{2}$。</p><p><strong>至多一个部门超员</strong>：若两个部门同时超员（各 $&gt; \frac{n}{2}$），则总人数 $&gt; n$，矛盾。因此，无约束贪心解中至多有一个部门违反容量约束。</p><h3 id="关键突破"><a class="markdownIt-Anchor" href="#关键突破"></a> 关键突破</h3><p>从 $3^n$ 的枚举瓶颈出发，利用「至多一个部门超员」这一性质，将问题分解为两步：</p><ol><li><strong>贪心</strong>：忽略容量约束，每个成员选满意度最高的部门，得到无约束最优解 $\text{sum}$。</li><li><strong>反悔</strong>：若唯一超员部门 $m$ 有 $\text{cnt}_m &gt; \frac{n}{2}$，将 $\text{cnt}_m - \frac{n}{2}$ 人从部门 $m$ 改派到各自的次优部门，使容量满足约束。</li></ol><h3 id="推导过程"><a class="markdownIt-Anchor" href="#推导过程"></a> 推导过程</h3><p>下面推导反悔策略的正确性。</p><p><strong>改派人数</strong>：部门 $m$ 超员 $\text{cnt}_m - \frac{n}{2}$ 人，需恰好移出这么多。不能多移（不必要损失），不能少移（仍超员）。</p><p><strong>改派目标</strong>：每个被移出的人应去自己的次优部门（满意度第二高的部门），而非第三优部门。因为次优满意度 $\ge$ 第三优满意度，改派到次优的损失 $\le$ 改派到第三优的损失。</p><p><strong>次优部门不会超员</strong>：设其余两个部门共有 $n - \text{cnt}_m$ 人。最坏情况下所有被改派的人都去同一个次优部门，该部门人数上界为：<br />$$<br />(n - \text{cnt}_m) + \left(\text{cnt}_m - \frac{n}{2}\right) = n - \frac{n}{2} = \frac{n}{2}<br />$$<br />恰好等于容量上界，不会超员。</p><p><strong>选损失最小的人改派</strong>：每个被改派成员 $i$ 的损失为 $a_{i, \max} - a_{i, \text{sub}}$（最优满意度与次优满意度之差）。将超员部门的成员按此损失升序排序，取前 $\text{cnt}_m - \frac{n}{2}$ 个改派，总损失最小。</p><p>最终答案为：<br />$$<br />\text{ans} = \text{sum} - \sum_{\text{改派的 } k \text{ 人}} \left(a_{i, \max} - a_{i, \text{sub}}\right)<br />$$<br />其中 $k = \text{cnt}_m - \frac{n}{2}$，取损失最小的 $k$ 人。</p><h2 id="5-正确性证明-proof-of-correctness"><a class="markdownIt-Anchor" href="#5-正确性证明-proof-of-correctness"></a> 5. 正确性证明 (Proof of Correctness)</h2><p>下面我们总结一下贪心策略的证明要点，共证明了三点：贪心解的无约束最优性、反悔策略的充分性、次优部门的安全性。</p><p><strong>引理 1（贪心最优）</strong>：无容量约束时，每个成员独立选择满意度最高的部门，满意度之和取到全局最大。这是因为 $\sum_{i=1}^{n} a_{i,d_i}$ 中各项独立，$\max_{d_i} \sum a_{i,d_i} = \sum \max_{d_i} a_{i,d_i}$。</p><p><strong>引理 2（至多一个超员部门）</strong>：若部门 $j$ 与 $j’$ 同时超员，$\text{cnt}<em>j + \text{cnt}</em>{j’} &gt; n$，但 $\text{cnt}_1 + \text{cnt}_2 + \text{cnt}_3 = n$，矛盾。故至多一个部门超员。</p><p><strong>引理 3（次优部门安全）</strong>：改派 $k = \text{cnt}_m - \frac{n}{2}$ 人到次优部门后，任一次优部门的人数上界为 $\frac{n}{2}$（见 §4 推导），不超员。</p><p><strong>定理（反悔最优）</strong>：在全局贪心解的基础上，从唯一超员部门 $m$ 移出恰好 $k$ 人，每人移到次优部门，选损失最小的 $k$ 人，所得方案是容量约束下的最优解。</p><p>设贪心解为 $S^<em>$（无约束最优），任一合法解 $T$ 的满意度 $\le S^</em>$（因为 $S^<em>$ 是无约束最大值）。合法解必须在 $S^</em>$ 基础上调整超员部门 $m$ 的分配：将至少 $k$ 人从部门 $m$ 移出。每人移出后，去次优部门的损失 $\le$ 去第三优部门的损失（$a_{i, \text{sub}} \ge a_{i, \text{third}}$），故最优调整中每人都去次优部门。在所有「移 $k$ 人到次优部门」的方案中，选损失最小的 $k$ 人总损失最小。由引理 3，次优部门不超员，方案合法。因此该反悔策略得到的解是合法解中的最优。</p><p>综上所述，算法正确。</p><h2 id="6-复杂度分析-complexity"><a class="markdownIt-Anchor" href="#6-复杂度分析-complexity"></a> 6. 复杂度分析 (Complexity)</h2><ul><li><p><strong>时间复杂度</strong>：$O(n \log n)$。每人对 $3$ 个满意度排序 $O(1)$（常数大小），收集超员部门成员 $O(n)$，按损失排序 $O(n \log n)$。$n \le 10^5$ 时约 $1.7 \times 10^6$ 次运算，1s 内轻松通过。</p></li><li><p><strong>空间复杂度</strong>：$O(n)$。存储 $n$ 个成员的满意度数组与临时向量，约几 MB，远低于 512 MB 限制。</p></li></ul><h2 id="7-实现细节与避坑指南-implementation-details"><a class="markdownIt-Anchor" href="#7-实现细节与避坑指南-implementation-details"></a> 7. 实现细节与避坑指南 (Implementation Details)</h2><table><thead><tr><th style="text-align:left">坑点</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left"><strong>整数范围</strong></td><td style="text-align:left">$\text{sum}$ 最大 $n \times \max a_{i,j} = 10^5 \times 2 \times 10^4 = 2 \times 10^9$，<code>int</code> 上限约 $2.147 \times 10^9$，够用。</td></tr><tr><td style="text-align:left"><strong>多组数据清空</strong></td><td style="text-align:left"><code>cnt</code>、<code>delta</code> 在 <code>solve()</code> 内部声明，每次调用自动初始化，无需手动清空。全局数组 <code>s[]</code> 每次被读入覆盖，也不会残留。</td></tr><tr><td style="text-align:left"><strong>pair 排序的稳定性</strong></td><td style="text-align:left">代码用 <code>pii{满意度, 部门号}</code> 存储，<code>sort</code> 对 <code>pair</code> 按 <code>first</code> 升序、<code>first</code> 相同时按 <code>second</code> 升序。当满意度相同时，部门号小的排前面，<code>a[2]</code> 的 <code>second</code> 为部门号较小的次优部门。这一行为确定性的，不影响正确性（满意度相同时去哪个次优部门损失相同）。</td></tr><tr><td style="text-align:left"><strong>$n = 2$ 的边界</strong></td><td style="text-align:left">$\frac{n}{2} = 1$，每部门最多 $1$ 人。若两人最优部门相同，需改派 $1$ 人到次优部门。逻辑自然处理。</td></tr><tr><td style="text-align:left"><strong>特殊性质 A（仅部门 1 有满意度）</strong></td><td style="text-align:left">所有成员 $a_{i,2} = a_{i,3} = 0$，贪心全选部门 1，$\text{cnt}<em>1 = n &gt; \frac{n}{2}$。改派损失 $= a</em>{i,1} - 0 = a_{i,1}$，取损失最小的 $\frac{n}{2}$ 人改派，其余留部门 1。正确。</td></tr></tbody></table><h2 id="8-参考代码-reference-code"><a class="markdownIt-Anchor" href="#8-参考代码-reference-code"></a> 8. 参考代码 (Reference Code)</h2><p>下面是我用 <code>pair</code> 排序同时解决「选最优部门」和「找次优部门」的写法。每人将 $3$ 个满意度连同部门号存为 <code>pii</code>，排序后 <code>a[3]</code> 即最优、<code>a[2]</code> 即次优，改派时只需取 <code>a[3].first - a[2].first</code> 作为损失。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;bits/stdc++.h&gt;</span></span></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std;</span><br><span class="line"><span class="comment">// #define int long long</span></span><br><span class="line"></span><br><span class="line"><span class="type">const</span> <span class="type">int</span> N = <span class="number">1e5</span> + <span class="number">5</span>;</span><br><span class="line"><span class="keyword">using</span> pii = pair&lt;<span class="type">int</span>, <span class="type">int</span>&gt;;</span><br><span class="line"><span class="keyword">using</span> vi = vector&lt;<span class="type">int</span>&gt;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">student</span></span><br><span class="line">&#123;</span><br><span class="line">    pii a[<span class="number">4</span>];</span><br><span class="line">&#125; s[N];</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> n;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">solve</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="type">int</span> sum = <span class="number">0</span>;</span><br><span class="line">    <span class="function">vi <span class="title">cnt</span><span class="params">(<span class="number">4</span>, <span class="number">0</span>)</span></span>;</span><br><span class="line">    cin &gt;&gt; n;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= n; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> j = <span class="number">1</span>; j &lt;= <span class="number">3</span>; j++)</span><br><span class="line">            cin &gt;&gt; s[i].a[j].first, s[i].a[j].second = j;</span><br><span class="line">        <span class="built_in">sort</span>(s[i].a + <span class="number">1</span>, s[i].a + <span class="number">4</span>);</span><br><span class="line">        sum += s[i].a[<span class="number">3</span>].first;</span><br><span class="line">        cnt[s[i].a[<span class="number">3</span>].second]++;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="type">int</span> max_ci = <span class="built_in">max_element</span>(cnt.<span class="built_in">begin</span>(), cnt.<span class="built_in">end</span>()) - cnt.<span class="built_in">begin</span>(), max_cv = *<span class="built_in">max_element</span>(cnt.<span class="built_in">begin</span>(), cnt.<span class="built_in">end</span>());</span><br><span class="line">    <span class="keyword">if</span> (max_cv &lt;= n / <span class="number">2</span>)</span><br><span class="line">        <span class="keyword">return</span> sum;</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> ans = sum;</span><br><span class="line">    vector&lt;student&gt; delta;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= n; i++)</span><br><span class="line">        <span class="keyword">if</span> (s[i].a[<span class="number">3</span>].second == max_ci)</span><br><span class="line">            delta.<span class="built_in">push_back</span>(s[i]);</span><br><span class="line"></span><br><span class="line">    <span class="built_in">sort</span>(delta.<span class="built_in">begin</span>(), delta.<span class="built_in">end</span>(), [&amp;](student s1, student s2)</span><br><span class="line">         &#123; <span class="keyword">return</span> s<span class="number">1.</span>a[<span class="number">3</span>].first - s<span class="number">1.</span>a[<span class="number">2</span>].first &lt; s<span class="number">2.</span>a[<span class="number">3</span>].first - s<span class="number">2.</span>a[<span class="number">2</span>].first; &#125;);</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; (max_cv - n / <span class="number">2</span>); i++)</span><br><span class="line">        ans -= delta[i].a[<span class="number">3</span>].first - delta[i].a[<span class="number">2</span>].first;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> ans;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">signed</span> <span class="title">main</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    ios::<span class="built_in">sync_with_stdio</span>(<span class="literal">false</span>);</span><br><span class="line"><span class="meta">#<span class="keyword">ifdef</span> DEBUG</span></span><br><span class="line">    <span class="type">clock_t</span> t0 = <span class="built_in">clock</span>();</span><br><span class="line">    <span class="built_in">freopen</span>(<span class="string">&quot;club2.in&quot;</span>, <span class="string">&quot;r&quot;</span>, stdin);</span><br><span class="line">    <span class="built_in">freopen</span>(<span class="string">&quot;data.out&quot;</span>, <span class="string">&quot;w&quot;</span>, stdout);</span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// Don&#x27;t stop. Don&#x27;t hide. Follow the light, and you&#x27;ll find tomorrow.</span></span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> t;</span><br><span class="line">    cin &gt;&gt; t;</span><br><span class="line">    <span class="keyword">while</span> (t--)</span><br><span class="line">        cout &lt;&lt; <span class="built_in">solve</span>() &lt;&lt; endl;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">ifdef</span> DEBUG</span></span><br><span class="line">    cerr &lt;&lt; <span class="string">&quot;Time used:&quot;</span> &lt;&lt; <span class="built_in">clock</span>() - t0 &lt;&lt; <span class="string">&quot;ms&quot;</span> &lt;&lt; endl;</span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="9-补充说明-additional-notes"><a class="markdownIt-Anchor" href="#9-补充说明-additional-notes"></a> 9. 补充说明 (Additional Notes)</h2><ul><li><strong>题目渊源</strong>：本题出自 <strong>CSP-S 2025 第二轮</strong>，是「贪心 + 反悔」的经典入门题。这类「先贪心求无约束最优，再对违反约束的部分做最小代价调整」的思路在竞赛中很常见，核心识别信号是约束结构简单（如至多一个维度违约）、且调整代价可排序取最小。本题 $3$ 个部门与 $\frac{n}{2}$ 限制的设定使得「至多一个超员」这一性质很容易发现，是反悔策略成立的基础。</li><li><strong>数据范围与测试点</strong>：测试点按 $n$ 规模分档，最小 $n = 2$（测试点 1），最大 $n = 10^5$（测试点 12 起）。特殊性质 A（仅部门 1 有满意度）和 B（仅部门 1、2 有满意度）是贪心 + 反悔的退化场景，C 为随机数据。$O(n \log n)$ 的排序解法可过全部测试点。</li></ul><h3 id="其他版本"><a class="markdownIt-Anchor" href="#其他版本"></a> 其他版本</h3><p>下面是一个不依赖 <code>pair</code> 排序的写法，直接用比较找最优和次优部门。思路相同，但省去了排序 $3$ 个元素的步骤，常数更小：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;bits/stdc++.h&gt;</span></span></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std;</span><br><span class="line"></span><br><span class="line"><span class="type">const</span> <span class="type">int</span> N = <span class="number">1e5</span> + <span class="number">5</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">student</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> a[<span class="number">4</span>]; <span class="comment">// a[1..3] = 满意度</span></span><br><span class="line">    <span class="type">int</span> best, sub; <span class="comment">// 最优/次优部门号</span></span><br><span class="line">&#125; s[N];</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> n;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">solve</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="type">int</span> sum = <span class="number">0</span>;</span><br><span class="line">    <span class="type">int</span> cnt[<span class="number">4</span>] = &#123;<span class="number">0</span>&#125;;</span><br><span class="line">    cin &gt;&gt; n;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= n; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        cin &gt;&gt; s[i].a[<span class="number">1</span>] &gt;&gt; s[i].a[<span class="number">2</span>] &gt;&gt; s[i].a[<span class="number">3</span>];</span><br><span class="line">        <span class="comment">// 找最优和次优部门</span></span><br><span class="line">        <span class="keyword">if</span> (s[i].a[<span class="number">1</span>] &gt;= s[i].a[<span class="number">2</span>]) &#123; s[i].best = <span class="number">1</span>; s[i].sub = (s[i].a[<span class="number">2</span>] &gt;= s[i].a[<span class="number">3</span>]) ? <span class="number">2</span> : <span class="number">3</span>; &#125;</span><br><span class="line">        <span class="keyword">else</span> &#123; s[i].best = <span class="number">2</span>; s[i].sub = (s[i].a[<span class="number">1</span>] &gt;= s[i].a[<span class="number">3</span>]) ? <span class="number">1</span> : <span class="number">3</span>; &#125;</span><br><span class="line">        <span class="comment">// 修正：需保证 best 是最大的</span></span><br><span class="line">        <span class="keyword">if</span> (s[i].a[s[i].sub] &gt; s[i].a[s[i].best]) <span class="built_in">swap</span>(s[i].best, s[i].sub);</span><br><span class="line">        sum += s[i].a[s[i].best];</span><br><span class="line">        cnt[s[i].best]++;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="type">int</span> max_ci = <span class="built_in">max_element</span>(cnt + <span class="number">1</span>, cnt + <span class="number">4</span>) - cnt;</span><br><span class="line">    <span class="type">int</span> max_cv = *<span class="built_in">max_element</span>(cnt + <span class="number">1</span>, cnt + <span class="number">4</span>);</span><br><span class="line">    <span class="keyword">if</span> (max_cv &lt;= n / <span class="number">2</span>)</span><br><span class="line">        <span class="keyword">return</span> sum;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 收集超员部门成员的改派损失</span></span><br><span class="line">    vector&lt;<span class="type">int</span>&gt; loss;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= n; i++)</span><br><span class="line">        <span class="keyword">if</span> (s[i].best == max_ci)</span><br><span class="line">            loss.<span class="built_in">push_back</span>(s[i].a[s[i].best] - s[i].a[s[i].sub]);</span><br><span class="line">    <span class="built_in">sort</span>(loss.<span class="built_in">begin</span>(), loss.<span class="built_in">end</span>());</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; max_cv - n / <span class="number">2</span>; i++)</span><br><span class="line">        sum -= loss[i];</span><br><span class="line">    <span class="keyword">return</span> sum;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">signed</span> <span class="title">main</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    ios::<span class="built_in">sync_with_stdio</span>(<span class="literal">false</span>);</span><br><span class="line">    <span class="type">int</span> t;</span><br><span class="line">    cin &gt;&gt; t;</span><br><span class="line">    <span class="keyword">while</span> (t--)</span><br><span class="line">        cout &lt;&lt; <span class="built_in">solve</span>() &lt;&lt; <span class="string">&#x27;\n&#x27;</span>;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>两种写法的复杂度相同，均为 $O(n \log n)$。<code>pair</code> 排序写法利用 <code>sort</code> 同时确定最优与次优部门，代码更紧凑；显式比较写法不依赖排序，但需要处理比较逻辑。本题每人只有 $3$ 个元素，排序常数极小，两种写法的实际运行时间差异可忽略。我觉得用 <code>pair</code> 排序的写法更清晰，因为它把「选最优」和「找次优」统一到了一次排序中。</p>]]>
    </content>
    <id>http://ttzc.github.io/ac6e74e8/</id>
    <link href="http://ttzc.github.io/ac6e74e8/"/>
    <published>2026-07-22T16:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="1-题目数据-problem-metadata"><a class="markdownIt-Anchor" href="#1-题目数据-problem-metadata"></a> 1. 题目数据 (Problem Metadata)</h2>
<ul>
<li>]]>
    </summary>
    <title>洛谷 P14361 社团招新 - Solution</title>
    <updated>2026-07-22T16:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>zaochen</name>
    </author>
    <category term="代码模板" scheme="http://ttzc.github.io/categories/code-template/"/>
    <category term="代码模板" scheme="http://ttzc.github.io/tags/%E4%BB%A3%E7%A0%81%E6%A8%A1%E6%9D%BF/"/>
    <category term="ST表" scheme="http://ttzc.github.io/tags/ST%E8%A1%A8/"/>
    <category term="RMQ" scheme="http://ttzc.github.io/tags/RMQ/"/>
    <category term="倍增" scheme="http://ttzc.github.io/tags/%E5%80%8D%E5%A2%9E/"/>
    <category term="区间最值" scheme="http://ttzc.github.io/tags/%E5%8C%BA%E9%97%B4%E6%9C%80%E5%80%BC/"/>
    <content>
      <![CDATA[<p>ST 表基于倍增思想，用 $st[i][j]$ 维护以 $i$ 为左端点、长度为 $2^j$ 的区间 $[i,,i+2^j-1]$ 的最值，由两个长度为 $2^{j-1}$ 的子区间合并而来。预处理 $O(n\log n)$，单次查询 $O(1)$，但不支持修改，适用于离线 RMQ 问题。</p><hr /><h2 id="区间最值查询维护最大值"><a class="markdownIt-Anchor" href="#区间最值查询维护最大值"></a> 区间最值查询（维护最大值）</h2><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">const</span> <span class="type">int</span> N = <span class="number">1e5</span> + <span class="number">5</span>, M = <span class="number">17</span>;   <span class="comment">// M = floor(log2(N)) + 1</span></span><br><span class="line"><span class="type">int</span> a[N], st[N][M], lg[N];</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">ST_init</span><span class="params">(<span class="type">int</span> n)</span> </span>&#123;                       <span class="comment">// 预处理</span></span><br><span class="line">    lg[<span class="number">1</span>] = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">2</span>; i &lt;= n; i++) lg[i] = lg[i &gt;&gt; <span class="number">1</span>] + <span class="number">1</span>;  <span class="comment">// 预处理 log2，避免查询时调用对数函数</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= n; i++) st[i][<span class="number">0</span>] = a[i];</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> j = <span class="number">1</span>; (<span class="number">1</span> &lt;&lt; j) &lt;= n; j++)</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i + (<span class="number">1</span> &lt;&lt; j) - <span class="number">1</span> &lt;= n; i++)</span><br><span class="line">            st[i][j] = <span class="built_in">max</span>(st[i][j - <span class="number">1</span>], st[i + (<span class="number">1</span> &lt;&lt; (j - <span class="number">1</span>))][j - <span class="number">1</span>]);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">ST_query</span><span class="params">(<span class="type">int</span> l, <span class="type">int</span> r)</span> </span>&#123;                <span class="comment">// 查询 [l, r] 的最大值</span></span><br><span class="line">    <span class="type">int</span> k = lg[r - l + <span class="number">1</span>];</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">max</span>(st[l][k], st[r - (<span class="number">1</span> &lt;&lt; k) + <span class="number">1</span>][k]);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>查询时用两个长度为 $2^k$ 的区间覆盖 $[l,r]$，其中 $k$ 是满足 $2^k \le r-l+1$ 的最大整数。两区间有重叠，但因为 $\max$ 满足可重复贡献性（$f(x,x)=x$），重叠部分不影响结果。</p></blockquote><hr /><h2 id="运算替换"><a class="markdownIt-Anchor" href="#运算替换"></a> 运算替换</h2><p>将 <code>max</code> 替换为其他满足<strong>可重复贡献性</strong>的运算即可（即 $f(x,x)=x$，重叠区间不改变结果）：</p><table><thead><tr><th>运算</th><th>替换</th><th>说明</th></tr></thead><tbody><tr><td>最大值</td><td><code>max</code></td><td>默认</td></tr><tr><td>最小值</td><td><code>min</code></td><td>把 <code>max</code> 全局替换</td></tr><tr><td>GCD</td><td><code>gcd</code></td><td>满足幂等性</td></tr><tr><td>按位与</td><td><code>&amp;</code></td><td>$x\ &amp;\ x = x$</td></tr><tr><td>按位或</td><td><code>|</code></td><td>$x\ | x = x$</td></tr></tbody></table><blockquote><p>加法、乘法等不满足可重复贡献性的运算不能用 ST 表，需改用前缀和或线段树。</p></blockquote><hr /><p>相关笔记：<a   href='/draft_post/'>【数据结构】ST 表 学习笔记</a></p>]]>
    </content>
    <id>http://ttzc.github.io/16a27d21/</id>
    <link href="http://ttzc.github.io/16a27d21/"/>
    <published>2026-07-21T16:00:00.000Z</published>
    <summary>
      <![CDATA[<p>ST 表基于倍增思想，用 $st[i][j]$ 维护以 $i$ 为左端点、长度为 $2^j$ 的区间 $[i,,i+2^j-1]$ 的最值，由两个长度为 $2^{j-1}$ 的子区间合并而来。预处理 $O(n\log n)$，单次查询 $O(1)$，但不支持修改，适用于离线]]>
    </summary>
    <title>代码模板-ST 表</title>
    <updated>2026-07-21T16:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>zaochen</name>
    </author>
    <category term="题解" scheme="http://ttzc.github.io/categories/solution/"/>
    <category term="贪心" scheme="http://ttzc.github.io/tags/%E8%B4%AA%E5%BF%83/"/>
    <category term="字符串" scheme="http://ttzc.github.io/tags/%E5%AD%97%E7%AC%A6%E4%B8%B2/"/>
    <category term="LeetCode" scheme="http://ttzc.github.io/tags/LeetCode/"/>
    <content>
      <![CDATA[<h2 id="1-题目数据-problem-metadata"><a class="markdownIt-Anchor" href="#1-题目数据-problem-metadata"></a> 1. 题目数据 (Problem Metadata)</h2><ul><li><strong>题目类型</strong>：传统题</li><li><strong>题目链接</strong>：<a href="https://leetcode.cn/problems/maximize-active-section-with-trade-i/">3499. 操作后最大活跃区段数 I - 力扣（LeetCode）</a></li></ul><h2 id="2-题意简述-problem-summary"><a class="markdownIt-Anchor" href="#2-题意简述-problem-summary"></a> 2. 题意简述 (Problem Summary)</h2><p>给定长度为 $n$（$1 \le n \le 10^5$）的二进制字符串 $s$，$s[i] \in {\texttt{‘0’}, \texttt{‘1’}}$。在 $s$ 两侧各补一个 $\texttt{‘1’}$ 得到 $t = \texttt{‘1’} + s + \texttt{‘1’}$，最多执行一次「交易」：</p><ol><li>选一个被 $\texttt{‘0’}$ 包围的连续 $\texttt{‘1’}$ 块，整体变 $\texttt{‘0’}$；</li><li>再选一个被 $\texttt{‘1’}$ 包围的连续 $\texttt{‘0’}$ 块，整体变 $\texttt{‘1’}$。</li></ol><p>求操作后 $s$ 中 $\texttt{‘1’}$ 的最大数量（两端补的 $\texttt{‘1’}$ 不计入）。</p><h2 id="3-朴素解法-brute-force"><a class="markdownIt-Anchor" href="#3-朴素解法-brute-force"></a> 3. 朴素解法 (Brute-Force)</h2><p>最直接的思路是枚举每一步的选择：先枚举所有「被 0 包围的 1 块」作为第一步的目标，变 0 后再枚举所有「被 1 包围的 0 块」作为第二步的目标，统计最终 1 的个数取最大。</p><p>字符串分段数为 $O(n)$，两步枚举组合数为 $O(n^2)$，每步重新计数 $O(n)$，总复杂度 $O(n^3)$。$n = 10^5$ 时约 $10^{15}$ 次运算，远超时限。瓶颈在于重复统计 1 的个数——其实操作对 1 总数的影响是可解析计算的，无需模拟。</p><h2 id="4-核心解法-main-solution"><a class="markdownIt-Anchor" href="#4-核心解法-main-solution"></a> 4. 核心解法 (Main Solution)</h2><h3 id="特殊性质"><a class="markdownIt-Anchor" href="#特殊性质"></a> 特殊性质</h3><p>交易的两步是耦合的：第一步把某个 1 块变 0，会让它两侧的 0 块合并成一个更大的 0 块；第二步再把这个合并后的 0 块变 1。关键在于，合并后的 0 块是否仍被 1 包围——答案是肯定的，因为原 1 块两侧的 0 块外侧本来就是 1。</p><h3 id="关键突破"><a class="markdownIt-Anchor" href="#关键突破"></a> 关键突破</h3><p>把操作还原到 $t$ 的分段结构上。设选中的 1 块为 $B_1$，其左右两侧的 0 块为 $L_0$、$R_0$，则第一步后 $L_0 + B_1 + R_0$ 合并成一个大 0 块（长度 $|L_0| + |B_1| + |R_0|$），两侧仍是 1，满足第二步的条件。第二步将其变 1 后，净效果是</p><p>$$<br />\underbrace{L_0}<em>{\texttt{0}}\ \underbrace{B_1}</em>{\texttt{1}}\ \underbrace{R_0}<em>{\texttt{0}} \longrightarrow \underbrace{L_0 + B_1 + R_0}</em>{\texttt{1}}<br />$$</p><p>增量 $= |L_0| + |R_0|$（$B_1$ 本来就是 1，变 1 无增量；$L_0$、$R_0$ 从 0 变 1，每个位置贡献 $+1$）。也就是说，一次交易的本质是「选两个相邻的、被 1 包围的 0 块，把它们连同中间的 1 块一起变成 1」，增量恰为两个 0 块长度之和。</p><h3 id="推导过程"><a class="markdownIt-Anchor" href="#推导过程"></a> 推导过程</h3><p>将 $t$ 按连续相同字符分段，得到交替的 1 段与 0 段。筛选出所有「被 1 包围的 0 段」，按出现顺序记其长度为 $z_1, z_2, \dots, z_k$。两个 0 段「相邻」指它们之间只隔一个 1 段（即可被同一笔交易所合并）。则</p><p>$$<br />\text{ans} = \text{cnt1} + \max_{1 \le i &lt; k}\big(z_i + z_{i+1}\big)<br />$$</p><p>其中 $\text{cnt1} = s.\text{count}(\texttt{‘1’})$ 为不操作时的 1 总数。若 $k &lt; 2$（没有两个相邻的可合并 0 段），则无法交易，$\text{ans} = \text{cnt1}$。</p><h2 id="5-正确性证明-proof-of-correctness"><a class="markdownIt-Anchor" href="#5-正确性证明-proof-of-correctness"></a> 5. 正确性证明 (Proof of Correctness)</h2><p>需证两点：交易增量公式成立，且枚举相邻 0 段对不重不漏。</p><p><strong>增量公式</strong>。设交易选中 1 段 $B_1$，其左右 0 段为 $L_0$、$R_0$。第一步 $B_1 \to \texttt{0}$，1 总数减少 $|B_1|$；此时 $L_0, B_1, R_0$ 合并为一个 0 段，被 1 包围，第二步整体变 1，1 总数增加 $|L_0| + |B_1| + |R_0|$。净增量 $= (|L_0| + |B_1| + |R_0|) - |B_1| = |L_0| + |R_0|$。公式成立。</p><p><strong>不重不漏</strong>。$t$ 的分段交替排列，任意被 1 包围的 0 段其两侧必为 1 段。一个可交易的 1 段必须两侧紧邻 0 段，故它恰对应「左右两个 0 段」这一对。遍历所有相邻的 0 段对 $(z_i, z_{i+1})$，即覆盖所有可交易 1 段，且不同 1 段对应不同的 0 段对，不重不漏。</p><p>综上所述，枚举相邻 0 段对取 $z_i + z_{i+1}$ 最大值，加上 $\text{cnt1}$，即为最优解。</p><h2 id="6-复杂度分析-complexity"><a class="markdownIt-Anchor" href="#6-复杂度分析-complexity"></a> 6. 复杂度分析 (Complexity)</h2><ul><li><strong>时间复杂度</strong>：$O(n)$。分段遍历一次，筛选与求相邻最大和各一遍，均为线性。$n = 10^5$ 时约 $3 \times 10^5$ 次运算，轻松通过。</li><li><strong>空间复杂度</strong>：$O(n)$，存储分段列表与筛选结果。远低于典型内存限制。</li><li><strong>常数优化</strong>：可用一次遍历维护「上一个被 1 包围的 0 段长度 $\text{pre}$」与「相邻和最大值 $\text{mx}$」，将空间降至 $O(1)$。但 $O(n)$ 已足够，分段写法可读性更好，不必为省内存引入额外状态维护。$O(1)$ 写法见 §9。</li></ul><h2 id="7-实现细节与避坑指南-implementation-details"><a class="markdownIt-Anchor" href="#7-实现细节与避坑指南-implementation-details"></a> 7. 实现细节与避坑指南 (Implementation Details)</h2><table><thead><tr><th>坑点</th><th>说明</th></tr></thead><tbody><tr><td><strong>两端补 1 的处理</strong></td><td>题目规定 $t = \texttt{‘1’} + s + \texttt{‘1’}$，实现时不必真的拼接，分段时在首尾各插入一个长度为 1 的虚拟 1 段即可。这保证首尾的 0 段也能被视为「被 1 包围」。</td></tr><tr><td><strong>被 1 包围的判定</strong></td><td>一个 0 段「被 1 包围」要求其前一段和后一段都是 1 段。补 1 后首尾段必为 1 段，故原串首尾的 0 段也满足条件。</td></tr><tr><td><strong>$k &lt; 2$ 的边界</strong></td><td>被筛选出的 0 段不足 2 个时无相邻对，<code>range(k - 1)</code> 为空，此时应返回 $\text{cnt1}$。用 <code>max([cnt1] + [...])</code> 自然处理：列表至少含 <code>cnt1</code>，空枚举不报错。</td></tr><tr><td><strong>分段构建的尾段</strong></td><td>循环结束后需把最后一段 <code>append</code> 进列表，再补末尾虚拟 1 段。漏掉尾段会导致最后一个 0 段丢失。</td></tr><tr><td><strong>整数范围</strong></td><td>答案 $\le n \le 10^5$，Python 整数无溢出问题；C++ 用 <code>int</code> 也足够。</td></tr></tbody></table><h2 id="8-参考代码-reference-code"><a class="markdownIt-Anchor" href="#8-参考代码-reference-code"></a> 8. 参考代码 (Reference Code)</h2><p>下面是我提交时使用的版本，采用「分段 + 筛选被 1 包围的 0 段 + 取相邻对最大值」的思路：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>:</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">maxActiveSectionsAfterTrade</span>(<span class="params">self, s: <span class="built_in">str</span></span>) -&gt; <span class="built_in">int</span>:</span><br><span class="line">        n = <span class="built_in">len</span>(s)</span><br><span class="line"></span><br><span class="line">        <span class="comment"># 按连续相同字符分段，首部插入虚拟 1 段</span></span><br><span class="line">        chunks = [(<span class="string">&quot;1&quot;</span>, <span class="number">1</span>)]</span><br><span class="line">        tpe, lth = s[<span class="number">0</span>], <span class="number">1</span></span><br><span class="line">        <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">1</span>, n):</span><br><span class="line">            <span class="keyword">if</span> s[i] != s[i - <span class="number">1</span>]:</span><br><span class="line">                chunks.append((tpe, lth))</span><br><span class="line">                tpe, lth = s[i], <span class="number">1</span></span><br><span class="line">            <span class="keyword">else</span>:</span><br><span class="line">                lth += <span class="number">1</span></span><br><span class="line">        chunks.append((tpe, lth))   <span class="comment"># 尾段</span></span><br><span class="line">        chunks.append((<span class="string">&quot;1&quot;</span>, <span class="number">1</span>))      <span class="comment"># 末尾虚拟 1 段</span></span><br><span class="line"></span><br><span class="line">        <span class="comment"># 筛选被 1 包围的 0 段长度</span></span><br><span class="line">        zeros = [</span><br><span class="line">            chunks[i][<span class="number">1</span>]</span><br><span class="line">            <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">1</span>, <span class="built_in">len</span>(chunks))</span><br><span class="line">            <span class="keyword">if</span> chunks[i][<span class="number">0</span>] == <span class="string">&quot;0&quot;</span></span><br><span class="line">            <span class="keyword">and</span> chunks[i - <span class="number">1</span>][<span class="number">0</span>] == <span class="string">&quot;1&quot;</span></span><br><span class="line">            <span class="keyword">and</span> chunks[i + <span class="number">1</span>][<span class="number">0</span>] == <span class="string">&quot;1&quot;</span></span><br><span class="line">        ]</span><br><span class="line"></span><br><span class="line">        cnt1 = s.count(<span class="string">&quot;1&quot;</span>)</span><br><span class="line">        <span class="comment"># 取相邻两个 0 段长度之和的最大值，加到 cnt1 上</span></span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">max</span>([cnt1] + [cnt1 + zeros[i] + zeros[i + <span class="number">1</span>]</span><br><span class="line">                             <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(zeros) - <span class="number">1</span>)])</span><br></pre></td></tr></table></figure><h2 id="9-补充说明-additional-notes"><a class="markdownIt-Anchor" href="#9-补充说明-additional-notes"></a> 9. 补充说明 (Additional Notes)</h2><ul><li><strong>题目背景</strong>：本题出自 LeetCode 第 153 场双周赛，题面用「交易」包装了一次区间翻转操作，核心是识别出两步操作的净效果等价于「合并两个相邻 0 段」。这种「把连续操作归约为单次效果」的化简思路在字符串贪心题中很常见。<br />0</li><li><strong>与 3501 题的关系</strong>：本题是 I 版（$n \le 10^5$），同系列的 II 版（3501）将 $n$ 放大到 $10^5$ 的多次操作，思路一脉相承但需更精细的维护。</li></ul><h3 id="其他版本"><a class="markdownIt-Anchor" href="#其他版本"></a> 其他版本</h3><p>下面是一次遍历的 $O(1)$ 空间写法，省去分段存储，边读边维护「上一个 0 段长度」与「相邻和最大值」：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>:</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">maxActiveSectionsAfterTrade</span>(<span class="params">self, s: <span class="built_in">str</span></span>) -&gt; <span class="built_in">int</span>:</span><br><span class="line">        n = <span class="built_in">len</span>(s)</span><br><span class="line">        cnt1 = <span class="number">0</span>          <span class="comment"># 1 的总数</span></span><br><span class="line">        pre = -<span class="number">1</span>          <span class="comment"># 上一个被 1 包围的 0 段长度，-1 表示尚不存在</span></span><br><span class="line">        mx = <span class="number">0</span>            <span class="comment"># 相邻两个 0 段长度之和的最大值</span></span><br><span class="line">        i = <span class="number">0</span></span><br><span class="line">        <span class="keyword">while</span> i &lt; n:</span><br><span class="line">            j = i + <span class="number">1</span></span><br><span class="line">            <span class="keyword">while</span> j &lt; n <span class="keyword">and</span> s[j] == s[i]:</span><br><span class="line">                j += <span class="number">1</span></span><br><span class="line">            cur = j - i</span><br><span class="line">            <span class="keyword">if</span> s[i] == <span class="string">&quot;1&quot;</span>:</span><br><span class="line">                cnt1 += cur</span><br><span class="line">            <span class="keyword">else</span>:</span><br><span class="line">                <span class="keyword">if</span> pre != -<span class="number">1</span>:</span><br><span class="line">                    mx = <span class="built_in">max</span>(mx, pre + cur)</span><br><span class="line">                pre = cur</span><br><span class="line">            i = j</span><br><span class="line">        <span class="keyword">return</span> cnt1 + mx</span><br></pre></td></tr></table></figure><p>两种写法复杂度同为 $O(n)$，差异在空间与可读性：分段写法逻辑分层清晰，便于调试和理解；一次遍历写法空间更省、常数更小。思路都经典，都有学习的价值。</p>]]>
    </content>
    <id>http://ttzc.github.io/8a401a71/</id>
    <link href="http://ttzc.github.io/8a401a71/"/>
    <published>2026-07-21T16:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="1-题目数据-problem-metadata"><a class="markdownIt-Anchor" href="#1-题目数据-problem-metadata"></a> 1. 题目数据 (Problem Metadata)</h2>
<ul>
<li>]]>
    </summary>
    <title>LeetCode 3499 操作后最大活跃区段数 I - Solution</title>
    <updated>2026-07-21T16:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>zaochen</name>
    </author>
    <category term="题解" scheme="http://ttzc.github.io/categories/solution/"/>
    <category term="ST表" scheme="http://ttzc.github.io/tags/ST%E8%A1%A8/"/>
    <category term="区间最值" scheme="http://ttzc.github.io/tags/%E5%8C%BA%E9%97%B4%E6%9C%80%E5%80%BC/"/>
    <category term="Luogu" scheme="http://ttzc.github.io/tags/Luogu/"/>
    <category term="min-max博弈" scheme="http://ttzc.github.io/tags/min-max%E5%8D%9A%E5%BC%88/"/>
    <category term="博弈论" scheme="http://ttzc.github.io/tags/%E5%8D%9A%E5%BC%88%E8%AE%BA/"/>
    <content>
      <![CDATA[<h2 id="1-题目数据-problem-metadata"><a class="markdownIt-Anchor" href="#1-题目数据-problem-metadata"></a> 1. 题目数据 (Problem Metadata)</h2><ul><li><strong>题目类型</strong>：传统题</li><li><strong>题目链接</strong>：<a href="https://www.luogu.com.cn/problem/P8818">P8818 [CSP-S 2022] 策略游戏 - 洛谷</a></li><li><strong>时间限制</strong>：1.00s</li><li><strong>内存限制</strong>：512.00MB</li></ul><h2 id="2-题意简述-problem-summary"><a class="markdownIt-Anchor" href="#2-题意简述-problem-summary"></a> 2. 题意简述 (Problem Summary)</h2><p>给定长度为 $n$ 的数组 $A$ 和长度为 $m$ 的数组 $B$（$1 \le n, m, q \le 10^5$，$|a_i|, |b_i| \le 10^9$）。共 $q$ 轮游戏，每轮给定 $l_1, r_1, l_2, r_2$：小 L 先从 $A[l_1…r_1]$ 中选一个数 $a$，小 Q 再从 $B[l_2…r_2]$ 中选一个数 $b$。小 L 要让 $a \cdot b$ 尽可能大，小 Q 要让 $a \cdot b$ 尽可能小。求每轮在双方最优策略下的乘积值：</p><p>$$<br />\text{ans} = \max_{a \in A[l_1…r_1]}; \min_{b \in B[l_2…r_2]}; a \cdot b<br />$$</p><h2 id="3-朴素解法-brute-force"><a class="markdownIt-Anchor" href="#3-朴素解法-brute-force"></a> 3. 朴素解法 (Brute-Force)</h2><p>最直接的想法是对每个查询枚举 $A[l_1…r_1]$ 中每个 $a$，对每个 $a$ 枚举 $B[l_2…r_2]$ 中每个 $b$ 计算 $a \cdot b$ 取最小值，再对 $a$ 取最大值。</p><ul><li>每次查询需 $O\big((r_1-l_1+1)(r_2-l_2+1)\big)$，最坏 $O(nm)$。</li><li>$q$ 次查询总计 $O(qnm) \approx 10^{15}$，完全不可行。</li></ul><p>即使优化为「先扫描 $B$ 求得 $b_{\min}, b_{\max}$，再枚举 $A$ 中每个 $a$ 根据 $a$ 的符号选 $b_{\min}$ 或 $b_{\max}$」，每次查询仍需 $O(n+m)$，总计 $O\big(q(n+m)\big) \approx 2 \times 10^{10}$，仍然超时。瓶颈在于每次查询都要重新扫描数组求极值。</p><h2 id="4-核心解法-main-solution"><a class="markdownIt-Anchor" href="#4-核心解法-main-solution"></a> 4. 核心解法 (Main Solution)</h2><h3 id="特殊性质"><a class="markdownIt-Anchor" href="#特殊性质"></a> 特殊性质</h3><p>这是一个 <strong>min-max 博弈</strong>：小 L 先手最大化，小 Q 后手最小化。关键观察在于，给定小 L 选定的 $a$ 后，小 Q 的最优选择只取决于 $a$ 的符号，而与 $B$ 区间的正负组成无关。</p><h3 id="关键突破"><a class="markdownIt-Anchor" href="#关键突破"></a> 关键突破</h3><p>固定 $a$，小 Q 要选 $b$ 使 $a \cdot b$ 最小：</p><ul><li>若 $a &gt; 0$：$a \cdot b$ 随 $b$ 单调递增，小 Q 选 $b_{\min}$。</li><li>若 $a &lt; 0$：$a \cdot b$ 随 $b$ 单调递减，小 Q 选 $b_{\max}$。</li><li>若 $a = 0$：乘积恒为 $0$。</li></ul><p>因此无论 $B$  区间是全正、全负还是混合，小 Q 的选择都统一为「$a$ 负选 $b_{\max}$，$a$ 正选 $b_{\min}$」。这就是为什么代码只需要维护 $B$ 的区间最大值与最小值，而不必像后文分析 $A$  那样细分 $B$  的正负。</p><p>下面分析小 L 的选数策略。定义小 L 选定 $a$ 后的最终结果为：<br />$$<br />g(a) = \begin{cases} a \cdot b_{\min} &amp; a &gt; 0 \ a \cdot b_{\max} &amp; a &lt; 0 \ 0 &amp; a = 0 \end{cases}<br />$$</p><p>小 L 要求 $\max g(a)$。注意到 $g(a)$ 在 $a &gt; 0$ 和 $a &lt; 0$ 两段上分别是关于 $a$ 的线性函数（系数 $b_{\min}$ 或 $b_{\max}$ 固定），而线性函数在区间上的最大值只在端点取得。因此小 L 只需考虑 $A$ 区间中的几个极值候选。接下来我们推导各种选择时的结果，以及选择策略。</p><h3 id="推导过程"><a class="markdownIt-Anchor" href="#推导过程"></a> 推导过程</h3><table><thead><tr><th style="text-align:left">小 L 选</th><th style="text-align:left">前提</th><th style="text-align:left">小 Q 选</th><th style="text-align:left">结果</th></tr></thead><tbody><tr><td style="text-align:left">最小负数 $a_n^{\min}$（绝对值最大）</td><td style="text-align:left">$A$ 含负数</td><td style="text-align:left">$b_{\max}$</td><td style="text-align:left">$a_n^{\min} \cdot b_{\max}$</td></tr><tr><td style="text-align:left">最大负数 $a_n^{\max}$（绝对值最小）</td><td style="text-align:left">$A$ 含负数</td><td style="text-align:left">$b_{\max}$</td><td style="text-align:left">$a_n^{\max} \cdot b_{\max}$</td></tr><tr><td style="text-align:left">最小正数 $a_p^{\min}$</td><td style="text-align:left">$A$ 含正数</td><td style="text-align:left">$b_{\min}$</td><td style="text-align:left">$a_p^{\min} \cdot b_{\min}$</td></tr><tr><td style="text-align:left">最大正数 $a_p^{\max}$</td><td style="text-align:left">$A$ 含正数</td><td style="text-align:left">$b_{\min}$</td><td style="text-align:left">$a_p^{\max} \cdot b_{\min}$</td></tr><tr><td style="text-align:left">$0$</td><td style="text-align:left">$A$ 含 $0$</td><td style="text-align:left">任意</td><td style="text-align:left">$0$</td></tr></tbody></table><p>之所以正数和负数各取两个端点，是因为 $b_{\min}$、$b_{\max}$ 的符号事先未知：当 $b_{\min} &gt; 0$ 时正数段越大越好（选 $a_p^{\max}$），当 $b_{\min} &lt; 0$ 时正数段越小越好（选 $a_p^{\min}$）；负数段同理。把所有存在的候选算一遍取 $\max$，即可覆盖所有情况。</p><p>至此问题归结为 $O(1)$ 查询 $A$ 的上述极值与 $B$ 的 $b_{\min}, b_{\max}$，用 <strong>ST 表</strong> 预处理即可。小 L 会在这些所有候选结果中选出一个最大的作为答案。</p><h2 id="5-正确性证明-proof-of-correctness"><a class="markdownIt-Anchor" href="#5-正确性证明-proof-of-correctness"></a> 5. 正确性证明 (Proof of Correctness)</h2><p>需证两点：小 Q 的最优策略，以及小 L 的候选集充分性。</p><p><strong>引理（小 Q 的最优选择）</strong>：设小 L 已选 $a$，小 Q 在 $B$ 中选 $b$ 使 $a \cdot b$ 最小。当 $a &gt; 0$ 时 $a \cdot b$ 关于 $b$ 单调递增，最小值在 $b = b_{\min}$ 取得；当 $a &lt; 0$ 时关于 $b$ 单调递减，最小值在 $b = b_{\max}$ 取得；当 $a = 0$ 时乘积恒为 $0$。这与 $b_{\min}, b_{\max}$ 本身的正负无关，因为 $b_{\min}$ 始终是 $B$ 中最小的、$b_{\max}$ 始终是最大的。</p><p><strong>定理（小 L 候选集充分性）</strong>：小 L 的最优 $a$ 一定在 ${a_p^{\max}, a_p^{\min}, a_n^{\max}, a_n^{\min}, 0}$（若存在）之中。对于 $a &gt; 0$ 的部分，$g(a) = a \cdot b_{\min}$ 是关于 $a$ 的一次函数，$b_{\min}$ 为常数，一次函数在区间上的最大值在端点取得，故只需检查最小正数与最大正数。$a &lt; 0$ 的部分，$g(a) = a \cdot b_{\max}$ 同理只需检查最小负数与最大负数。$a = 0$ 时 $g(0) = 0$。三类候选的并集不重不漏地覆盖了 $a$ 的所有取值，取 $\max$ 即得全局最优。</p><p>综上所述，算法正确。</p><h2 id="6-复杂度分析-complexity"><a class="markdownIt-Anchor" href="#6-复杂度分析-complexity"></a> 6. 复杂度分析 (Complexity)</h2><ul><li><strong>时间复杂度</strong>：$O\big((n+m) \log n + q\big)$。ST 表建表 $5$ 个 $A$ 表加 $2$ 个 $B$ 表，每个 $O(n \log n)$，约 $7 \times 10^5 \times 17 \approx 1.2 \times 10^7$ 次运算；每次查询 $O(1)$，$q$ 次共 $10^5$。1s 时限内轻松通过。</li><li><strong>空间复杂度</strong>：$O(n \log n)$。$7$ 个 ST 表，每个表 $10^5 \times 20 \times 8$ 字节 $\approx 16$ MB，共约 $112$ MB，低于 $512$ MB 限制。</li></ul><h2 id="7-实现细节与避坑指南-implementation-details"><a class="markdownIt-Anchor" href="#7-实现细节与避坑指南-implementation-details"></a> 7. 实现细节与避坑指南 (Implementation Details)</h2><table><thead><tr><th style="text-align:left">坑点</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left"><strong>整数溢出</strong></td><td style="text-align:left">$|a_i|, |b_i| \le 10^9$，乘积最大 $10^{18}$，必须用 <code>long long</code>。代码用 <code>#define int long long</code> 统一处理，是 OI 常用习惯。</td></tr><tr><td style="text-align:left"><strong>哨兵值设计</strong></td><td style="text-align:left"><code>inf = 2e9</code> 大于所有合法 $a_i$，<code>-inf</code> 小于所有合法 $a_i$，用作「不存在」标记。乘积不会触及 $\pm 2 \times 10^{18}$，<code>ans</code> 初始化为 $-2 \times 10^{18}$ 安全。</td></tr><tr><td style="text-align:left"><strong>正/负数表的哨兵混用</strong></td><td style="text-align:left"><code>a_max_p</code> 用 <code>max(a[i], 0)</code>（无正数时返回 $0$，配合 <code>L &gt; 0</code> 判定）；<code>a_min_p</code> 用 <code>inf</code> 哨兵（无正数时返回 <code>inf</code>，配合 <code>L != inf</code> 判定）。两种风格混用但逻辑自洽：夹 $0$ 的表靠符号判定，<code>inf</code> 哨兵的表靠显式比较。</td></tr><tr><td style="text-align:left"><strong>判定候选有效性</strong></td><td style="text-align:left">每个候选取出后必须检查是否「真的存在」：负数候选查 <code>L &lt; 0 &amp;&amp; L != -inf</code>，正数候选查 <code>L &gt; 0 &amp;&amp; L != inf</code>，避免用哨兵值参与乘法。</td></tr><tr><td style="text-align:left"><strong>$\log_2$ 预处理</strong></td><td style="text-align:left">代码预处理 <code>lg2</code> 数组到 $10^5$，查询时 $O(1)$ 取 $k$，避免每次调用库函数。</td></tr></tbody></table><h2 id="8-参考代码-reference-code"><a class="markdownIt-Anchor" href="#8-参考代码-reference-code"></a> 8. 参考代码 (Reference Code)</h2><p>下面是我按「先分析小 L 的选数策略，再推导小 Q 的最优反应」这一直觉顺序写出的 ST 表解法。维护 $A$ 的五个区间量（最大/最小正数、最大/最小负数、是否含 $0$）与 $B$ 的最大/最小值，每次查询枚举四个极值候选加 $0$ 取 $\max$。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;bits/stdc++.h&gt;</span></span></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std;</span><br><span class="line"><span class="meta">#<span class="keyword">define</span> int long long</span></span><br><span class="line"></span><br><span class="line"><span class="type">const</span> <span class="type">int</span> N = <span class="number">1e5</span> + <span class="number">5</span>, inf = <span class="number">2e9</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> n, m, q;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> a[N], b[N];</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> a_max_p[N][<span class="number">20</span>], a_min_p[N][<span class="number">20</span>], a_min_n[N][<span class="number">20</span>], a_max_n[N][<span class="number">20</span>];</span><br><span class="line"><span class="type">int</span> b_max[N][<span class="number">20</span>], b_min[N][<span class="number">20</span>];</span><br><span class="line"><span class="type">int</span> a_0[N][<span class="number">20</span>];</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> lg2[N];</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> pow2(x) (1&lt;&lt;(x))</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">ST_init</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>;i &lt;= n;i++) &#123;</span><br><span class="line">        a_max_p[i][<span class="number">0</span>] = <span class="built_in">max</span>(a[i], <span class="number">0LL</span>);</span><br><span class="line">        a_min_p[i][<span class="number">0</span>] = ((a[i] &gt; <span class="number">0</span>) ? a[i] : inf);</span><br><span class="line">        a_max_n[i][<span class="number">0</span>] = ((a[i] &lt; <span class="number">0</span>) ? a[i] : -inf);</span><br><span class="line">        a_min_n[i][<span class="number">0</span>] = <span class="built_in">min</span>(a[i], <span class="number">0LL</span>);</span><br><span class="line">        a_0[i][<span class="number">0</span>] = (a[i] == <span class="number">0</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> j = <span class="number">1</span>;j &lt;= lg2[n];j++) &#123;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>;i + <span class="built_in">pow2</span>(j) - <span class="number">1</span> &lt;= n;i++) &#123;</span><br><span class="line">            a_max_p[i][j] = <span class="built_in">max</span>(a_max_p[i][j - <span class="number">1</span>], a_max_p[i + <span class="built_in">pow2</span>(j - <span class="number">1</span>)][j - <span class="number">1</span>]);</span><br><span class="line">            a_min_p[i][j] = <span class="built_in">min</span>(a_min_p[i][j - <span class="number">1</span>], a_min_p[i + <span class="built_in">pow2</span>(j - <span class="number">1</span>)][j - <span class="number">1</span>]);</span><br><span class="line">            a_max_n[i][j] = <span class="built_in">max</span>(a_max_n[i][j - <span class="number">1</span>], a_max_n[i + <span class="built_in">pow2</span>(j - <span class="number">1</span>)][j - <span class="number">1</span>]);</span><br><span class="line">            a_min_n[i][j] = <span class="built_in">min</span>(a_min_n[i][j - <span class="number">1</span>], a_min_n[i + <span class="built_in">pow2</span>(j - <span class="number">1</span>)][j - <span class="number">1</span>]);</span><br><span class="line">            a_0[i][j] = a_0[i][j - <span class="number">1</span>] | a_0[i + <span class="built_in">pow2</span>(j - <span class="number">1</span>)][j - <span class="number">1</span>];</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>;i &lt;= m;i++) &#123;</span><br><span class="line">        b_max[i][<span class="number">0</span>] = b_min[i][<span class="number">0</span>] = b[i];</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> j = <span class="number">1</span>;j &lt;= lg2[m];j++) &#123;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>;i + <span class="built_in">pow2</span>(j) - <span class="number">1</span> &lt;= m;i++) &#123;</span><br><span class="line">            b_max[i][j] = <span class="built_in">max</span>(b_max[i][j - <span class="number">1</span>], b_max[i + <span class="built_in">pow2</span>(j - <span class="number">1</span>)][j - <span class="number">1</span>]);</span><br><span class="line">            b_min[i][j] = <span class="built_in">min</span>(b_min[i][j - <span class="number">1</span>], b_min[i + <span class="built_in">pow2</span>(j - <span class="number">1</span>)][j - <span class="number">1</span>]);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">ST_query_max</span><span class="params">(<span class="type">int</span> st[][<span class="number">20</span>], <span class="type">int</span> l, <span class="type">int</span> r)</span> </span>&#123;</span><br><span class="line">    <span class="type">int</span> k = lg2[r - l + <span class="number">1</span>];</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">max</span>(st[l][k], st[r - <span class="built_in">pow2</span>(k) + <span class="number">1</span>][k]);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">ST_query_min</span><span class="params">(<span class="type">int</span> st[][<span class="number">20</span>], <span class="type">int</span> l, <span class="type">int</span> r)</span> </span>&#123;</span><br><span class="line">    <span class="type">int</span> k = lg2[r - l + <span class="number">1</span>];</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">min</span>(st[l][k], st[r - <span class="built_in">pow2</span>(k) + <span class="number">1</span>][k]);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">signed</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="comment">// Don&#x27;t stop. Don&#x27;t hide. Follow the light, and you&#x27;ll find tomorrow.</span></span><br><span class="line"></span><br><span class="line">    <span class="built_in">scanf</span>(<span class="string">&quot;%lld %lld %lld\n&quot;</span>, &amp;n, &amp;m, &amp;q);</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">2</span>;i &lt;= <span class="number">100000</span>;i++) &#123;</span><br><span class="line">        lg2[i] = lg2[i / <span class="number">2</span>] + <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>;i &lt;= n;i++) <span class="built_in">scanf</span>(<span class="string">&quot;%lld &quot;</span>, &amp;a[i]);</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>;i &lt;= m;i++) <span class="built_in">scanf</span>(<span class="string">&quot;%lld &quot;</span>, &amp;b[i]);</span><br><span class="line"></span><br><span class="line">    <span class="built_in">ST_init</span>();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>;i &lt;= q;i++) &#123;</span><br><span class="line">        <span class="type">int</span> l1, r1, l2, r2;</span><br><span class="line">        <span class="built_in">scanf</span>(<span class="string">&quot;%lld %lld %lld %lld\n&quot;</span>, &amp;l1, &amp;r1, &amp;l2, &amp;r2);</span><br><span class="line">        <span class="type">int</span> ans = <span class="number">-2e18</span>, L, Q;</span><br><span class="line">        <span class="keyword">if</span> (<span class="built_in">ST_query_max</span>(a_0, l1, r1)) ans = <span class="number">0</span>;</span><br><span class="line">        <span class="comment">// L 选一个最小的负数</span></span><br><span class="line">        L = <span class="built_in">ST_query_min</span>(a_min_n, l1, r1);</span><br><span class="line">        <span class="keyword">if</span> (L &lt; <span class="number">0</span> &amp;&amp; L != -inf) &#123;</span><br><span class="line">            Q = <span class="built_in">ST_query_max</span>(b_max, l2, r2);</span><br><span class="line">            ans = <span class="built_in">max</span>(ans, L * Q);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// L 选一个最大的正数</span></span><br><span class="line">        L = <span class="built_in">ST_query_max</span>(a_max_p, l1, r1);</span><br><span class="line">        <span class="keyword">if</span> (L &gt; <span class="number">0</span> &amp;&amp; L != inf) &#123;</span><br><span class="line">            Q = <span class="built_in">ST_query_min</span>(b_min, l2, r2);</span><br><span class="line">            ans = <span class="built_in">max</span>(ans, L * Q);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// L 选一个最大的负数</span></span><br><span class="line">        L = <span class="built_in">ST_query_max</span>(a_max_n, l1, r1);</span><br><span class="line">        <span class="keyword">if</span> (L &lt; <span class="number">0</span> &amp;&amp; L != -inf) &#123;</span><br><span class="line">            Q = <span class="built_in">ST_query_max</span>(b_max, l2, r2);</span><br><span class="line">            ans = <span class="built_in">max</span>(ans, L * Q);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// L 选一个最小的正数</span></span><br><span class="line">        L = <span class="built_in">ST_query_min</span>(a_min_p, l1, r1);</span><br><span class="line">        <span class="keyword">if</span> (L &gt; <span class="number">0</span> &amp;&amp; L != inf) &#123;</span><br><span class="line">            Q = <span class="built_in">ST_query_min</span>(b_min, l2, r2);</span><br><span class="line">            ans = <span class="built_in">max</span>(ans, L * Q);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;%lld\n&quot;</span>, ans);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="9-补充说明-additional-notes"><a class="markdownIt-Anchor" href="#9-补充说明-additional-notes"></a> 9. 补充说明 (Additional Notes)</h2><ul><li><strong>题目渊源</strong>：本题出自 <strong>CSP-S 2022 第二轮</strong> 第二题，是「min-max 博弈 + ST 表」的经典结合。这类「先手最大化、后手最小化」的零和博弈在竞赛中很常见，核心识别信号是双层 $\max\text{-}\min$ 嵌套——出现时先固定先手选择，分析后手的最优反应函数，往往能将后手的连续选择域压缩到少数极值上。</li><li><strong>思路与代码的简化</strong>：我在初步分析时曾想细分 $B$ 区间的正负组成（只含负数、只含正数、含 $0$、正负混合），对应维护 $B$ 的最大正数、最大非正数、最小负数、最小非负数。但推导后发现，小 Q 的最优 $b$ 选择只取决于 $a$ 的符号——$a$ 负选 $b_{\max}$，$a$ 正选 $b_{\min}$——与 $B$ 的正负组成无关。因此代码只需要 $b_{\max}$ 和 $b_{\min}$ 两个量，初步分析中的复杂分类是推导过程的中间产物，最终被统一掉了。这种「分析时多想几步、实现时只保留必要部分」是常见的解题节奏。ST 表作为静态区间极值查询的经典工具，在这类需要 $O(1)$ 查询多个极值的问题中几乎是首选，方法经典，有学习的价值。</li><li><strong>其他方法</strong>：线段树等区间数据结构也可以解决 RMQ 问题，但在此类场景中 ST 表更优。</li></ul>]]>
    </content>
    <id>http://ttzc.github.io/2983d0b1/</id>
    <link href="http://ttzc.github.io/2983d0b1/"/>
    <published>2026-07-21T16:00:00.000Z</published>
    <summary>CSP-S 2022 min-max 博弈题，利用符号分类与 ST 表预处理区间极值，将 O(qnm) 枚举优化至 O(q log n)，实现双方最优策略下的乘积值查询。</summary>
    <title>洛谷 P8818 策略游戏 - Solution</title>
    <updated>2026-07-21T16:00:00.000Z</updated>
  </entry>
</feed>
