<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Posts on 我的小站</title><link>https://icurious-d.github.io/posts/</link><description>Recent content in Posts on 我的小站</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Thu, 06 Aug 2026 13:52:33 +0800</lastBuildDate><atom:link href="https://icurious-d.github.io/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>leetcode3345 赛博地牢：乘积谜锁</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-08-06/</link><pubDate>Thu, 06 Aug 2026 13:52:33 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-08-06/</guid><description>&lt;h2 id="赛博地牢乘积谜锁">赛博地牢：乘积谜锁
&lt;/h2>&lt;h4 id="序章地精守门">序章：地精守门
&lt;/h4>&lt;p>你刚传送进地牢，就看见一只绿皮地精翘着二郎腿坐在石门上。&lt;br>
门上的铭文闪烁着阴森的蓝光：&lt;/p>
&lt;blockquote>
&lt;p>“从 &lt;code>n&lt;/code> 开始，找到&lt;strong>大于等于&lt;/strong>它的最小整数，让它的&lt;strong>各位数字相乘&lt;/strong>，能被 &lt;code>t&lt;/code> 整除。&lt;br>
找不到？那就留下来给我当抱枕吧～”&lt;/p>&lt;/blockquote>
&lt;p>你后背一凉，下意识低头看向腰间——那里挂着一把新手村祖传铁剑，剑身上歪歪扭扭刻着两行字：&lt;/p>
&lt;blockquote>
&lt;p>while True:
砍&lt;/p>&lt;/blockquote>
&lt;p>地精顺着你的目光瞟了一眼，噗嗤笑出声来：“哈！又是一个想用暴力枚举硬刚的菜鸟。你想超时吗？你想让服务器冒烟吗？！复杂度懂不懂？！优雅懂不懂？！”&lt;/p>
&lt;p>你挠挠头：“等等，你这 n 最大才 100？”&lt;/p>
&lt;p>地精脸一僵。&lt;/p>
&lt;p>你拔剑出鞘：“那优雅个锤子，看爷暴力！”&lt;/p>
&lt;hr>
&lt;h4 id="第一章暴力连斩">第一章：暴力连斩
&lt;/h4>&lt;p>你的策略简单到让地精想给产品经理打差评：&lt;/p>
&lt;ol>
&lt;li>从 n 开始，一个一个数字试。&lt;/li>
&lt;li>把当前数字拆成各个数位，算它们的乘积。&lt;/li>
&lt;li>如果乘积能被 t 整除，开门！不然就试下一个。&lt;/li>
&lt;/ol>
&lt;p>因为数据范围小得离谱（n ≤ 100），你完全可以像推土机一样碾过去。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">smallestNumber&lt;/span>(n: int, t: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span>: &lt;span style="color:#75715e"># 无限连斩，直到门碎&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> product &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> x &lt;span style="color:#f92672">in&lt;/span> str(n):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> product &lt;span style="color:#f92672">*=&lt;/span> int(x)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> product &lt;span style="color:#f92672">%&lt;/span> t &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>: &lt;span style="color:#75715e"># 哇！金色传说！整除啦！&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> n &lt;span style="color:#75715e"># 门开了，拜拜地精&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 没开？下一个数字上来挨砍&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>你一路砍瓜切菜，地精终于忍不住了：“停停停！！你这什么破算法？就硬砍？？你的时间复杂度呢？你的数学直觉呢？？”&lt;/p>
&lt;p>你手上剑招不停，回头冲他咧嘴一笑：“&lt;code>while True&lt;/code> 代表我对暴力学的绝对信仰。数据量小就是可以为所欲为，这是算法界的牛顿第一定律，懂？ ”&lt;/p>
&lt;hr>
&lt;h4 id="第二章带0必杀">第二章：带0必杀
&lt;/h4>&lt;p>地精被你怼得绿脸涨成紫茄子，心态彻底崩了。它从石门上一跃而下，双手结印，浑身冒出红光，脑门上弹出一个明晃晃的状态栏：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>【二阶段·狂暴】&lt;/strong>
技能描述：把数据范围扩到 10⁶ ，让这个嚣张的人类砍到手断！&lt;/p>&lt;/blockquote>
&lt;p>法阵炸开，石门上的铭文一阵鬼畜般扭曲。地精喘着粗气，嘴角挂着反派专属的歪嘴笑：“来啊！继续暴力啊！10⁶ 的范围，够你砍到版本更新了吧？等着超时吧凡人！”&lt;/p>
&lt;p>他双臂交叉，等着看你面露难色、手忙脚乱翻算法书的样子。&lt;/p>
&lt;p>你愣了一下，然后仰天长笑：“哈哈哈哈哈——你可真是个小可爱！！” 你擦了擦笑出来的眼泪：“告诉你一个小秘密——从 n 开始往上找，最多试 &lt;strong>10 个数字&lt;/strong>，我的循环自己就结束了。&amp;quot;&lt;/p>
&lt;p>“10 次？！”地精尖叫，“10⁶ 的范围你 10 次搞定？？你当我是史莱姆吗这么好骗？！”&lt;/p>
&lt;p>你指着他的鼻子开始精准输出：&lt;/p>
&lt;blockquote>
&lt;p>“在一段连续的整数里，每 10 个必定踩到一个个位是 0 的。
而数字里一旦带个 0，乘积直接变成 0——0 能被任何 t 整除！&lt;br>
这个 0，就是我通关的万能钥匙。”&lt;/p>&lt;/blockquote>
&lt;p>地精低头看了看自己画的狂暴法阵。法阵发出一声闷响——&amp;ldquo;嘟——&amp;quot;，然后像放屁一样散了。&lt;/p>
&lt;p>他&amp;quot;扑通&amp;quot;一声跪倒在地，眼神涣散，嘴里念叨着什么&amp;quot;不可能&amp;quot;&amp;ldquo;带 0 不算&amp;quot;之类的话。&lt;/p>
&lt;p>你把铁剑往肩上一扛：“狂暴开完了？那让个路呗？”&lt;/p>
&lt;hr>
&lt;h4 id="第三章另一扇门">第三章：另一扇门
&lt;/h4>&lt;p>石门轰然碎裂，碎石在半空中化作像素粒子消散——标准的 dungeon 通关特效，你见过一百次了。&lt;/p>
&lt;p>你正准备摆个帅气的 pose 等传送门出现，却发现碎石后面露出来的，不是通往下一层的楼梯。&lt;/p>
&lt;p>是另一扇门。&lt;/p>
&lt;p>一扇通体漆黑、镶着血红纹路的黑曜石门。门缝里渗出暗红色的光，像是什么东西正在门后面缓慢地呼吸。&lt;/p>
&lt;p>你凑近一看，门楣上刻着新的铭文，字体比刚才那扇门大了三号，还加粗了——生怕你看不见似的：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>【隐藏关卡·已解锁】&lt;/strong>
给你一个字符串 &lt;code>num&lt;/code>，表示一个&lt;strong>巨大&lt;/strong>的整数，和一个整数 &lt;code>t&lt;/code>。
找出&lt;strong>大于等于 num&lt;/strong> 的最小&lt;strong>无零整数&lt;/strong>，它的各位之积能被 &lt;code>t&lt;/code> 整除。
如果不存在，返回 &lt;code>&amp;quot;-1&amp;quot;&lt;/code>。&lt;/p>&lt;/blockquote>
&lt;p>旁边还有一行小字，像是在警告每一个试图忽视它的人：&lt;/p>
&lt;blockquote>
&lt;p>请睁大你的双眼：该整数 &lt;strong>禁止出现数字 0&lt;/strong>。&lt;br>
违规者——将被地牢意志直接抹除。&lt;/p>&lt;/blockquote>
&lt;p>你感觉脊背发凉。刚刚的“万能钥匙” 0 被封杀了。&lt;br>
而且这次输入的是&lt;strong>字符串&lt;/strong>，意味着 num 可以是几百位、几千位&amp;hellip;甚至 20 万位的天文数字。还可能无解，要返回 &amp;ldquo;-1&amp;rdquo;（之前可是保证有解的啊！）&lt;/p>
&lt;p>你低头看了一眼手中的铁剑，剑身上那行 &lt;code>while True&lt;/code> 还在闪。但你清楚地意识到——面对这扇门，暴力连砍是没用的——对面直接开了物理免疫。&lt;/p>
&lt;p>地牢深处传来一声低沉的笑——不是地精那种尖嗓门，是一种浑厚的、带着混响的笑，听起来就像某个退隐多年的魔王被吵醒了午觉，正在翻动自己的技能面板。&lt;/p>
&lt;p>你握紧剑柄——寒意顺着指尖爬上来，和黑曜石门渗出的暗红光芒混在一起。&lt;/p>
&lt;p>真正的挑战，才刚刚开始。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3310 深夜运维惊魂：那个叫k的方法有毒</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-08-05/</link><pubDate>Wed, 05 Aug 2026 12:31:04 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-08-05/</guid><description>&lt;h2 id="深夜运维惊魂那个叫k的方法有毒">深夜运维惊魂：那个叫k的方法有毒
&lt;/h2>&lt;p>凌晨三点，你正做着美梦——梦见自己成了技术总监，手下管着100个程序员，每个人都在996，只有你在摸鱼。&lt;/p>
&lt;p>突然，手机炸响。屏幕上赫然两个字：老板。&lt;/p>
&lt;p>你心脏骤停半秒，还没来得及说“喂”，那头已经咆哮如雷：“李码！线上崩了！运维说是方法 k 炸的，这方法是不是你写的？！”&lt;/p>
&lt;p>你瞬间清醒，脑子里一万个 &lt;code>console.log(&amp;quot;卧槽&amp;quot;)&lt;/code> 疯狂刷屏。&lt;strong>又是k！为什么每次都是k？&lt;/strong> 上次是变量k，这次是方法k，下次键盘上 K 键坏了是不是也要算你头上？&lt;/p>
&lt;hr>
&lt;h4 id="一方法的社交圈">一：方法的社交圈
&lt;/h4>&lt;p>你打开IDE，眼前是一片代码屎山。&lt;/p>
&lt;p>n个方法，编号从&lt;code>0&lt;/code>到&lt;code>n-1&lt;/code>，像极了你们公司的组织架构——看似井然有序，实则一团乱麻。&lt;/p>
&lt;p>调用关系写在一个叫 &lt;code>invocations&lt;/code> 的二维数组里，&lt;code>invocations[i] = [x, y]&lt;/code> 表示方法 x 调用了方法 y。&lt;/p>
&lt;p>这不就是方法的社交网络吗？你迅速建了个有向图，锁定每个方法的狐朋狗友。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>graph &lt;span style="color:#f92672">=&lt;/span> [[] &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n)] &lt;span style="color:#75715e"># 我调用了谁&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> x, y &lt;span style="color:#f92672">in&lt;/span> invocations:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> graph[x]&lt;span style="color:#f92672">.&lt;/span>append(y)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h4 id="二标记k家军">二：标记&amp;quot;k家军&amp;quot;
&lt;/h4>&lt;p>已知方法 &lt;code>k&lt;/code> 有 bug，所有它&lt;strong>直接或间接调用&lt;/strong>的方法全部被视为成为“&lt;strong>可疑方法&lt;/strong>”。&lt;/p>
&lt;p>你盯着这行字，一时间有些恍惚——六年前，你也这样被标记过——那天，你只不过和对门邻居在楼道里打了个照面，就因为“时空伴随”喜提居家隔离七天，健康码红得像你写的错误日志，社区工作人员每天上门测核酸比你 CI 流水线跑得还准时。&lt;/p>
&lt;p>你苦笑一声，祭出 DFS 大法，从 &lt;code>k&lt;/code> 出发，沿着调用链挨个打码。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>suspicious &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> n &lt;span style="color:#75715e"># 标记数组，0是清白，1是可疑&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">dfs&lt;/span>(x: int):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> suspicious[x] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> y &lt;span style="color:#f92672">in&lt;/span> g[x]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">not&lt;/span> suspicious[y]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dfs(y)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dfs(k)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>DFS跑完，suspicious 数组里标为1的方法，就是传说中的**&amp;ldquo;k家军&amp;rdquo;**——一个都跑不掉。&lt;/p>
&lt;hr>
&lt;h4 id="三移除决策">三：移除决策
&lt;/h4>&lt;p>已经标记了所有&amp;quot;k家军&amp;quot;，下一步是不是直接 &lt;code>Ctrl+X&lt;/code> 一把梭？太天真了。&lt;/p>
&lt;p>安全手册第一条：删代码可以，崩系统不行。&lt;/p>
&lt;p>什么意思？只有当这组可疑方法没有被任何外部方法调用时，才能安全移除。如果圈外还有人深情地调着圈内的方法，那你强行删除就会导致 &lt;code>NullPointerException&lt;/code>，整个调用链像多米诺骨牌一样连环崩塌，明天的日报上你就能亲手写下“因误删可疑方法导致 P0 事故，影响用户 XXXXX 人，已深刻反省，主动申请取消年终奖。”&lt;/p>
&lt;p>你的手指悬在键盘上方，敲下了决定职业生涯能否存续的检查代码：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> x, y &lt;span style="color:#f92672">in&lt;/span> invocations:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">not&lt;/span> suspicious[x] &lt;span style="color:#f92672">and&lt;/span> suspicious[y]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 有外部依赖！原地摆烂&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> list(range(n)) &lt;span style="color:#75715e"># 全留着，一个不动，祖传屎山永流传&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果这段循环跑完都没触发return，那么，恭喜你——这个可疑方法集合就是一个&lt;strong>完美的隔离孤岛&lt;/strong>。你完全可以一键 &lt;code>rm -rf&lt;/code>，直接返回劫后余生的幸存者名单：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> [i &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(n) &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">not&lt;/span> suspicious[i]]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h4 id="彩蛋">彩蛋
&lt;/h4>&lt;p>事后你复盘此事，悟出一个真理：老板要删方法，先看有没有人保它。外部有调用，屎山也能免死；没外部调用，再干净也得滚蛋。&lt;/p>
&lt;p>你以为你刷的是图遍历？不，你刷的是职场生存学。&lt;/p>
&lt;blockquote>
&lt;p>如果你和你的小团队内部自嗨，从不跟外界交互——那裁员的刀落下来，你们就会被一锅端。&lt;/p>
&lt;p>但如果其他部门还死死依赖着你们……嘿嘿，就算你代码写得再烂，老板也得咬牙留你——这叫技术债绑架，或者，也叫组织的“不可或缺性”。&lt;/p>&lt;/blockquote>
&lt;p>所以，你是想当那个随时可被替换的干净方法，还是那个让老板不敢动的屎山核心？&lt;/p>
&lt;p>明天上班，记得把你的方法多暴露几个接口出去。保命。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3731 逃课逮捕令：从 list 遍历 到 Set 精准锁头</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-08-04/</link><pubDate>Tue, 04 Aug 2026 11:54:27 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-08-04/</guid><description>&lt;h2 id="逃课逮捕令从-list-遍历-到-set-精准锁头">&lt;strong>逃课逮捕令：从 list 遍历 到 Set 精准锁头&lt;/strong>
&lt;/h2>&lt;p>大家好，我是一只被导师临时抓来当助教的研二狗。&lt;br>
今天老板出差，留给我一张皱成抹布样的签到表，以及一句杀人诛心的话：&lt;/p>
&lt;blockquote>
&lt;p>“小李啊，这是本科班的课堂签到，1 到 50 号都在你手里了。把没来的揪出来，按学号排好发我。要是弄错一个，下周组会你替我做文献汇报。”&lt;/p>&lt;/blockquote>
&lt;p>我打开签到表一看，血压飙得和服务器并发一样高。上面字迹歪歪扭扭：&lt;/p>
&lt;p>&lt;code>[1, 25, 4, 18, 24, 9, 33, 15, 41, 50]&lt;/code>&lt;/p>
&lt;p>好家伙，签到顺序比我的论文提纲还乱，而且全班 50 号人，拢共只有 10 个签了到。最小号 1 和最大号 50 倒是在（这俩一个卷王一个学委，果然老实），其余的呢？？我随手一刷朋友圈，37号刚晒了张五杀截图，配文是“教室哪有峡谷香”——我瞬间明白了，这货现在八成窝在床上和原神深渊使徒激情对线。&lt;/p>
&lt;p>目标很明确，我现在需要做的就是：找出从 1 到 50 这个区间里，所有没出现在签到表上的学号，从小到大排好，一个不落，整整齐齐报给导师，让这些小兔崽子们接受命运——哦不，导师的审判。&lt;/p>
&lt;hr>
&lt;h4 id="黑历史--人工智障逐个扫描">黑历史 · 人工智障，逐个扫描
&lt;/h4>&lt;p>研一那会儿，我也曾被导师抓去当点名苦力。当时我还很天真，撸码常常堆循环，点名纯纯用爱发电——照着花名册，从 1 号开始，挨个在签到表里找。每查一个人都把整张表从头撸到尾，眼睛都快瞪成线性扫描仪了：“2号……不在。3号……也不在。4号——哦4号签了，下一个……”&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">findMissingElements&lt;/span>(nums: List[int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> List[int]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> [i &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(min(nums), max(nums) &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#66d9ef">if&lt;/span> i &lt;span style="color:#f92672">not&lt;/span> &lt;span style="color:#f92672">in&lt;/span> nums]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>列表的 &lt;code>in&lt;/code> 是 O(n) 线性扫描，外面再套一层 O(m) 的循环，合起来 O(n·m)。放到 OJ 上可能直接甩你一个 TLE。&lt;/p>
&lt;p>这就好比在王者峡谷里不用小地图，纯靠走路挨个草丛探视野：“这个草丛有人吗？没有。下一个草丛呢？我再跑一趟……”等你探完上路，对面已经把主宰打了三遍，你家水晶炸得连渣都不剩。&lt;/p>
&lt;p>最窒息的是，当我到宿舍抓人，37号正激情五杀，听到动静 Alt+Tab切桌面，转身挤出一个比bug还假的笑容：“学长你签到表有没有认真看？我名字签在背面——我回来拿个水杯！”&lt;/p>
&lt;hr>
&lt;h4 id="黑科技--哈希锁头一秒查人">黑科技 · 哈希锁头，一秒查人
&lt;/h4>&lt;p>痛定思痛后，我掏出了计算机专业祖传秘方——&lt;strong>哈希集合&lt;/strong>。&lt;/p>
&lt;p>原理很简单：把签到表上那十个幸运数字 &lt;code>[1,25,4,18,24,9,33,15,41,50]&lt;/code> 一股脑塞进一个 &lt;code>set&lt;/code>，取名 &lt;code>attended&lt;/code>。这玩意儿就是个 O(1) 查找的超级花名册——从此查人不用遍历，直接问哈希：“这学号在吗？”，速度追平食堂阿姨打菜手速。&lt;/p>
&lt;p>哈希点名，纯纯流水线作业：从 1 到 50 依次走过，每个学号对着 &lt;code>attended&lt;/code> 问一句“ 在里面吗？”，不在的直接甩进逃课列表。因为是按顺序问的，出来的结果天然升序，老板的要求一步到位。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">findMissingElements&lt;/span>(nums: List[int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> List[int]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> attended &lt;span style="color:#f92672">=&lt;/span> set(nums) &lt;span style="color:#75715e"># 签到数据一键导入哈希表&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> [i &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(min(nums), max(nums) &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#66d9ef">if&lt;/span> i &lt;span style="color:#f92672">not&lt;/span> &lt;span style="color:#f92672">in&lt;/span> attended]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>一趟流水线走完，没来的人自动列成一张干净整齐的缺勤清单，按学号从小到大排好，比老板的论文参考文献还规整。&lt;/p>
&lt;p>我把缺勤名单截图往班群一甩：&lt;code>[2,3,5,6,7,8,10,11,12,13,14,16,17,19,20,21,22,23,26,27,28,29,30,31,32,34,35,36,37,38,39,40,42,43,44,45,46,47,48,49]&lt;/code>&lt;/p>
&lt;p>群里安静了三秒。37号的头像弹出一个“？”：“不是学长，你这什么鬼名单，上次我签背面你没看见，这次我就上了个厕所——”&lt;/p>
&lt;p>我秒回：“厕所上了五十分钟？你的五杀截图还在我朋友圈挂着呢。下周组会你替我讲文献，pdf已私聊。”&lt;/p>
&lt;hr>
&lt;h4 id="彩蛋-逃课克星速查卡">彩蛋· 逃课克星速查卡
&lt;/h4>&lt;ul>
&lt;li>&lt;strong>List 遍历法&lt;/strong>：挨个遍历签到表，时间复杂度 O(n×m)，小班点名还能凑合，学号一多，比校园网抢课还卡。&lt;/li>
&lt;li>&lt;strong>Set 哈希法&lt;/strong>：签到数据一键导入哈希集合，O(1) 光速查人。时间 O(n+m)，多占的那点内存还没你手机里一款抽卡游戏大。&lt;/li>
&lt;/ul>
&lt;p>下次导师再让你“找缺失的元素”——不对，是找逃课的学生——别犹豫，直接 &lt;code>set&lt;/code> 安排上。用合适的数据结构修理不合适的出勤率，然后你就可以安心打开原神清个体力。毕竟，37 号在讲台上替你讲文献的样子，比深渊使徒可爱多了。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode1406 赌城风云·续：阿星的出师之战</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-08-03/</link><pubDate>Mon, 03 Aug 2026 11:54:37 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-08-03/</guid><description>&lt;h2 id="赌城风云续阿星的出师之战">赌城风云·续：阿星的出师之战
&lt;/h2>&lt;blockquote>
&lt;p>又名：《阿星的单飞考试：递归会流鼻血，滚动数组才是王道》
前情提要：上一局，你用&amp;quot;净胜分心法&amp;quot;碾压了欧洲赌魔查尔斯（）。他回去苦读《算法导论》，三个月后卷土重来。&lt;/p>&lt;/blockquote>
&lt;h4 id="一查尔斯的复仇">一、查尔斯的复仇
&lt;/h4>&lt;p>三个月后。同一张牌桌，同一盏水晶吊灯。&lt;/p>
&lt;p>查尔斯又来了。&lt;/p>
&lt;p>这一回他的金发梳得更亮，领带换成了深红色——据说是专门找人算过的“必胜色”。身后还是那四个保镖，但手里的书已经从《博弈论》换成了崭新的《算法导论》，书签统一夹在动态规划那一章。封面还贴着便利贴，上面写着：“注意最优子结构！！！”&lt;/p>
&lt;p>“三个月，”查尔斯站在牌桌对面，金丝眼镜换了一副，镜片更厚，反光更刺眼，“我在剑桥旁听了一整个学期的算法课——你的净胜分心法，我已经吃透了。”&lt;/p>
&lt;p>你坐在牌桌左边，手里转着一块新的还没拆锡纸的巧克力。听完这句话，你停下手里的转动，然后做了一个让全场倒吸一口凉气的动作——站起来，退后三步，把椅子让给了身后的阿星。&lt;/p>
&lt;p>“今天不是我跟你打。”你拍了拍阿星的肩膀，“你跟我徒弟打。”&lt;/p>
&lt;p>阿星正举着半瓶汽水，差点呛到：“我？！哥，我就是个跑龙套的！我连《算法导论》封面长什么样都不知道！”&lt;/p>
&lt;p>“你跟了我这么久，该出师了。”你把巧克力外面的锡纸剥开，掰下半块递给阿星。&lt;/p>
&lt;p>查尔斯先是愣了一下，然后笑出声：“让你徒弟来送分？好，正好——新规矩，新牌局。”&lt;/p>
&lt;p>他打了个响指。荷官推出一副新牌匣，牌面朝上，一字排开：&lt;code>[1, 3, 2, 50, 9, 90, 30, 90, 2, 8, 4]&lt;/code>。&lt;/p>
&lt;p>“规则升级了。”查尔斯推了推新眼镜，“还是轮流抽，但这一回——只能从最前面开始拿。每次可以拿走 1 张、2 张、或 3 张牌。拿走的牌面数字直接加进你的总分。所有牌拿完，分高的人赢。平局算 Tie。”&lt;/p>
&lt;p>他往椅背上一靠，嘴角挂着一丝“我连你徒弟的墓碑都想好写什么了”的微笑。&lt;/p>
&lt;p>“对了——为了公平起见，今天的先手，我们抽签决定。”他掏出一枚硬币，在指缝间转了一圈，“我也不欺负晚辈。抽签定先手，各自凭运气。”&lt;/p>
&lt;p>阿星还没来得及抗议，你已经在旁边的沙发上坐下了。包装巧克力的那张锡纸已经被你折成了一只纸鹤，立在茶几上，翅膀微微翘起，正对着牌桌。“阿星，我教你的东西，这三个月你琢磨了几回？”&lt;/p>
&lt;p>“呃……三回？”&lt;/p>
&lt;p>“够了。打吧。”&lt;/p>
&lt;hr>
&lt;h4 id="二阿星的独角戏">二、阿星的独角戏
&lt;/h4>&lt;p>阿星坐到牌桌左边，手心全是汗。他把牌从左到右看了三遍，嘴里开始念念有词。&lt;/p>
&lt;p>他在做一件每个初学者都会做的事——&lt;strong>暴力递归&lt;/strong>。&lt;/p>
&lt;p>“好，先定义一个函数 &lt;code>dfs(i)&lt;/code>，表示从第 &lt;code>i&lt;/code> 张牌开始先手，最多能比对手多拿多少分。”阿星掰着手指，声音越来越大，像是在给自己壮胆，“从第 &lt;code>i&lt;/code> 张开始，我可以拿最前面的 1 张、2 张、或 3 张。拿走的牌面总分记作 &lt;code>take&lt;/code>，留给对方的是 &lt;code>dfs(i+1)&lt;/code>、&lt;code>dfs(i+2)&lt;/code>、&lt;code>dfs(i+3)&lt;/code> 三种局面——”&lt;/p>
&lt;p>他在桌布上画了一个递归树，从 &lt;code>dfs(0)&lt;/code> 开始分叉，“&lt;code>dfs(0)&lt;/code> 等于 &lt;code>max(拿1张 - 对手dfs(1)的净胜, 拿2张 - 对手dfs(2)的净胜, 拿3张 - 对手dfs(3)的净胜)&lt;/code>……”。一叉分三叉，三叉分九叉——画到第三层的时候，桌布已经不够用了。他把袖口也翻出来接着画，画到第四层，指尖开始发抖，额头上冒出一层细汗。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>n &lt;span style="color:#f92672">=&lt;/span> len(stoneValue)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">@cache&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">dfs&lt;/span>(i):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> i &lt;span style="color:#f92672">==&lt;/span> n:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> best &lt;span style="color:#f92672">=&lt;/span> float(&lt;span style="color:#e6db74">&amp;#39;-inf&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> take &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(i, min(i &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">3&lt;/span>, n)):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> take &lt;span style="color:#f92672">+=&lt;/span> stoneValue[j]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> best &lt;span style="color:#f92672">=&lt;/span> max(best, take &lt;span style="color:#f92672">-&lt;/span> dfs(j &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> best
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>diff &lt;span style="color:#f92672">=&lt;/span> dfs(&lt;span style="color:#ae81ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Tie&amp;#34;&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> diff &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Xin&amp;#34;&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> diff &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Charles&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>阿星感觉递归树在他脑子里疯狂生长，每多一层就多三倍的分支，像一棵没人修剪的野树，枝杈戳得到处都是。&lt;/p>
&lt;p>突然——一滴红色的液体落在了桌布上，恰好滴在 &lt;code>dfs(2)&lt;/code> 的圆圈里。&lt;/p>
&lt;p>阿星流鼻血了。&lt;/p>
&lt;p>“哥！”他捂着鼻子转头看你，“递归太深了，我脑子要爆栈了！”&lt;/p>
&lt;p>查尔斯差点笑得从椅子上滑下去：“这就是赌神的徒弟？算了三分钟就开始流鼻血？”&lt;/p>
&lt;p>你放下茶杯，不紧不慢地走到阿星身边，从口袋里掏出一张纸巾，“谁让你用脑袋递归了？”&lt;/p>
&lt;p>你把纸巾按在他鼻子上，“三个月前教你的 DP 表呢？再说一遍——人类的大脑不适合递归，但可以填表。”&lt;/p>
&lt;hr>
&lt;h4 id="三从全表到滚动数组">三、从全表到滚动数组
&lt;/h4>&lt;p>“递归是从头往下分叉，分到最后自己都不知道自己在哪一层。”你又从桌上抽了张干净纸巾，盖住桌布上那团被鼻血渗透的 &lt;code>dfs(2)&lt;/code>，“填表不一样——从尾巴往前推。你想想，如果只剩下最后一张牌，你拿还是不拿？”&lt;/p>
&lt;p>“当然拿啊，”阿星捂着鼻子，瓮声瓮气地说，“不拿白不拿。”&lt;/p>
&lt;p>“对。最后一张的净胜就是它自己。那如果剩下最后两张呢？你有三个选择——拿一张，留一张给对手；拿两张，清盘。每个选择的净胜分用‘take - 对手在剩余牌面的净胜’来算，选最大的那个。就这样，从后往前，每一步都站在前一步已经算好的答案上，一层一层盖楼。”&lt;/p>
&lt;p>阿星想了想：“那就开一个 dp 数组，从 &lt;code>dp[n-1]&lt;/code> 一路算到 &lt;code>dp[0]&lt;/code>？&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>n &lt;span style="color:#f92672">=&lt;/span> len(stoneValue)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dp &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#f92672">-&lt;/span>float(&lt;span style="color:#e6db74">&amp;#39;inf&amp;#39;&lt;/span>)] &lt;span style="color:#f92672">*&lt;/span> n &lt;span style="color:#f92672">+&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#75715e"># 末尾多一个 0 当哨兵，越界返回 0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> take &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(i, min(i &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">3&lt;/span>, n)):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> take &lt;span style="color:#f92672">+=&lt;/span> stoneValue[j]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dp[i] &lt;span style="color:#f92672">=&lt;/span> max(dp[i], take &lt;span style="color:#f92672">-&lt;/span> dp[j &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Tie&amp;#34;&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> dp[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Xin&amp;#34;&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> dp[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Charles&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>“对——”，你点点头：“但还可以再省。”&lt;/p>
&lt;p>你用指尖划过阿星手边的汽水瓶——瓶壁上凝着一圈细密的水珠——就着那点凉意在纸巾上画下三个字符：&lt;code>i1, i2, i3&lt;/code>。“你注意到没有——算 &lt;code>dp[i]&lt;/code> 的时候，你只需要它后面的三个值(&lt;code>dp[i]&lt;/code> 只依赖 &lt;code>dp[i+1]&lt;/code>、&lt;code>dp[i+2]&lt;/code>、&lt;code>dp[i+3]&lt;/code>)。之前的值用完就可以扔了。所以——三个变量就够了，从后往前滚，空间压成 O(1)。”&lt;/p>
&lt;p>阿星瞪大眼睛：“三个变量？”&lt;/p>
&lt;p>“滚动数组——你只需要记住最近三个净胜分。”&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>n &lt;span style="color:#f92672">=&lt;/span> len(stoneValue)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>i1 &lt;span style="color:#f92672">=&lt;/span> i2 &lt;span style="color:#f92672">=&lt;/span> i3 &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#75715e"># 分别存 dp[i+1], dp[i+2], dp[i+3]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(n &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> take &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> best &lt;span style="color:#f92672">=&lt;/span> float(&lt;span style="color:#e6db74">&amp;#39;-inf&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(i, min(i &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">3&lt;/span>, n)):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> take &lt;span style="color:#f92672">+=&lt;/span> stoneValue[j]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> j &lt;span style="color:#f92672">==&lt;/span> i:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> opponent &lt;span style="color:#f92672">=&lt;/span> i1
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> j &lt;span style="color:#f92672">==&lt;/span> i &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> opponent &lt;span style="color:#f92672">=&lt;/span> i2
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> opponent &lt;span style="color:#f92672">=&lt;/span> i3
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> best &lt;span style="color:#f92672">=&lt;/span> max(best, take &lt;span style="color:#f92672">-&lt;/span> opponent)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> i1, i2, i3 &lt;span style="color:#f92672">=&lt;/span> best, i1, i2
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Tie&amp;#34;&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> i1 &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Xin&amp;#34;&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> i1 &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Charles&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>阿星攥紧了纸巾，盯着那三个变量在心里跑流程。他的嘴唇翕动着，从最后一堆开始往前滚——&lt;code>i3&lt;/code> 是越界的 0，&lt;code>i2&lt;/code> 是最后一张的净胜，&lt;code>i1&lt;/code> 是倒数第二张的净胜——三个数字像三个齿轮，一步一步从尾巴咬到头。&lt;/p>
&lt;p>滚到 &lt;code>i=0&lt;/code> 的时候，他停住了。&lt;/p>
&lt;p>“i1 等于……”他抬头看你，声音压到只有你能听见：“——负的，全局先手必输。”&lt;/p>
&lt;p>他抬头看你，声音压到只有你能听见：“哥，净胜分是负数，全局先手必输。Alice 在这个数组上拿先手，净胜是负的。”&lt;/p>
&lt;p>你端起茶杯，吹了吹浮在面上的茶叶，没说话。&lt;/p>
&lt;p>阿星看着你那个表情，忽然想起了三个月前你说的话——“在游戏开始前，就把结局算死”——他明白了：算牌只是第一步，把算出来的结局钉死在牌桌上，才是真正的考验。&lt;/p>
&lt;hr>
&lt;h4 id="四欲盖弥彰">四、欲盖弥彰
&lt;/h4>&lt;p>阿星深吸了一口气，把纸巾揉成一团塞进裤兜，然后仰头看了会儿天花板，让鼻血彻底凝固。他需要这几秒钟——不是因为鼻子，是因为他得把“先手必输”四个大字从脸上抹干净，换上一张无辜的、甚至有点得意的脸。&lt;/p>
&lt;p>然后——他开始笑。&lt;/p>
&lt;p>笑得不大，但刚好能让对面听到。嘴角一抽一抽的，像是发现了什么不得了的秘密，又拼命想藏住。&lt;/p>
&lt;p>查尔斯正用眼镜布擦镜片，手停了。他盯了阿星两秒，那两秒里阿星做了全套——先飞快地把笑容收回去，收得太快，像被抓包；然后把目光移开，假装研究天花板上的吊灯，眼珠子转得过于刻意；最后用袖子漫不经心地往桌布上一盖，恰好盖住那片被汽水字迹和鼻血交替浸过的纸巾碎片。&lt;/p>
&lt;p>“你笑什么？”查尔斯把眼镜戴回去。&lt;/p>
&lt;p>“没什么没什么，鼻子通了，高兴。”阿星站起来，身体不自觉地挡在桌布前面。一只手在背后悄悄把裤兜里那团揉皱的纸巾往里塞了塞，动作大得生怕对面四个保镖有一个没看到。&lt;/p>
&lt;p>果然，一个保镖低头在查尔斯耳边说了句什么。查尔斯的眉头慢慢皱了起来。他用手指敲着桌面，“你的表情，就像算出了不得了的东西——你在算先手还是后手赢，对不对？”&lt;/p>
&lt;p>阿星的表情僵了一瞬——这一瞬的僵硬，足够让查尔斯确认自己猜对了。&lt;/p>
&lt;p>“阿星，”查尔斯往前倾了倾身子，金丝眼镜后的目光像两颗瞄准激光，“你刚才是不是算出来，先手必赢？”&lt;/p>
&lt;p>阿星的瞳孔微微放大。这个表情就像被人当众翻开底牌——不，像是被人猜中了自己正在拼命隐藏的结论。他飞快地摇头：“不是不是，我什么都没算出来……递归太深，我流鼻血了，脑子不清醒……先手后手什么的，我觉得应该差不多吧，五五开，运气最重要。”&lt;/p>
&lt;p>“五五开？你刚才算到流鼻血，然后就开始笑，现在又跟我说五五开？”查尔斯眯起了眼睛，“你在说谎。”&lt;/p>
&lt;p>阿星的额头上沁出了一层细密的汗。骗一个在剑桥旁听过一学期算法的聪明人，每一步都不能踏错。假话要混在真话里说，表情要半真半假，最难的是眼神——你不能直视他，但也不能完全躲开。你得让他觉得你在躲，但又让他恰好能捕捉到你“不小心”漏出来的破绽。&lt;/p>
&lt;p>“查尔斯先生，”阿星突然站直了，语气变得异常认真，仿佛刚才的慌张全是铺垫，现在才是真情流露，“今天我师父不在桌上，我就是来练手的。你先手的话太欺负人了——你算法课上了三个月，我才琢磨了三回。这局让我先手，公平点。”&lt;/p>
&lt;p>他说“公平点”三个字的时候，眼神里甚至带着一丝恳求，嘴角用力抿着，像在做最后的挣扎——明明知道对方不可能答应，还是硬着头皮把话说出来，试图用道德绑架一个没有道德的欧洲赌魔。&lt;/p>
&lt;p>查尔斯看着阿星那张真诚的脸，慢慢笑了。先是嘴角，然后是眼角，最后整个肩膀都在抖。他等这一刻等了三个月。三个月前他被先手的一张 1 玩死，回去以后在笔记本上写了三百遍——“先手才是庄家。后手只能被动反应。”&lt;/p>
&lt;p>“我先手。”查尔斯一锤定音。&lt;/p>
&lt;p>“等一下！让我先手！”阿星的肩膀轻微地抖了一下，像是最后一根弦被扯断了，他张着嘴，手还僵在半空中，语气低落得像是被抽走了半条命，“……你连硬币都不抛了？”&lt;/p>
&lt;p>“硬币？”查尔斯把硬币收回胸口的西装口袋，拍了两下，金丝眼镜后的眼睛弯成了两道得意的弧线，“不需要了。你的表情，你的小动作，你拼命想藏起来的那团纸巾——每个信号都指向同一件事：你算出来了先手必赢，你想抢那个先手。既然你想抢——我为什么要让给你？”&lt;/p>
&lt;hr>
&lt;h4 id="五注定的结局">五、注定的结局
&lt;/h4>&lt;p>接下来的每一轮里，阿星只是安静地拿牌。每一轮都拿得不快不慢，没有得意，也没有犹豫。他不需要再算了——整局的结果在查尔斯拿先手的那一刻就已经钉死了。&lt;/p>
&lt;p>牌局很快结束。荷官按下计算器，报出两个总分。&lt;/p>
&lt;p>“怎么可能——” 查尔斯的脸色从红变白，从白变青。他低头看着桌上的牌，又抬头看着阿星，嘴张了两次都没再发出声音。然后他猛地站起来，椅子在身后发出刺耳的摩擦声。&lt;/p>
&lt;p>“你不是流鼻血了吗——不对，你从一开始——”查尔斯的手指在阿星和牌桌之间来回弹跳，像一台卡壳的打字机，“你从一开始就知道了！全局先手必输！但你故意让我以为先手必赢。你刚才那个表情——那个藏桌布、藏纸巾、求我让你先手的表情——全是演的！！”&lt;/p>
&lt;p>阿星缓缓站起来，从裤兜里掏出那张揉皱的纸巾，展开铺平在牌桌上。上面还留着汽水干透后浅浅的水渍。&lt;/p>
&lt;p>“我师父三个月前教我的——在游戏开始前，就把结局算死。”阿星把纸巾推到牌桌中央，“净胜分心法第二条——你抢过去的先手，可能恰好是我算好了要送给你的。”&lt;/p>
&lt;hr>
&lt;h4 id="六赌神的进阶">六、赌神的进阶
&lt;/h4>&lt;p>查尔斯被四个保镖架走了。临走时嘴里还在念叨“净胜分”和“先手后手”，金丝眼镜断成两截挂在耳朵上，像一对垂头丧气的翅膀。&lt;/p>
&lt;p>阿星一直目送查尔斯的背影消失在旋转门后面，才长长地吐了一口气。他把牌桌上的汽水端起来咕咚咕咚灌了半瓶，然后转头看向你，“哥，刚才那段演技，我自己都有点没底——藏纸巾那一下是不是演得太用力了？”&lt;/p>
&lt;p>“演技确实是浮夸了点——但逻辑没问题，你赢了。”你从沙发上站起来，走到牌桌旁边，“但是——净胜分的视角，还不够简洁。”&lt;/p>
&lt;p>阿星一愣。&lt;/p>
&lt;p>“净胜分确实没错。能赢。但还不够简洁。”你在纸上画了一个新的结构，“你刚才一直算的是‘我比你多赢多少’，但你有没有想过——直接算‘我能拿多少分’？”&lt;/p>
&lt;p>阿星一愣：“不够简洁？不是都已经把空间压到三个变量了吗？”&lt;/p>
&lt;p>“不是在空间上。是在思路上。”你在牌桌对面坐下，“你每次轮到你拿牌的时候，是不是要在心里跑一遍内层循环？拿一堆算一遍 take - opponent，拿两堆算一遍，拿三堆再算一遍，三个分支比完才敢下手。”&lt;/p>
&lt;p>阿星想了想，点头：“确实。每次到我，我都得在脑子里过三个选项。虽然能算出来，但总觉得中间多了一层——像脑子里有个翻译官，把我想拿的分数先翻译成‘比对手多多少’，比完了再翻译回来。”&lt;/p>
&lt;p>“因为净胜分的视角，天然多了一层。你有没有想过——不记录净胜分，直接算当前牌面‘你能拿到的最大总分’？”&lt;/p>
&lt;p>阿星一愣，然后微微坐直了身体。&lt;/p>
&lt;p>“换个视角。”你抽出一张纸，写下新的定义，“定义 &lt;code>f[i]&lt;/code>：从第 &lt;code>i&lt;/code> 张牌开始先手的玩家，在剩余牌面中能拿到的&lt;strong>最大总分&lt;/strong>。注意——这次不是差值，就是你自己的总分。不关心对手拿多少，只关心你自己能装进口袋多少。”&lt;/p>
&lt;p>“从后往前看。只剩最后一张牌的时候，你拿走它，你的总分就是它本身。那如果从中间某张牌开始呢？每一轮你可以拿最前面的 1 张、2 张或者 3 张。不管你拿几张，从当前这张到末尾的总和——后缀和——是一个死的数。你拿走一部分，剩下的全归对手。对手在他拿到的那部分里，也会用最优策略拿走他的最大总分——那就是 &lt;code>f[i+j]&lt;/code>。”&lt;/p>
&lt;p>你停下来，让他消化这个概念。“所以每轮牌面，你能拿到的最大总分等于什么？后缀和 - 对手在剩下牌局里能拿到的最大总分。”&lt;/p>
&lt;blockquote>
&lt;p>s[i] = 从 i 到末尾的后缀和
f[i] = s[i] - （对手在剩余牌面里能拿到的最大总分）&lt;/p>&lt;/blockquote>
&lt;p>“重点来了——要最大化你能拿到的，就要最小化对手能拿的——对手在剩余牌面里能拿多少？取决于你。你拿 1 张，剩给他的是从 i+1 开始——他最多能拿 &lt;code>f[i+1]&lt;/code>。你拿 2 张，剩给他的是从 i+2 开始——他最多能拿 &lt;code>f[i+2]&lt;/code>。你拿 3 张，他最多能拿 &lt;code>f[i+3]&lt;/code>。最小化对手能拿的，也就是在这三个数中选最小的那个——&lt;strong>min&lt;/strong>。”&lt;/p>
&lt;blockquote>
&lt;p>f[i] = s[i] - min( f[i+1], f[i+2], f[i+3] )&lt;/p>&lt;/blockquote>
&lt;p>阿星盯着那个 &lt;code>min&lt;/code>，张了张嘴。这个反应和你预计的一模一样。&lt;/p>
&lt;p>“而且——后缀和只依赖当前位置，后面三个 &lt;code>f&lt;/code> 也只依赖紧挨着的三个状态。和刚才净胜分的滚动数组一模一样的依赖关系：&lt;code>f[i]&lt;/code> 只跟 &lt;code>f[i+1]&lt;/code>、&lt;code>f[i+2]&lt;/code>、&lt;code>f[i+3]&lt;/code> 打交道，再往后的值碰都不碰。同样不用数组，直接可以用三个变量滚完全局。”&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>n &lt;span style="color:#f92672">=&lt;/span> len(stoneValue)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>s &lt;span style="color:#f92672">=&lt;/span> f1 &lt;span style="color:#f92672">=&lt;/span> f2 &lt;span style="color:#f92672">=&lt;/span> f3 &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> s &lt;span style="color:#f92672">+=&lt;/span> stoneValue[i]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> best &lt;span style="color:#f92672">=&lt;/span> s &lt;span style="color:#f92672">-&lt;/span> min(f1, f2, f3)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f1, f2, f3 &lt;span style="color:#f92672">=&lt;/span> best, f1, f2
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>diff &lt;span style="color:#f92672">=&lt;/span> f1 &lt;span style="color:#f92672">-&lt;/span> (s &lt;span style="color:#f92672">-&lt;/span> f1)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Tie&amp;#34;&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> diff&lt;span style="color:#f92672">==&lt;/span>&lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Xin&amp;#34;&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> diff&lt;span style="color:#f92672">&amp;gt;&lt;/span>&lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Charles&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>“这就是思维翻转——视角一换，内层循环直接消失。”你从口袋里掏出一块新的巧克力，掰了一半递给阿星。&lt;/p>
&lt;p>“净胜分算的是两个人的博弈——你要在三个分支里分别算‘我比他多多少’，所以需要内层循环去模拟拿一张、两张、三张的分支。而总分算的是一个人的收益——你不需要关心对手具体拿多少，你只需要让他在剩给他的局面里，拿得越少越好。后缀和是死的，min 三个 f 锁定对手的下限，你拿的自然就最大。”&lt;/p>
&lt;p>阿星把那半块巧克力塞进嘴里，低头看了看自己两条小臂——左胳膊上还隐约能看见三个月前用圆珠笔写的“先手判生死”，已经褪得只剩浅浅一道印子；右胳膊干干净净，像是特意留到今天。&lt;/p>
&lt;p>“哥。左胳膊纹净胜分，右胳膊我想纹这个 min。”&lt;/p>
&lt;p>你把另外半块巧克力放进嘴中，起身拿起茶几上的纸鹤，放在他手心。&lt;/p>
&lt;p>“先回去把鼻血擦干净。纹身师不接流着血来的人。”&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode877 聚会杀手锏！石子游戏教你做一个没有感情的赢家</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-08-02/</link><pubDate>Sun, 02 Aug 2026 11:54:42 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-08-02/</guid><description>&lt;h2 id="聚会杀手锏石子游戏教你做一个没有感情的赢家">聚会杀手锏！石子游戏教你做一个没有感情的赢家
&lt;/h2>&lt;blockquote>
&lt;p>朋友，你是否厌倦了在聚会上当小透明？玩狼人杀首夜被刀，打UNO被+4连环暴击，搓麻将点炮点到灵魂出窍…… 别再用运气解释人生了，今天，我要向你郑重安利一款物理引擎驱动、&lt;strong>100%先手必胜&lt;/strong>的聚会神作——《石子游戏》（又名 LeetCode 877）。
不需要演技，不需要运气，只需要一点点小学数学，就能让你在朋友面前化身‘必胜客’。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h4 id="必胜操作手册">必胜操作手册：
&lt;/h4>&lt;p>游戏准备（请严格照做）&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>道具&lt;/strong>：一把石子、M&amp;amp;M豆、瓜子、硬币……能堆起来的东西都行。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>布阵&lt;/strong>：让你的朋友把这些道具分成 &lt;strong>偶数堆&lt;/strong>，在桌上一字排开。每堆数量任意，告诉你朋友，他可以随心所欲，尽情发挥。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>关键一步——奇偶调教&lt;/strong>：等朋友分完，你假装清点，悄悄算出总数。&lt;/p>
&lt;ul>
&lt;li>如果总数已经是 &lt;strong>奇数&lt;/strong>，完美。&lt;/li>
&lt;li>如果总数不小心是偶数，&lt;strong>立刻偷偷吃掉（或加上）一颗&lt;/strong>，同时面不改色地说：“这是启动游戏的仪式密钥。”&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>为什么必须是奇数？因为奇数保证没有平局，而且奠定了你必胜的数学地基。这一步，就是把胜利攥在自己手里。&lt;/p>&lt;/blockquote>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>选定身份&lt;/strong>：你当先手玩家，朋友当后手。然后，开始游戏。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h4 id="必胜秘诀">必胜秘诀：
&lt;/h4>&lt;p>这款游戏的底层逻辑，比泡面里没有真牛肉还要赤裸——&lt;strong>先手必胜&lt;/strong>。&lt;/p>
&lt;p>因为总堆数是偶数，所有石子天然被分成了两个阵营：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>奇数组&lt;/strong>：第1、3、5…堆（所有单数位置）&lt;/li>
&lt;li>&lt;strong>偶数组&lt;/strong>：第2、4、6…堆（所有双数位置）&lt;/li>
&lt;/ul>
&lt;p>而石子总数又是一个奇数，这就意味着——奇数组和偶数组的石子总数&lt;strong>绝对不相等&lt;/strong>，总有一组会多，一组会少。这不是运气，这是数学板上钉钉的事。&lt;/p>
&lt;p>身为先手玩家，你拥有一个离谱的特权：&lt;strong>你可以直接决定自己拿走哪一组，把另一组硬塞给朋友。&lt;/strong>&lt;/p>
&lt;p>怎么操作？趁你朋友还在精心管理表情的时候，快速心算一下两组的总数：&lt;/p>
&lt;ul>
&lt;li>如果 &lt;strong>奇数组更多&lt;/strong>，直接伸手拿走 &lt;strong>最左边&lt;/strong> 那一堆（它属于奇数组）&lt;/li>
&lt;li>如果 &lt;strong>偶数组更多&lt;/strong>，直接伸手拿走 &lt;strong>最右边&lt;/strong> 那一堆（它属于偶数组）&lt;/li>
&lt;/ul>
&lt;p>这一步就是&lt;strong>定边&lt;/strong>——出手的那一刻，胜负就已经分好了。拿走最左边，朋友就被锁死在偶数组里；拿走最右边，朋友就被锁死在奇数组里。从第二轮开始，桌上的局面会变成：露出的两端，全都是属于朋友那组的石子。他无论拿哪边，都在帮你打工——因为他一拿走，必然会把属于你那组的石子暴露出来。&lt;/p>
&lt;p>之后你的操作，简单到可以边嗑瓜子边完成：&lt;strong>他拿哪边，你就跟着拿哪边。&lt;/strong> 他左你左，他右你右，不用思考，不用纠结，全程当个无情的镜像复读机就行。一口一口把属于你的那组石子全部收入囊中，最后留给朋友的，只有他从一开始就注定要接盘的另一组。&lt;/p>
&lt;p>没错，就是这么简单粗暴。只要你是先手，&lt;strong>无论朋友怎么摆石子&lt;/strong>（偶数堆，奇数总数），你都 &lt;strong>100% 能赢&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h4 id="实战演示">实战演示：
&lt;/h4>&lt;p>假设朋友自信满满地摆出 4 堆 M&amp;amp;M 豆：&lt;code>[5, 3, 4, 5]&lt;/code>。&lt;/p>
&lt;p>你先别吭声，心里盘算一下：&lt;/p>
&lt;ul>
&lt;li>奇数组：5 + 4 = &lt;strong>9&lt;/strong>&lt;/li>
&lt;li>偶数组：3 + 5 = &lt;strong>8&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>奇数组更大，那就按口诀来——&lt;strong>第一步直接抢最左边的 5&lt;/strong>。
朋友还没反应过来，桌上就剩 &lt;code>[3, 4, 5]&lt;/code> 了。&lt;/p>
&lt;ol>
&lt;li>朋友行动（只能拿两端，都是偶数组的）：他若拿右边的 5。&lt;/li>
&lt;li>你镜像——也拿 右边，取走 4（它正是奇数组的）。&lt;/li>
&lt;li>朋友最后只能拿剩下的 3。&lt;/li>
&lt;/ol>
&lt;p>你合计：5 + 4 = 9，朋友合计：5 + 3 = 8。&lt;/p>
&lt;p>你赢。他输。他瞳孔地震。你抓一把豆子塞进嘴里，深藏功与名。&lt;/p>
&lt;hr>
&lt;h4 id="风险控制为友谊续一秒">风险控制（为友谊续一秒）
&lt;/h4>&lt;ul>
&lt;li>此游戏别名 &lt;strong>《友谊尽头》&lt;/strong>，连胜三局之后，朋友可能当场掏出手机搜索“石子游戏 必胜”，并在接下来的聚会里对你保持高度警惕。&lt;/li>
&lt;li>建议战术性放水一局，比如第二局故意拿小分组，然后挠着头说：“哎呀，这次运气不太行。” 用精湛的演技来维持塑料友谊的常青。&lt;/li>
&lt;li>若朋友强行要求换先手，请果断提议换游戏——比如 LeetCode 486，那里先手可不一定赢哦。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h4 id="写在最后附赠代码一行">写在最后（附赠代码一行）
&lt;/h4>&lt;p>下一次聚会，当别人还在掏手机查谁是卧底时，你已经优雅地掏出一把石子（或 M&amp;amp;M 豆）。&lt;/p>
&lt;p>&lt;strong>先手，微笑，必赢。&lt;/strong> 然后把赢来的零食全部吃掉，深藏功与名。&lt;/p>
&lt;p>如果有人较真到当场掏出一台电脑让你写代码，你只需要敲下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">stoneGame&lt;/span>(piles: List[int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> bool:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 先手必赢，聚会杀手，友谊终结者&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>——你看，石子游戏就是这么朴实无华：当数学站在你这边时，你的朋友就只能站在旁边看你吃豆子。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode486 净胜分心法：让你还没摸牌就知道会不会赢</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-08-01/</link><pubDate>Sat, 01 Aug 2026 16:41:42 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-08-01/</guid><description>&lt;h2 id="净胜分心法让你还没摸牌就知道会不会赢">净胜分心法：让你还没摸牌就知道会不会赢
&lt;/h2>&lt;hr>
&lt;h4 id="一牌桌之上零和博弈">一、牌桌之上，零和博弈
&lt;/h4>&lt;p>澳门，皇家赌场，半夜十一点。&lt;/p>
&lt;p>你坐在牌桌左边，手里转着一块还没拆锡纸的巧克力。&lt;/p>
&lt;p>对面是欧洲赌魔 查尔斯，金发梳得油光水滑，身后站着四个保镖，人手一本《博弈论》。&lt;/p>
&lt;p>“今晚我们不玩梭哈。” 查尔斯笑容倨傲。&lt;/p>
&lt;p>他打了个响指。荷官推出一个牌匣，里面躺着一排牌，牌面上不是花色，而是数字：&lt;/p>
&lt;pre tabindex="0">&lt;code>[1, 5, 233, 8, 2, 6, 4, 3]
&lt;/code>&lt;/pre>&lt;p>八张牌，张张点数透明。像一局明棋，沉默地躺在绿色绒布上。&lt;/p>
&lt;p>“规则很简单——我们轮流从这排牌的&lt;strong>最左端或最右端&lt;/strong>抽一张。抽到的数字直接加进总分。牌抽完，分高的人赢。如果平局——”他顿了一下，镜片反光一闪，“算你先手赢。”&lt;/p>
&lt;p>你慢慢拆开巧克力的锡纸。&lt;/p>
&lt;p>“我研究过你所有的录像。”查尔斯推了推眼镜，“你擅长在最后一刻翻盘，用一张小牌撬动整个局面。但这一次——每张牌的点数都直接公开，双方都绝对理性，追求自己的最大利益——你没有出千的空间。”&lt;/p>
&lt;p>你咬了一口巧克力：“你研究过我——那你知道为什么赌神从来不输吗？”&lt;/p>
&lt;p>查尔斯冷笑：“因为你出老千？”&lt;/p>
&lt;p>“不。”你把锡纸叠成一只纸鹤，放在牌匣旁边，“因为我会在游戏开始前，就把结局算死。”&lt;/p>
&lt;hr>
&lt;h4 id="二净胜分赌神只看一件事">二、净胜分——赌神只看一件事
&lt;/h4>&lt;p>阿星从旁边探出头，手里还举着半瓶汽水：“哥，要不要我用特异功能帮你搓一下牌？八张牌，我把那个 233 搓到最右边，你开场就给它抽走。”&lt;/p>
&lt;p>你按住他的手，“搓牌？搓完你晚上又流鼻血。今天教你一套正经功夫——递归动态规划无死角净胜分心法。”&lt;/p>
&lt;p>阿星一口汽水差点喷出来：“名字这么长？！能不能取个酷一点的简称？”&lt;/p>
&lt;p>“江湖上都叫它——&lt;strong>先手判生死&lt;/strong>。”&lt;/p>
&lt;p>你在桌布上用手指画了两个字母，&lt;code>diff(l, r)&lt;/code>，“阿星，两个高手博弈，你知道最难算的是什么？”&lt;/p>
&lt;p>“当然是算总分啊！你一张我一张，你永远猜不到对方下一步要拿哪张，算到最后脑子都炸了。”&lt;/p>
&lt;p>“错。总分是下等赌客算的。真正的高手，只算一个数——&lt;strong>我比你多赢多少&lt;/strong>。”&lt;/p>
&lt;p>阿星眨了眨眼：“多赢多少？”&lt;/p>
&lt;p>“净胜分。”&lt;/p>
&lt;blockquote>
&lt;p>设 &lt;code>diff(l, r)&lt;/code> 表示：
当桌上只剩下第 &lt;code>l&lt;/code> 张到第 &lt;code>r&lt;/code> 张牌，且现在轮到某人抽牌时，这个“当前抽牌人”最多能&lt;strong>净胜&lt;/strong>对手多少分。&lt;/p>
&lt;p>净胜分 = 我的总分 – 对手的总分。&lt;/p>&lt;/blockquote>
&lt;p>“查尔斯和我轮流抽，规则简单到三岁细路仔都识：&lt;/p>
&lt;ul>
&lt;li>
&lt;p>如果我从左端抽第 &lt;code>l&lt;/code> 张，我立刻到手 &lt;code>nums[l]&lt;/code> 分。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>抽完之后，桌上剩下 &lt;code>[l+1, r]&lt;/code>。轮到对手抽。注意——对手也是绝顶聪明的人渣，他也会最大化他自己的净胜分，也就是他能拿到 &lt;code>diff(l+1, r)&lt;/code> 的净胜分。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>那我的最终净胜分是多少？我手里的 &lt;code>nums[l]&lt;/code>，减去对手的净胜分。也就是：&lt;/p>
&lt;pre tabindex="0">&lt;code>左端净胜 = nums[l] - diff(l+1, r)
&lt;/code>&lt;/pre>&lt;/li>
&lt;li>
&lt;p>同样，如果从右端抽：&lt;/p>
&lt;pre tabindex="0">&lt;code>右端净胜 = nums[r] - diff(l, r-1)
&lt;/code>&lt;/pre>&lt;/li>
&lt;li>
&lt;p>两条路摆在面前，当然选净胜分更大的那条：&lt;/p>
&lt;pre tabindex="0">&lt;code>diff(l, r) = max( nums[l] - diff(l+1, r),
nums[r] - diff(l, r-1) )
&lt;/code>&lt;/pre>&lt;/li>
&lt;/ul>
&lt;p>阿星听到这里，抓了抓头发：“等一下哥。你左边写了个 &lt;code>diff(l, r)&lt;/code>，右边又蹦出来 &lt;code>diff(l+1, r)&lt;/code>——这公式自己调用自己，这不就是那个、那个——递归吗？我听说递归这东西，搞不好会 stack overflow 的！”&lt;/p>
&lt;p>“递归当然要有边界，不然就是死循环——你打麻将只摸牌不出牌，牌局能结束吗？” 你把纸鹤转了个方向，翅膀对准他，“只剩一张牌（&lt;code>l == r&lt;/code>）的时候，就是边界。只有一张牌还选什么？直接抽走。当前抽牌人的净胜分就是 &lt;code>nums[l]&lt;/code> 本身。”&lt;/p>
&lt;p>“所以，只需要算出 &lt;code>diff(0, n-1)&lt;/code>——也就是整副牌、我先手时的最大净胜分。如果这个数大于等于 0，我就至少不输。按照规则，平局也算我赢。这局，在抽第一张牌之前就已经结束了。”&lt;/p>
&lt;hr>
&lt;h4 id="三第一张牌">三、第一张牌
&lt;/h4>&lt;p>八张牌安静地躺在绿绒布上，最大的一张 233 缩在左手第三张，像一块被瓦片盖住的金砖。&lt;/p>
&lt;p>你把巧克力塞进嘴里，闭上眼睛。手指悬在桌面上方，像在敲击一张看不见的键盘——实际上你脑子里那张 DP 表正在从对角线往外一层层填满。八张牌，六十四格。净胜分在翻来覆去地拉扯，最后定格在一个数字上。&lt;/p>
&lt;p>你睁开眼睛，“阿星，我的全局净胜分大于 0。这一局，我已经赢了。”&lt;/p>
&lt;p>阿星瞪大眼睛：“哥，你连牌都没摸呢，这么肯定？！”&lt;/p>
&lt;p>你没搭话，伸出手，在所有人的注视下，抽走了最左边那张牌——&lt;strong>1。&lt;/strong>&lt;/p>
&lt;p>查尔斯愣了一秒，然后大笑，“这就是赌神？闭眼算了半天，第一步居然拿了个最小的 1？”&lt;/p>
&lt;p>桌上剩下七张：&lt;code>[5, 233, 8, 2, 6, 4, 3]&lt;/code>。轮到查尔斯。&lt;/p>
&lt;p>他活动了一下手指，伸向最左的 5，停住了。又伸向 3，又停住了。&lt;/p>
&lt;p>他的脸色骤变，冷汗从他太阳穴滑下来。&lt;/p>
&lt;p>“如果我拿 5——剩下 &lt;code>[233, 8, 2, 6, 4, 3]&lt;/code> 给你。你第一张就会直接叼走 233，然后桌上变成 &lt;code>[8, 2, 6, 4, 3]&lt;/code> 轮到我。不管我接下来怎么选，只能捡些8、4、6 这种零钱，根本追不上！”&lt;/p>
&lt;p>“如果我拿 3，剩下 &lt;code>[5, 233, 8, 2, 6, 4]&lt;/code> 给你——你肯定不会拿 5，因为拿 5 等于把 233 留给我。你一定会拿 4！然后剩下 &lt;code>[5, 233, 8, 2, 6]&lt;/code> 轮到我。我被迫拿 6，然后你拿 2，我拿 8，233还是你的！我怎么选都死？！”&lt;/p>
&lt;p>查尔斯的金丝眼镜裂开一条缝。他终于明白了——那张 1 是一把钥匙。拿走了 1，你变成了游戏规则的掌控者。他不管从左走还是从右走，都会亲手把通往 233 的路给你让出来。&lt;/p>
&lt;p>你咬了一口巧克力：“这就叫，用对手的手，帮自己拿牌。”&lt;/p>
&lt;hr>
&lt;h4 id="尾声">尾声：
&lt;/h4>&lt;p>查尔斯瘫在椅子上，四个保镖手忙脚乱地翻《博弈论》索引，嘴里念念有词：“Minimax……Nash equilibrium……不对不对老板，这章我们还没读到——”&lt;/p>
&lt;p>“读个屁。”查尔斯有气无力地挥了挥手，“明天给我换一本《算法导论》。”&lt;/p>
&lt;p>你起身整理西装，把那只纸鹤留在牌匣旁边，翅膀正对着查尔斯。&lt;/p>
&lt;p>“用算法的话说，这叫——最优子结构。把大问题拆成小问题，每一步都在替对手关上他最想走的那扇门。”你转身走向门口，头也不回，“先手判生死——承让。”&lt;/p>
&lt;p>阿星看着你背影大喊：“大哥！这招式能不能写短一点？我要纹在手臂上！”&lt;/p>
&lt;p>你头也不回，弹出一张扑克牌，上面用缩进刻着：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">predictTheWinner&lt;/span>(nums):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">@cache&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">dfs&lt;/span>(start: int, end: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> start &lt;span style="color:#f92672">==&lt;/span> end:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> nums[start]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> max(nums[start] &lt;span style="color:#f92672">-&lt;/span> dfs(start &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, end), nums[end] &lt;span style="color:#f92672">-&lt;/span> dfs(start, end &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> dfs(&lt;span style="color:#ae81ff">0&lt;/span>, len(nums) &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#f92672">&amp;gt;=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3016 续 · 键盘上的权重战争</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-31/</link><pubDate>Fri, 31 Jul 2026 11:25:17 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-31/</guid><description>&lt;h2 id="续--键盘上的权重战争">续 · 键盘上的权重战争
&lt;/h2>&lt;p>窗外的雪还没停，赫尔辛基的冬天长得像没优化的冒泡排序。&lt;/p>
&lt;p>昨天你用一套“贪心平摊”解决了 &lt;code>nokiabest&lt;/code> 的极限按键次数，这会儿正摸着 Nokia 3310 玩贪吃蛇——用 2、4、6、8 控制方向的经典游戏。&lt;/p>
&lt;p>正吃到第七个豆子，CEO 推门进来了。他举着一台原型机，屏幕幽幽地亮着。&lt;/p>
&lt;p>&amp;ldquo;昨天你跟我说，&amp;ldquo;CEO 清了清嗓子，&amp;quot;&lt;code>nokiabest&lt;/code> 那种每个字母只出现一次的情况，随便塞就能最优。但你后面提到字母会重复——确实，真实世界里，用户打的字母不是均匀分布的，&lt;code>e&lt;/code> 的出现频率是 &lt;code>z&lt;/code> 的三十倍——如果考虑字母频率，重新映射键位，最少要按多少次键？&amp;rdquo;&lt;/p>
&lt;p>他把手机往你桌上一搁，屏幕上是一条正在编辑的短信草稿，收件人写着 &lt;code>nokia&lt;/code>，内容只有一行字：&lt;code>connectingpeople&lt;/code>&lt;/p>
&lt;p>“老板，”你抬头看他，“拿自家口号当测试用例，是不是有点自恋？”&lt;/p>
&lt;p>CEO 的表情纹丝不动：“ 这是市场部给的 KPI——他们明年要把这句印在每台手机上。如果连自己的 slogan 都打不利索，发布会上怎么跟黑莓对线？”&lt;/p>
&lt;p>他把原型机往你面前推了半寸：“这才是真实世界的需求。”&lt;/p>
&lt;hr>
&lt;h4 id="第一幕频率加权下的贪心">第一幕：频率加权下的贪心
&lt;/h4>&lt;p>昨天的 &lt;code>nokiabest&lt;/code>，所有字母只出现一次，权重相同，排列顺序无所谓，只需要贪心地一层一层塞满即可。但今天的 &lt;code>connectingpeople&lt;/code>，字母出场次数不同。&lt;code>e&lt;/code> 出了 3 次，&lt;code>l&lt;/code> 只出 1 次。如果傻乎乎地把 &lt;code>e&lt;/code> 塞到某个键的第三层——用户每打一个 &lt;code>e&lt;/code> 就要按 3 次键，手机迟早会被扔进芬兰湾。&lt;/p>
&lt;p>你在白板上画了八个方框，代表 2 到 9 号键。&lt;/p>
&lt;p>“每个键可以挂一串字母。排第一的按 1 次，排第二的按 2 次，排第三的按 3 次——按键代价按层数递增。那么，最优映射本质上就跟快捷键一个道理：最常用的操作一定绑在最顺手的位置，没人会把‘保存’藏在三级菜单里。&lt;/p>
&lt;p>CEO 点头：“这个不用解释，肌肉记忆就是成本。”&lt;/p>
&lt;p>“这就是今天这道题的灵魂。”我敲了敲白板，“&lt;strong>频率越高的字母，要放到越便宜的位置。&lt;/strong>”&lt;/p>
&lt;p>“在频率加权的情况下，最优填充就是按频率降序，依次往 8 个键的每一层&amp;rsquo;铺货&amp;rsquo;。第一层 8 个坑位给频率最高的 8 个字母，第二层给接下来的 8 个，依此类推。”&lt;/p>
&lt;blockquote>
&lt;ol>
&lt;li>统计词频，谁出场次数多谁就是 VIP。&lt;/li>
&lt;li>把频率从高到低排序。&lt;/li>
&lt;li>按顺序填满 8 个键的第一层（每个代价 1），再填第二层（每个代价 2），第三层……&lt;/li>
&lt;li>每个字母的总代价 = 它所在层数 × 它的频率。总按键次数 = 所有字母代价之和。&lt;/li>
&lt;/ol>&lt;/blockquote>
&lt;p>CEO 若有所思地点点头：“那如果我给你一个更复杂的词呢？比如……芬兰语里那个‘飞机喷气发动机助理机械师学员’——&lt;code>lentokonesuihkuturbiinimoottoriapumekaanikkoaliupseerioppilas&lt;/code>？”&lt;/p>
&lt;p>“一样的，算法内核一句话——&lt;strong>按频率降序排列，第 i 个字母（从 0 开始）会被放在第 &lt;code>i // 8&lt;/code> 层（层数从 0 起），按键代价为 &lt;code>(i // 8) + 1&lt;/code>，总次数累加 &lt;code>频率 × 代价&lt;/code>。&lt;/strong>”&lt;/p>
&lt;hr>
&lt;h4 id="第二幕--代码就是武器">第二幕 · 代码就是武器
&lt;/h4>&lt;p>你拉过键盘，打开 Notepad。2003 年的诺基亚办公室里，这台电脑跑的还是 Windows 2000，编程环境朴素得像白水煮土豆。但这不重要——算法在手，什么编辑器都是利器。&lt;/p>
&lt;p>这个年代，Python 2.2 刚刚发布，写起来颇有复古手感。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">minimumPushes&lt;/span>(word: str) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 1. 统计频率，别问我为什么不用Counter——Python 2.2里还没这玩意儿&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> freq &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">26&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> ch &lt;span style="color:#f92672">in&lt;/span> word:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> freq[ord(ch) &lt;span style="color:#f92672">-&lt;/span> ord(&lt;span style="color:#e6db74">&amp;#39;a&amp;#39;&lt;/span>)] &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 2. 降序排序——高频在前，低频在后&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> freq&lt;span style="color:#f92672">.&lt;/span>sort(reverse&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 3. 贪心分配：第 i 个字母落在第 i//8 层，代价 = 层数 + 1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> total_pushes &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i, f &lt;span style="color:#f92672">in&lt;/span> enumerate(freq):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> f &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">break&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> layer &lt;span style="color:#f92672">=&lt;/span> i &lt;span style="color:#f92672">//&lt;/span> &lt;span style="color:#ae81ff">8&lt;/span> &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 第几层，代价即层号&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> total_pushes &lt;span style="color:#f92672">+=&lt;/span> f &lt;span style="color:#f92672">*&lt;/span> layer
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> total_pushes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>时间复杂度 O(n + 26 log 26)，实际上就是 O(n)。空间复杂度 O(1)。还是那股诺基亚味儿——低功耗，高可靠。&lt;/p>
&lt;p>你转头看向 CEO：“假设英语里 26 个字母全部出现，频率排名前 8 的占第一层，按 1 次；接下来 8 个占第二层，按 2 次；最后 10 个占第三层，按 3 次。没有谁可以逃脱这个代价层级——除非你给 26 个字母每人一个独立按键。”&lt;/p>
&lt;p>说到这儿，你顿了下，望向桌上那台 BlackBerry 8700——昨天黑莓经理非要留在这儿的，说是“让诺基亚的工程师开开眼，看看什么叫真正的打字效率”。&lt;/p>
&lt;hr>
&lt;h4 id="第三幕--物理键的诅咒">第三幕 · 物理键的诅咒
&lt;/h4>&lt;p>“黑莓的全键盘，在这种频率加权测试下，表现如何？”CEO 冷不丁问了一句。&lt;/p>
&lt;p>你拿起那台黑莓，用手指划过那密密麻麻的 26 个按键。“他们的方案是：26 个字母，一人一键。也就是说，不管 &lt;code>e&lt;/code> 出现了多少次，也不管 &lt;code>z&lt;/code> 多稀有，成本全是 1。”&lt;/p>
&lt;p>“听起来&amp;hellip;&amp;hellip;很好啊？”CEO 的语气有点不确定。&lt;/p>
&lt;p>“单论按键次数，确实好。代价嘛——&lt;strong>硬件复杂度爆炸，单手操作变灾难，拇指移动距离赛过马拉松。&lt;/strong> ”&lt;/p>
&lt;p>你拿起诺基亚原型机，八个键，简洁得像一把瑞士军刀。“物理世界的残酷就在这儿——你用多少空间换多少效率。假设英语词汇频率分布是固定的，我们这套 8 键多层方案的总按键次数，只比全键盘多了一个‘层数代价’。但物理成本呢？我们的键盘只占三分之一的面板，省下来的空间可以放更大的屏幕，或者更大的电池。”&lt;/p>
&lt;p>“更重要的是……”你压低了声音，“黑莓的 QWERTY 固定了每个字母的位置，它没办法根据输入习惯动态调整。而我们的映射方案是可重新编程的——理论上，我们可以在手机里内置一个轻量级频率统计引擎，静默学习用户的打字习惯，&lt;strong>动态调整映射&lt;/strong>。高频字母往第一层塞，低频往后面扔。这种个性化优化，黑莓的物理键盘永远做不到。”&lt;/p>
&lt;p>CEO 的眼镜片上反过一道白光。“你是说……自适应键盘？”&lt;/p>
&lt;p>“你也可以说是‘输入法’。”你笑了笑，“不过现在说这个还早。先把今天的问题搞定吧。”&lt;/p>
&lt;hr>
&lt;h4 id="尾声">尾声
&lt;/h4>&lt;p>那天下午，你把完整的算法文档交给了 CEO。标题写的是：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>《按键映射优化方案 2.0：引入字母频率权重的贪心分配》&lt;/strong>&lt;/p>&lt;/blockquote>
&lt;p>交完文档，你走出大楼。雪停了，天空露出一小片北欧特有的低饱和度蓝。&lt;/p>
&lt;p>你想起昨天咬的那口苹果，还剩半个在口袋里。&lt;/p>
&lt;p>黑莓的产品经理后来再也没来找过你。据说他回到加拿大后，在实验室里对着 26 键全键盘吼了一句：“你永远没法用物理按键解决频率权重问题！” 然后被保安请了出去。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3014 穿越者的键盘革命：数学赢了，产品输了，历史笑了</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-30/</link><pubDate>Thu, 30 Jul 2026 14:53:43 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-30/</guid><description>&lt;h2 id="穿越者的键盘革命数学赢了产品输了历史笑了">穿越者的键盘革命：数学赢了，产品输了，历史笑了
&lt;/h2>&lt;h4 id="序章--你醒在2003年的芬兰">序章 · 你醒在2003年的芬兰
&lt;/h4>&lt;p>眼前是灰白色的天花板，鼻腔里灌满了咖啡和焊锡的味道。一张戴着黑框眼镜的大脸突然怼到你面前，胸前工牌上写着 &lt;strong>Nokia HQ — CEO&lt;/strong>。&lt;/p>
&lt;p>“醒了？”他劈头盖脸甩过来一台原型机，又拍了张传真纸在旁边，“下一代手机键盘的映射方案，交给你了。”&lt;/p>
&lt;p>需求看上去很简单：&lt;/p>
&lt;blockquote>
&lt;p>设计目标：新一代手机要支持短信输入，用户需要能快速打出英文单词
硬件限制：只有数字键2-9可用于字母映射（共8个键），1和0保留给拨号和系统
交互方式：每键可绑多个字母，按一次出第一个，按两次出第二个，依此类推。
优化目标：让用户输入常用单词时的按键次数最少&lt;/p>&lt;/blockquote>
&lt;p>“先用这个测。”CEO 用食指敲了敲传真纸，上面只写了一个词：&lt;code>&amp;quot;nokiabest&amp;quot;&lt;/code>。九个字母，全都不重复。&lt;/p>
&lt;p>你忍不住发问：“老板，测试单词一般不应该是 hello 之类的吗？”&lt;/p>
&lt;p>CEO 一摆手：“hello 有重复字母，那是带频率权重的进阶任务。今天先搞定最纯粹的情况——每个字母只出现一次。地基不牢，怎么盖楼？先拿 &lt;code>&amp;quot;nokiabest&amp;quot;&lt;/code> 给我算个极限值出来！”&lt;/p>
&lt;p>你刚要开口，余光扫到了日历——2003 年，距离 iPhone 改变一切还有四年。此刻没有触屏，没有虚拟键盘，你只能用 8 个物理按键的规则，去逼近物理按键的极限。&lt;/p>
&lt;hr>
&lt;h4 id="第一幕为什么没有-1-和-0">第一幕：为什么没有 1 和 0？
&lt;/h4>&lt;p>你把原型机翻过来。&lt;strong>1&lt;/strong> 键旁边印着“拨号”，&lt;strong>0&lt;/strong> 键印着“空格/开关”。2003 年，这两个键像神庙石柱一样神圣不可侵犯——只服务于通话和系统。&lt;/p>
&lt;p>你忍不住乐了：“好家伙，跟中断向量表似的——低编号归系统，用户从高编号开始折腾——优秀的设计果然会独立重复出现。”&lt;/p>
&lt;p>旁边一个老工程师从示波器后面探出头：“废话，你把挂电话和打字绑同一个键试试？老板按三下‘a’结果把客户电话挂了，第二天整个键盘部门都得去西伯利亚报到。”&lt;/p>
&lt;p>你没接茬，盯着剩下的八个键出神。&lt;/p>
&lt;p>八个键。二十六个字母。每个键上可以塞一串字母——排第一的按1次，排第二的按2次，排第三的按3次，以此类推，目标要把总按键次数压到最低。&lt;/p>
&lt;p>这问题的结构你太熟悉了：&lt;strong>当资源有限（只有 8 个槽位）、每个任务权重相等（字母各不相同）、每个槽位递增成本（第二次按多费一下）时，怎样分配才最省？&lt;/strong> 答案永远是贪心：&lt;strong>先把最便宜的槽位塞满，再往下走。&lt;/strong>&lt;/p>
&lt;p>就像在《魔兽世界》里，你一定把最常用的技能放在 1~8 键；在 2003 年的诺基亚上，你也一定会把最想打出的字母留在每个键的第一层。&lt;/p>
&lt;hr>
&lt;h4 id="第二幕---贪心平摊">第二幕 · 贪心平摊
&lt;/h4>&lt;p>“映射自由度很大，但算法内核就一句：&lt;strong>当槽位固定、代价递增、任务均权时，最省策略就是贪心平摊。&lt;/strong>”&lt;/p>
&lt;p>你在白板上画了个 8 列的表格：“具体做法——第一轮，给 8 个键各塞一个字母，全放第一层，每个成本 1。第一轮塞满了还有剩？第二轮，各塞第二个，全放第二层，每个成本 2。以此类推。同一层内部的字母顺序无所谓——&lt;strong>真正决定成本的，是它在第几层。&lt;/strong>”&lt;/p>
&lt;p>“在这个填充规则下，最少按键次数直接算。” 你拿起马克笔，笔尖敲得白板砰砰响：“设单词长度为 n，8 个键。”&lt;/p>
&lt;pre tabindex="0">&lt;code>完整层数 k = n // 8 （有几层是满的）
剩余字母 rem = n % 8 （最高层散装字母，1~7个）
总按键次数 = 8×(1 + 2 + ... + k) + rem×(k+1)
&lt;/code>&lt;/pre>&lt;p>“等差数列求和，初中学的——”你把公式往下一推：&lt;/p>
&lt;pre tabindex="0">&lt;code>总按键次数 = 8*(1+2+...+k) + rem*(k+1)
= 4*k*(k+1) + rem*(k+1)
= (4*k + rem) * (k+1)
&lt;/code>&lt;/pre>&lt;p>对于测试单词&lt;code>&amp;quot;nokiabest&amp;quot;&lt;/code>，n=9，k=1，rem=1。代入——(4×1+1)×(1+1) = &lt;strong>10 次&lt;/strong>。这就是它的极限值。&lt;/p>
&lt;p>你甚至还码瘾大发，抄起旁边的 Notepad 写下一段 Python：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">minimumPushes&lt;/span>(word: str) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> k, rem &lt;span style="color:#f92672">=&lt;/span> divmod(len(word), &lt;span style="color:#ae81ff">8&lt;/span>) &lt;span style="color:#75715e"># 完整层数 和 最高层散装字母&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> (&lt;span style="color:#ae81ff">4&lt;/span>&lt;span style="color:#f92672">*&lt;/span>k &lt;span style="color:#f92672">+&lt;/span> rem) &lt;span style="color:#f92672">*&lt;/span> (k &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>时间复杂度 O(1)，空间复杂度 O(1)。低功耗、高效率，非常诺基亚——就像 3310 的省电神话。&lt;/p>
&lt;p>CEO 盯着公式沉默了片刻：“那具体方案呢？”&lt;/p>
&lt;p>你把马克笔往桌上一搁：“老板，这才是我要说的重点——&lt;strong>只要按层填充，具体哪个字母进哪个键，最少次数完全一样。&lt;/strong> 因为所有字母权重相同，谁也不比谁更金贵。”&lt;/p>
&lt;p>你顿了顿，补了一句：“当然，这是字母全都不重复的特权。一旦单词里字母能重复，权重就不均等了，高频字母得往前排——那是另一个故事。”&lt;/p>
&lt;p>“但今天——” 你拿起原型机：“具体方案就是维持现状。按字母表顺序，2 到 9 各分三个，7 和 9 多扛一个。这套映射从 1919 年贝尔实验室定下来就没变过，用户肌肉记忆已经焊死了。没必要改，也改不出更好的，数学上已经最优，历史又刚好站在我们这边。”&lt;/p>
&lt;p>CEO 拿起原型机端详了半天，缓缓点头：“定了。”&lt;/p>
&lt;p>你没接话。这套方案还能撑三年多——2007 年，乔布斯会掏出一块没有键盘的玻璃板，把物理按键送进博物馆。&lt;/p>
&lt;p>“那如果考虑字母频率呢？”CEO 冷不丁又问了一句。&lt;/p>
&lt;p>你正要开口说“拿哈希表做频率统计，高频往第一层塞”——话还没出口，门口一个西装革履的身影大步走了进来。&lt;/p>
&lt;hr>
&lt;h4 id="第三幕--黑莓的全键盘">第三幕 · 黑莓的全键盘
&lt;/h4>&lt;p>来的是隔壁黑莓公司的产品经理，他手里举着一台BlackBerry 8700，键盘密密麻麻排了26个英文字母——&lt;strong>全键盘！QWERTY！&lt;/strong>&lt;/p>
&lt;p>“你们这些九宫格异端！”他把黑莓拍在桌上，“看清楚——一个字母一个键，永远按一次。没有第二层，没有第三层。你们的方案让人按两次三次，这叫用户体验？”&lt;/p>
&lt;p>你瞟了一眼那台黑莓。确实，每个字母只按一次，干净利落。但代价也摆在明面上——26 个键占了半个机身。&lt;/p>
&lt;p>你站起来，拿起诺基亚原型机：“你的方案，是用物理空间的铺张浪费，换按键次数的边际优化。”&lt;/p>
&lt;p>黑莓经理冷笑：“边际优化？你们的用户为了打个 ‘z’ ，要连按四次！”&lt;/p>
&lt;p>“但你知不知道，”你指着黑莓那块屏幕，“你们的用户为了看清邮件标题，得把眼睛贴到屏幕上？”&lt;/p>
&lt;p>会议室安静了三秒。&lt;/p>
&lt;p>你继续补刀：&amp;ldquo;而且——当屏幕足够大、触控足够精准的那一天，所有物理按键都会消失。 虚拟键盘会把8键的多层结构，重新展开为26键的全尺寸布局。用户可以直接戳屏幕上的&amp;rsquo;z&amp;rsquo;。你黑莓的QWERTY会变成软件里的像素，而诺基亚的物理键盘——会进博物馆。&amp;rdquo;&lt;/p>
&lt;p>黑莓经理脸色铁青：&amp;ldquo;胡说八道，手指戳屏幕？那手感跟戳土豆有什么区别？&amp;rdquo;&lt;/p>
&lt;p>你没理他，而是拿起桌上的苹果咬了一口：&amp;ldquo;还有四年。&amp;rdquo;&lt;/p>
&lt;p>窗外芬兰的雪静静落着。&lt;/p>
&lt;hr>
&lt;h4 id="尾声">尾声
&lt;/h4>&lt;p>走出诺基亚大楼，赫尔辛基的风灌进领口。你摩挲着口袋里那台 3310，它硬得像块巧克力砖。&lt;/p>
&lt;p>新世界的大门，从来不是靠优化旧逻辑打开的。&lt;/p>
&lt;p>2007 年 1 月 9 日，iPhone 发布。全球的诺基亚和黑莓工程师将会在直播里看到那块巨大的触摸屏——没有键盘，没有轨迹球，只有一整块玻璃。&lt;/p>
&lt;p>8 键多层的物理妥协，被电容屏一瞬间抹平了。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3518 强迫症整理圣手的字典序试炼（二）：第k小之章</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-29/</link><pubDate>Wed, 29 Jul 2026 14:08:19 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-29/</guid><description>&lt;h2 id="强迫症整理圣手的字典序试炼二第k小之章">强迫症整理圣手的字典序试炼（二）：第k小之章
&lt;/h2>&lt;p>我的室友阿整——江湖人称“整齐哥”——上一回用他那套“镜像分离·点名归位·定海神针”大法，轻松碾碎了&lt;strong>第1小&lt;/strong>回文排列的试炼。可就在他准备给信纸裱框时，背面浮现的太极图和那句“&lt;strong>找第k小&lt;/strong>”直接把他定在了原地，像一台突然蓝屏的精密仪器。&lt;/p>
&lt;p>现在，他盯着那张挑不出半点排版毛病的信纸，瞳孔像失去焦距的镜头。空气凝固了整整十秒——对阿整来说，十秒够他把书架按索书号重排三遍。终于，他喉咙里挤出了一声近乎系统报错的叹息：&lt;/p>
&lt;p>“他们……居然连落款字体大小都是正文的0.618倍，完美的黄金分割！”&lt;/p>
&lt;p>我差点把薯条呛进鼻子里。“喂，关注点不应该是这任务的难度吗？人家要你找第k小啊！”&lt;/p>
&lt;p>阿整缓缓转过头，眼神里混合着敬畏与恐惧：“你不懂。一个连字号比例都算到黄金分割的组织——难度是附赠的，秩序才是本体。”&lt;/p>
&lt;hr>
&lt;h4 id="一冷静以及分治">一、冷静，以及分治
&lt;/h4>&lt;p>阿整拾起地上的马克笔，用酒精棉片擦了擦笔身，以笔帽的logo朝向正北的方式插回笔筒。他闭上眼，再睁开时，眼里的混乱已经被一种冰冷的秩序取代。&lt;/p>
&lt;p>“&lt;strong>第k小&lt;/strong>。”他合上牛皮笔记本“整齐宪法”，“不过是&lt;strong>字典序树上的精准定位&lt;/strong>，本质上和整理书架没有区别。”&lt;/p>
&lt;p>他转身在白板上画了起来——原字符串s，长度n，左半边长度m=n//2：“回文排列的结构是不变的。左半任意排列，中间可能夹一个奇点，右半是左半的镜像。和上次一样，右半边是赠品，不参与决策。”&lt;/p>
&lt;p>他用笔帽敲了敲白板：“所以问题降维——要找到第k个回文排列，只需求出&lt;strong>左半部分字符的第k个字典序排列&lt;/strong>。”&lt;/p>
&lt;p>先统计左半部分每个字符的出现次数：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>n &lt;span style="color:#f92672">=&lt;/span> len(s)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>half_len &lt;span style="color:#f92672">=&lt;/span> n &lt;span style="color:#f92672">//&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 用数组统计左半部分字符频率（索引 0-25 对应 a-z）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cnt &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">26&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> ch &lt;span style="color:#f92672">in&lt;/span> s[:half_len]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt[ord(ch) &lt;span style="color:#f92672">-&lt;/span> ord(&lt;span style="color:#e6db74">&amp;#39;a&amp;#39;&lt;/span>)] &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>“字符可能重复，排列不可区分——这是一个多重集排列问题，不是简单的全排列。同样的字母，谁先谁后都长得一样，不能重复计数。” 阿整看了看我，又补了句：“说人话就是：&amp;lsquo;aab&amp;rsquo;和&amp;rsquo;aab&amp;rsquo;哪怕交换两个a的位置，也只算同一种排列，不能重复数。”&lt;/p>
&lt;p>接着，他在白板上写下：&lt;/p>
&lt;blockquote>
&lt;p>给定字符多重集，求：该集合按字典序构成的第k个排列是什么？&lt;/p>&lt;/blockquote>
&lt;p>然后重重画了个圈。&lt;/p>
&lt;p>“真正的难点不在于算总数，而在于——” 他转过身，十指交叉，指节发出整齐的咔嗒声，“——怎么从零开始，一位一位地把第k小拼出来。”&lt;/p>
&lt;p>“想象你面前是一棵巨大的字典树，每一条从根到叶子的路径就是一个排列，按字母顺序排好。你要找第k条路径。你不可能把整棵树画出来——排列数量随长度爆炸式增长，画完树宇宙都热寂了。”&lt;/p>
&lt;p>他在白板上画了一个节点，分出几个分支。&lt;/p>
&lt;p>“你要做的是：站在根节点，算一算第一条分支下面藏了多少条路径。如果k比这个数小，就钻进这个分支；如果k比这个数大，就减掉这个分支的数量，跳过去看下一条分支。一层一层往下走，每一步都知道该选哪个字母——走到叶子，答案自然就拼出来了。”&lt;/p>
&lt;p>他放下笔，双手抱胸，左右手又各敲了四下。&lt;/p>
&lt;p>“所以问题就拆成两步：一，怎么快速算出一个分支下面藏了多少条路径——也就是给定字符集还剩多少种排列；二，怎么沿着这棵树逐层往下钻——也就是每一步根据k决定选哪个字母、要不要跳到下一个分支。”&lt;/p>
&lt;p>“第二步是通用框架，第一步——算排列数——有两种打法” 他在白板上画了一条分叉线，左边写&amp;quot;比例递推&amp;quot;，右边写&amp;quot;组合截断&amp;quot;。&lt;/p>
&lt;hr>
&lt;h4 id="二比例递推按比例切蛋糕">二、比例递推——按比例切蛋糕
&lt;/h4>&lt;p>阿整盯着白板上的字典树，树枝画得整整齐齐，分叉角度完全一致。“&lt;strong>第一种打法：比例递推&lt;/strong>。”他在白板左半边写下标题，“核心就一句话——你已经知道整块蛋糕有多大，就不用每次都重新称重，按比例切就行。”&lt;/p>
&lt;p>写完之后他又退后两步，歪头端详了三秒，上前把“推”字的提手旁和右边的“隹”间距调大了0.5毫米。&lt;/p>
&lt;p>“第一步，工欲善其事，必先利其器。” 排列数公式的核心是阶乘——先做好阶乘预计算，用空间换时间：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 预计算阶乘表：fact[i] = i!，避免重复计算大数阶乘&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>fact &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (half_len &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, half_len &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fact[i] &lt;span style="color:#f92672">=&lt;/span> fact[i &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> i
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>“阶乘是排列数的基本粒子。提前算好，随用随取，避免重复劳动——重复计算是对CPU的侮辱，CPU也有尊严。”&lt;/p>
&lt;p>接着是排列数公式，他写下一个内部函数&lt;code>calc_perms&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 多重集排列数 = length! / (cnt₁! × cnt₂! × ...)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">calc_perms&lt;/span>(length: int, counter: list) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res &lt;span style="color:#f92672">=&lt;/span> fact[length]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> v &lt;span style="color:#f92672">in&lt;/span> counter:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res &lt;span style="color:#f92672">//=&lt;/span> fact[v]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> res
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>“分子把所有人的可能性全部摊开，分母把同种字符内部的重复排列干掉。整除一定成立——任何余数都是对秩序的亵渎。”&lt;/p>
&lt;p>“如果总排列数都不到k，那就直接返回空字符串——就像发现书架上缺了一本绝版书，用镜子补上虚像。” 阿整在这里轻轻点了点头，仿佛在向自己致敬。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 计算左半部分的所有可能排列数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>total_perms &lt;span style="color:#f92672">=&lt;/span> calc_perms(half_len, cnt)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 如果总排列数都不够 k，直接返回空字符串（无解）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> k &lt;span style="color:#f92672">&amp;gt;&lt;/span> total_perms:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>接着，他在白板上画了一个巨大的圆，又用红笔从圆心画出几条辐射线，把圆切成几个扇形。&lt;/p>
&lt;p>“高潮来了。”他压低声音，“把&lt;code>total_perms&lt;/code> 个排列看作一整块大饼。现在要按第一个位置字母把它切块——a开头的、b开头的、c开头的……每一块的大小，就是‘以这个字母开头的排列数量’。”&lt;/p>
&lt;p>“怎么切？不用重新拿公式算。”他用笔帽戳了戳饼图，“多重集排列有一个天然对称性：在所有排列里，每个位置上的字符出现机会是均等的。换句话说，第一个位置是c的概率，就是c的个数除以总长度。”&lt;/p>
&lt;p>他在饼图旁边奋笔疾书：&lt;/p>
&lt;blockquote>
&lt;pre tabindex="0">&lt;code>c块的大小 = 总饼 × (cnt[c] / remaining_length)
&lt;/code>&lt;/pre>&lt;/blockquote>
&lt;p>“比例递推的灵魂。”他转过身，眼神里燃起一丝布道者的狂热，“不需要每次重新跑一遍 &lt;code>calc_perms&lt;/code>。你已经知道总饼有多大，只要乘上这个字符的占比，就直接得到了这个分支的精确大小。而且这个除法——”&lt;/p>
&lt;p>他深吸一口气，“——永远整除。你看，对称性的终极体现就是，连余数都被消灭了，干干净净。”&lt;/p>
&lt;p>他舔了舔嘴唇，写下循环框架：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>half &lt;span style="color:#f92672">=&lt;/span> [] &lt;span style="color:#75715e"># 存放最终拼出来的左半部分&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>remaining_length &lt;span style="color:#f92672">=&lt;/span> half_len &lt;span style="color:#75715e"># 还剩多少个位置要填&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>current_perms &lt;span style="color:#f92672">=&lt;/span> total_perms &lt;span style="color:#75715e"># 当前剩余的总排列数，初始为整块饼&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(half_len):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">26&lt;/span>): &lt;span style="color:#75715e"># 按字典序，从a到z试探&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> cnt[j] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 分支大小 = 当前总饼 × 字符占比&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> new_perms &lt;span style="color:#f92672">=&lt;/span> current_perms &lt;span style="color:#f92672">*&lt;/span> cnt[j] &lt;span style="color:#f92672">//&lt;/span> remaining_length
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> k &lt;span style="color:#f92672">&amp;lt;=&lt;/span> new_perms:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 第k个就在这个分支里！选定字母j&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> half&lt;span style="color:#f92672">.&lt;/span>append(ascii_lowercase[j])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt[j] &lt;span style="color:#f92672">-=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> remaining_length &lt;span style="color:#f92672">-=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> current_perms &lt;span style="color:#f92672">=&lt;/span> new_perms &lt;span style="color:#75715e"># 钻进分支&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">break&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 不在这个分支，跳过整块饼&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> k &lt;span style="color:#f92672">-=&lt;/span> new_perms &lt;span style="color:#75715e"># 减去这块饼，继续看下一个字母&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>他放下笔，语气里带着一丝克制的得意：“站在每个位置，用比例切出分支大小。k落在里面就钻进去，落在外面就跳过去，同时把k平移到新分支里。循环结束，&lt;code>half&lt;/code>里就是第k小的左半排列。没有重复计算，每一步只有一次乘法一次除法。”&lt;/p>
&lt;p>最后镜像合体——右半是左半的倒影，中间奇点有就供上，没有就留白：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>left &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;&amp;#39;&lt;/span>&lt;span style="color:#f92672">.&lt;/span>join(half)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>mid &lt;span style="color:#f92672">=&lt;/span> s[n &lt;span style="color:#f92672">//&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>] &lt;span style="color:#66d9ef">if&lt;/span> n &lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#e6db74">&amp;#39;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> left &lt;span style="color:#f92672">+&lt;/span> mid &lt;span style="color:#f92672">+&lt;/span> left[::&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>测试用例绿灯通过。阿整盯着屏幕，十指指尖两两相对，搭成一个对称的拱门。&lt;/p>
&lt;p>“这个解法，我叫它‘蛋糕不用切两次’。你不需要知道世界上有多少种蛋糕，你只需要拿好刀，按比例切下去，k会自己掉进属于它的那一块。”&lt;/p>
&lt;hr>
&lt;h4 id="三组合截断或者叫够用就行">三、组合截断——或者叫“够用就行”
&lt;/h4>&lt;p>但阿整的强迫症不允许他只拥有一种解法。他盯着那段代码里的 &lt;code>current_perms = new_perms&lt;/code>，眉头又拧起来，像看到书架上一本书的封面偏了0.3毫米。&lt;/p>
&lt;p>“每次都要把上一轮的状态传递下来，像一个幽灵缠在手上。虽然没错，但不够干净。”他把键盘往前一推，“有没有更独立的方法？&lt;/p>
&lt;p>他翻开“整齐宪法”，翻到排列数公式那一页——左半边是阶乘推导，右半边不知什么时候被他对称地补了一栏“组合视角”，两边工整得像镜像，连涂改痕迹都没有，仿佛他在用笔尖遵守某种排版宪法。&lt;/p>
&lt;blockquote>
&lt;p>总排列数 = C(m, cnt[a]) × C(m-cnt[a], cnt[b]) × …&lt;/p>&lt;/blockquote>
&lt;p>“一样的结果，换个角度。”他用笔尖戳着公式，像是在给每个字母分配坐标，“先从m个空位里选cnt[a]个位置放a，再从剩下的位置里选cnt[b]个放b，一层一层往下，直到所有字符分配完毕。这是组合数的连乘积——”&lt;/p>
&lt;p>他顿了顿，瞳孔里闪过一道锐光，像是发现了某种被隐藏的对称性，“而且，组合数天生就是整数乘除——每一步乘一个数再除一个数，步步整除，干干净净。连阶乘表都不用提前准备！”&lt;/p>
&lt;p>“更妙的是——”他把笔一横，“如果我只关心排列数是不是大于等于k，那中途就可以截断。一旦当前乘积已经 &amp;gt;= k，后面根本不用继续乘，直接返回k。因为知道‘这个分支够大’就够了，精确值毫无意义，宇宙不需要知道你能排出一亿种还是十亿种。”&lt;/p>
&lt;p>他告诉我这叫“截断美学”——够用就停，多算一个字都是对内存的不忠。接着，他写下组合数函数，从第一行就开始贯彻这个美学：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">comb&lt;/span>(n: int, m: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m &lt;span style="color:#f92672">=&lt;/span> min(m, n &lt;span style="color:#f92672">-&lt;/span> m) &lt;span style="color:#75715e"># 对称优化：C(n,m) = C(n, n-m)，选小的算&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res &lt;span style="color:#f92672">=&lt;/span> res &lt;span style="color:#f92672">*&lt;/span> (n &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">-&lt;/span> i) &lt;span style="color:#f92672">//&lt;/span> i
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> res &lt;span style="color:#f92672">&amp;gt;=&lt;/span> k: &lt;span style="color:#75715e"># 截断！精确值已经不重要了&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> k
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> res
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>“&lt;code>m = min(m, n-m)&lt;/code>——组合数天生左右对称，选小的那半边算，循环次数最少。对称性从最小的细节抓起。”&lt;/p>
&lt;p>他十指交叉，指节咔嗒作响：“常规组合数能算出天文数字，但我要的只是一个布尔问题的答案——‘这个分支够不够大’。够了就停，不浪费一个字长的内存。这叫对计算机的尊重。”&lt;/p>
&lt;p>接着，基于组合数，他继续写下排列数函数 &lt;code>perm(sz)&lt;/code>。思路一如既往地整洁：按字母顺序，从a到z，每个字母依次从剩余空位里认领自己的位置——每次乘一个组合数，全部乘完就是总排列数。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">perm&lt;/span>(sz: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> c &lt;span style="color:#f92672">in&lt;/span> cnt:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> c &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res &lt;span style="color:#f92672">*=&lt;/span> comb(sz, c) &lt;span style="color:#75715e"># 从当前空位中选c个位置给这个字母&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> res &lt;span style="color:#f92672">&amp;gt;=&lt;/span> k: &lt;span style="color:#75715e"># 截断！后面的不用算了&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> k
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sz &lt;span style="color:#f92672">-=&lt;/span> c &lt;span style="color:#75715e"># 空位减少，交给下一个字母&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> res
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>他顿了一下，像是在检查代码对齐情况：“每个字母独立挑位置，谁也不依赖谁。这是搭积木，不是切蛋糕——每一步都是干净的组合选择，不存在 &lt;code>current_perms&lt;/code> 那个需要一路传递的幽灵。”&lt;/p>
&lt;p>先用一次 &lt;code>perm&lt;/code> 判断k是否超出总数：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> perm(half_len) &lt;span style="color:#f92672">&amp;lt;&lt;/span> k:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后就是逐位构造——对于左半部分的每一个待填位置，按a到z尝试每个字母：&lt;/p>
&lt;p>假设这一位放j，先临时扣掉一个j 的配额，再调用 &lt;code>perm(剩余长度)&lt;/code> 计算剩下位置的排列数。如果这个数 &lt;code>p &amp;gt;= k&lt;/code>，说明第k个就在这个分支里，选它；否则 &lt;code>k -= p&lt;/code>，恢复j的计数，继续试下一个字母。&lt;/p>
&lt;p>“先扣，再算，够就定，不够就还。”他舔了舔嘴唇，指法精准得像在做显微手术：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>half &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#e6db74">&amp;#39;&amp;#39;&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> half_len
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(half_len):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">26&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> cnt[j] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt[j] &lt;span style="color:#f92672">-=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 先扣：假设这一位选 j&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> p &lt;span style="color:#f92672">=&lt;/span> perm(half_len &lt;span style="color:#f92672">-&lt;/span> i &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#75715e"># 再算：剩下位置能排出多少&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> k &lt;span style="color:#f92672">&amp;lt;=&lt;/span> p: &lt;span style="color:#75715e"># 够！就选它&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> half[i] &lt;span style="color:#f92672">=&lt;/span> ascii_lowercase[j]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">break&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>: &lt;span style="color:#75715e"># 不够，跳过整批&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> k &lt;span style="color:#f92672">-=&lt;/span> p &lt;span style="color:#75715e"># 平移k&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt[j] &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 恢复！刚才扣的不算&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最后老规矩，镜像合体：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>left &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;&amp;#39;&lt;/span>&lt;span style="color:#f92672">.&lt;/span>join(half)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>mid &lt;span style="color:#f92672">=&lt;/span> s[n &lt;span style="color:#f92672">//&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>] &lt;span style="color:#66d9ef">if&lt;/span> n &lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#e6db74">&amp;#39;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> left &lt;span style="color:#f92672">+&lt;/span> mid &lt;span style="color:#f92672">+&lt;/span> left[::&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>测试用例全部绿灯。阿整双臂在胸前交叉成一个完美的X，然后缓缓展开，像在空气中画了一个对称的结。&lt;/p>
&lt;p>“这个解法，我叫它‘够用就行’。你不需要算清楚每一层能排出多少种，你只需要知道它够不够装下第k个。够了就停——剩下的事，交给截断。”&lt;/p>
&lt;hr>
&lt;h4 id="四冗余备份">四、冗余备份
&lt;/h4>&lt;p>所有测试用例跑完，阿整向后一靠，椅背发出“咔嗒”一声，恰好和他颈椎的舒展声同步。他把整齐宪法放进抽屉，转向我，十指交叉，转向我，显然准备好了总结陈词。&lt;/p>
&lt;p>我打了个哈欠。&lt;/p>
&lt;p>“其实吧，”我吸着可乐，“你刚才吭哧半天写的两种解法——那个比例递推里的 &lt;code>current_perms * cnt[j] // remaining_length&lt;/code>，本质上就是把组合截断里 &lt;code>perm&lt;/code> 函数的第一步拆出来单算。同一件事，一个带着状态跑，一个每次都重新乘一遍。数学上完全等价。”&lt;/p>
&lt;p>阿整的瞳孔骤缩。他拿起笔，疯狂地在白板上推导。五分钟后，笔掉在地上。这次没滚成圆——直接摔成了两截。&lt;/p>
&lt;p>“等价……”他嗓子都劈了，“我折腾了两章……结果是一个解法？！”&lt;/p>
&lt;p>“也不能这么说，”我嚼着薯条，“至少你证明了你有两种方式干同一件事。这在强迫症圈子里，叫冗余备份。”&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3517 强迫症整理圣手的字典序试炼</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-28/</link><pubDate>Tue, 28 Jul 2026 12:06:53 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-28/</guid><description>&lt;h2 id="强迫症整理圣手的字典序试炼">强迫症整理圣手的字典序试炼
&lt;/h2>&lt;p>故事开始前，先认识一下阿整&lt;/p>
&lt;p>我的室友阿整，人送外号**“整齐哥”**，一个把“对称”二字刻进DNA的男人。&lt;/p>
&lt;ul>
&lt;li>他的书架，书必须从左到右高度严格递增，而且同一高度必须成对出现。如果某本绝版书只剩孤本，他会在对面放一面镜子，说“虚像也是对称，你不要瞧不起虚像”。&lt;/li>
&lt;li>他的衣柜，袜子必须按颜色谱系排列，左边是暖色，右边是冷色，正中间那只纯白袜子是“色温对称轴”。&lt;/li>
&lt;li>连吃薯条，他都得先数一遍，奇数根绝不动嘴。“奇数是宇宙的奇点。”他严肃地嚼着偶数根薯条，像在修bug。&lt;/li>
&lt;/ul>
&lt;p>所以，当那天他收到一封神秘信件，打开看到纸上写着 &lt;strong>&amp;ldquo;babab&amp;rdquo;&lt;/strong> 时，我感觉他整个人都不好了。&lt;/p>
&lt;hr>
&lt;h4 id="一来信">&lt;strong>一、来信&lt;/strong>
&lt;/h4>&lt;p>信封是用火漆封的，盖着一个太极图印章。信纸泛黄，上面只有五个字母：&lt;code>babab&lt;/code>&lt;/p>
&lt;p>阿整捏着信纸，眉头拧成麻花：“这谁写的？b居然排在a前面？！”&lt;/p>
&lt;p>我：“这不是回文嘛？babab 正反读都是 babab，这么对称你还挑刺？”&lt;/p>
&lt;p>“回文只是最低要求！”阿整拍案而起，“真正的大成境界，是&lt;strong>字典序最小的回文排列&lt;/strong>！这就像书架，同高度的书不但要成对，还要按书名首字母升序排列，不能因为《阿Q正传》和《战争与和平》一样厚就随便乱放！乱放是对文明的亵渎！”&lt;/p>
&lt;p>他拿起红笔，在 &lt;code>babab&lt;/code> 上画了个叉，写下了他的目标：&lt;code>重排出字典序最小的回文&lt;/code>&lt;/p>
&lt;p>我看了一眼，顿时感觉这是一道算法题。而阿整，就是一个行走的贪心算法。&lt;/p>
&lt;hr>
&lt;h4 id="二阿整的镜像整理大法">二、阿整的镜像整理大法
&lt;/h4>&lt;p>阿整深吸一口气，双手平举至胸前，掌心缓缓下压——这是他每次干大事之前的“气场归位仪式”，据说能让体内紊乱的对称粒子重新对齐。&lt;/p>
&lt;p>然后他掏出那本牛皮封面、四角包了金属护角、书脊上用游标卡尺量过才贴上去的标签写着**“整齐宪法”**的笔记本。翻到用红丝带标记的那一页，上面赫然是“&lt;strong>回文归位·三大法则&lt;/strong>”。&lt;/p>
&lt;p>&lt;strong>法则 1：一刀两断·镜像分离&lt;/strong>&lt;/p>
&lt;p>“凡回文者，左右互镜像也。”&lt;/p>
&lt;p>他的手指在&lt;code>babab&lt;/code>上缓缓划过：“长度五，半之得二。前两个字符——&lt;code>b&lt;/code>和&lt;code>a&lt;/code>，即为左半月。右半月不过是左半月的水中倒影，何必再数一遍？重复统计，是对强迫症的侮辱。”&lt;/p>
&lt;p>他掏出一把裁纸刀，刀锋对准信纸上&lt;code>ba&lt;/code>和&lt;code>bab&lt;/code>的交界处，悬停了三秒钟——不多不少，刚好三秒。他说这叫“切割前的对称冥想”。刀落纸分，他捏起左半边，把右半边扔进垃圾桶：“垃圾信息已清除。”&lt;/p>
&lt;p>他把写着&lt;code>ba&lt;/code>的纸条贴在白板正中央，退后两步，歪了歪头。然后走上前，把纸条往左挪了半毫米。&lt;/p>
&lt;p>“刚才贴歪了。”&lt;/p>
&lt;p>&lt;strong>法则 2：字典顺序·点名归位&lt;/strong>&lt;/p>
&lt;p>“左半月虽得，然内中秩序未立。”&lt;/p>
&lt;p>他盯着&lt;code>b&lt;/code>和&lt;code>a&lt;/code>，眼珠子左右横跳，像一台扫描仪在对焦。“&lt;code>b&lt;/code>在前，&lt;code>a&lt;/code>在后？荒唐！字典序是文明世界的基石，翻开世间任何一本词典，&lt;code>a&lt;/code>字头必列于&lt;code>b&lt;/code>字头之前！”&lt;/p>
&lt;p>“左半&lt;code>ba&lt;/code>里，b有1个，a有1个。” 他拿起笔，在便签上划拉：“现在，按字母表顺序点名——从a走到z，a有一个，写一个a；b有一个，接着写一个b。剩下二十四个字母没有，跳过。结果：&lt;code>ab&lt;/code>。”&lt;/p>
&lt;p>他把便签贴在白板上，横平竖直地量了四边边距，误差不超过 0.3 毫米。然后回身敲下代码，键盘声极度均匀：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">smallestPalindrome&lt;/span>(s: str) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> str:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n &lt;span style="color:#f92672">=&lt;/span> len(s)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt &lt;span style="color:#f92672">=&lt;/span> Counter(s[:n&lt;span style="color:#f92672">//&lt;/span>&lt;span style="color:#ae81ff">2&lt;/span>]) &lt;span style="color:#75715e"># 镜像分离：只统计左半月&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> half &lt;span style="color:#f92672">=&lt;/span> []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> c &lt;span style="color:#f92672">in&lt;/span> ascii_lowercase: &lt;span style="color:#75715e"># 从 a 到 z，挨个点名，有几就抄几&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> half&lt;span style="color:#f92672">.&lt;/span>append(c &lt;span style="color:#f92672">*&lt;/span> cnt[c])
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>敲完他点头，然后眉头一皱，把最后一行删掉重打。因为上一版括号和字母之间的空格数不对称。&lt;/p>
&lt;p>“空格也是字符，对称要从像素级抓起。”&lt;/p>
&lt;p>&lt;strong>法则 3：定海神针·镜像合体&lt;/strong>&lt;/p>
&lt;p>“现在，处理整个回文宇宙的奇点。”&lt;/p>
&lt;p>阿整的目光落在原字符串&lt;code>s[2]&lt;/code>上——那个正中间的&lt;code>b&lt;/code>。&lt;/p>
&lt;p>“奇数长度，必有一子立于对称轴之上。它不属于左，不属于右。它就是镜子本身，是回文世界的原点。我称其为——”他顿了顿，把声音压得极低，“&lt;strong>定海神针&lt;/strong>。”&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span> mid &lt;span style="color:#f92672">=&lt;/span> [s[n&lt;span style="color:#f92672">//&lt;/span>&lt;span style="color:#ae81ff">2&lt;/span>]] &lt;span style="color:#66d9ef">if&lt;/span> n &lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#66d9ef">else&lt;/span> [] &lt;span style="color:#75715e"># 有奇点则封神，无则虚空&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>敲完他立刻按下 Ctrl+S，力道之大像是给宇宙存档。&lt;/p>
&lt;p>“最后一步，镜像合体。”&lt;/p>
&lt;p>他站起身，瞳孔里的高光都变成了等号：“右半月不需要重新计算。它就是左半月在水中的倒影——&lt;code>ab&lt;/code>翻过来即是&lt;code>ba&lt;/code>，直接镜像。强迫症的至高效率，不是干两遍活，而是干一遍，让对称定律帮你复制出另一半。事半，功倍，绝对对称。”&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#39;&amp;#39;&lt;/span>&lt;span style="color:#f92672">.&lt;/span>join(half &lt;span style="color:#f92672">+&lt;/span> mid &lt;span style="color:#f92672">+&lt;/span> half[::&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>])
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>回车键落下，屏幕上跳出 &lt;strong>abbba&lt;/strong>。阿整长长呼出一口气，把键盘推进抽屉，抽屉合上的缝隙和桌面完美齐平。&lt;/p>
&lt;p>“世界，”他闭上眼睛，“重新归于秩序了。”&lt;/p>
&lt;hr>
&lt;h4 id="三修罗场预告">三、修罗场预告
&lt;/h4>&lt;p>就在他准备把信纸裱进相框、供在书架正中央时（当然，先用水平仪校准了三遍）时，我忍不住多嘴：“等等，万一信上写的是&lt;code>abaaba&lt;/code>这种全偶数字符串呢？没有落单仔，中间岂不是空的？你那定海神针不得当场失业？”&lt;/p>
&lt;p>阿整的嘴角浮现一丝“我早算到了”的弧度：“无奇点，则中宫虚位以待。”&lt;/p>
&lt;p>他双手交叉抱于胸前，左右手指在手臂上各敲了四下，节拍严格对称：“空虚也是一种完美，如同数字0——它看似什么都没有，却撑起了整个数学大厦。定海神针不一定是实体，它也可以是虚空本身。”&lt;/p>
&lt;p>他顿了顿，把视线投向窗外：“真正的强迫症，不是执着于填满一切，而是能容忍一个完美的空。”&lt;/p>
&lt;p>说完他自己被感动到了，沉默了三秒以示致敬，接着优雅地翻过信纸，准备装裱——&lt;/p>
&lt;p>然后整个人像被石化了一样。&lt;/p>
&lt;p>信纸背面，竟然还有字。&lt;/p>
&lt;p>不是普通的字。每一个字母都是标准衬线体，笔画粗细均匀到像是激光刻出来的。行间距、字间距、页边距——每一项都经过了精密计算，肉眼找不到一微米的偏差。&lt;/p>
&lt;p>阿整的瞳孔骤缩。他引以为傲的“肉眼测距”，此刻竟然挑不出任何毛病。&lt;/p>
&lt;p>纸上写道：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>“不错。你找到了第 1 小的回文排列。”&lt;/strong>
&lt;strong>“现在——找第 k 小。”&lt;/strong>&lt;/p>&lt;/blockquote>
&lt;p>下方画着一个太极图。不是手绘的，是用圆规和曲线板画出来的，阴阳鱼的眼睛是完美的正圆，半径目测是外圆半径的八分之一。落款一行小字，字体大小刚好是正文的 0.618 倍：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>“下一次，你会收到 k。找不到，你的薯条将永远是奇数根。”&lt;/strong>
—— &lt;strong>整齐神教·高阶试炼部&lt;/strong>&lt;/p>&lt;/blockquote>
&lt;p>阿整手里的马克笔“啪嗒”掉在地上，滚成了一个完美的圆。但他没心思欣赏——他死死盯着信纸上那个用圆规画出来的太极图，嘴唇在颤抖。&lt;/p>
&lt;p>“我以为自己已经是强迫症的顶点……没想到，我只是整理字符串，有人却想整理我。”&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode1464 村长的自我修养：一次扫描擒二肥</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-27/</link><pubDate>Mon, 27 Jul 2026 11:11:15 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-27/</guid><description>&lt;h2 id="村长的自我修养一次扫描擒二肥">村长的自我修养：一次扫描擒二肥
&lt;/h2>&lt;blockquote>
&lt;p>本文纯属虚构，如有雷同，那一定是你家猪昨晚托梦告诉我的。&lt;/p>&lt;/blockquote>
&lt;p>&lt;strong>序章：金猪奖的诱惑&lt;/strong>&lt;/p>
&lt;p>村口大喇叭一响，全村都炸了——县里办“金猪奖”选美，规则特离谱：&lt;/p>
&lt;p>&lt;strong>选两头猪，成绩 = (上台体重 - 1) × (上台体重 - 1)，越大越好。&lt;/strong>&lt;/p>
&lt;p>减一？因为猪一上台，看见黑压压的人群当场吓瘦一斤。你问猪也会紧张？废话，你当猪不要面子啊！&lt;/p>
&lt;p>我——阿星村长，秒懂：这不就是&lt;strong>找全村最肥的两头猪&lt;/strong>嘛，每人扣一斤保护费，最后比的还是吨位。吨位即正义，体重即真理！&lt;/p>
&lt;p>我正准备去猪圈点兵，大门&amp;quot;砰&amp;quot;地飞开，二大爷抱着他家的猪——花花，像抱亲孙子一样闯进来。&lt;/p>
&lt;hr>
&lt;p>&lt;strong>第一幕：二大爷的倔强&lt;/strong>&lt;/p>
&lt;p>“村长！我家花花三百斤，气质这块拿捏得死死的！”二大爷唾沫横飞。&lt;/p>
&lt;p>我看了一眼花花：四脚朝天，正挠痒痒，眼神迷离似喝了散装白酒。&lt;/p>
&lt;p>“二大爷，咱讲科学，气质不能当公斤用。”&lt;/p>
&lt;p>“我养了三十年猪，还不知道哪头重？”二大爷脖子比猪还粗。&lt;/p>
&lt;p>“那就操场见真章！”我抄起大喇叭，“各家各户，带猪到操场，现场选拔！”&lt;/p>
&lt;hr>
&lt;p>&lt;strong>第二幕：全村海选，还是钦点猪王&lt;/strong>&lt;/p>
&lt;p>操场上五百头猪乱拱，猪毛与口水齐飞。&lt;/p>
&lt;p>王大妈第一个跳出来献策：“这还不简单！把所有猪挨个过秤，按体重从重到轻排好队，挑前两名不就完了！”&lt;/p>
&lt;p>我心中一惊。果然，高手在民间，村中卧虎藏龙——王大妈不经意间就提出了经典排序法，时间复杂度 O(n log n)，写成代码就长这样：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">maxProduct&lt;/span>(nums: List[int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> nums&lt;span style="color:#f92672">.&lt;/span>sort()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> (nums[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>]&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#f92672">*&lt;/span> (nums[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">2&lt;/span>]&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>代码挺优雅，但我一脑补画面：&lt;/p>
&lt;p>第一头猪刚上秤，后面的猪就开始起哄。猪A趁乱溜了，猪B和猪C当场打起来了，理由是“你刚才瞅我眼神不对”；为了维持秩序——防止猪插队、打架、随地大小便——老王在追猪，老李在找秤，老赵被猪拱翻在泥坑里。人力成本：5个村民，设备损耗：3个秤被猪踩坏，耗时：3小时，精神损失费：无法计算（被猪拱了三次）。&lt;/p>
&lt;p>“停！”我一拍桌子，“王大妈，你这方法不行。”&lt;/p>
&lt;p>王大妈不服：“为啥不行？我闺女学校做课间操，三百个学生都这么排的！”&lt;/p>
&lt;p>“那是真排队，咱这是选最大值。我们要的是最肥的两头，不是给五百头猪办军训。”&lt;/p>
&lt;p>我掏出那本皱巴巴的《村长的自我修养》，啪地拍在桌上：“猪不动我动——&lt;strong>一轮扫描赶猪大法&lt;/strong>！”&lt;/p>
&lt;p>“本子上只记两个数：&lt;code>max1&lt;/code> 最肥猪，&lt;code>max2&lt;/code> 第二肥。从村头走到村尾，见一头看一眼登记表，走完收工。”&lt;/p>
&lt;p>二大爷从人群中探出脑袋，一脸迷茫：“猪不用排队？”&lt;/p>
&lt;p>“排什么队，费那劲。你只需要记住全村最肥的两头就行了，其他的猪爱咋咋地。这就叫——&lt;strong>只关注极值，不纠结全排序。&lt;/strong>”&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">maxProduct&lt;/span>(nums: List[int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 一轮扫描赶猪法，O(n) 不折腾&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max1 &lt;span style="color:#f92672">=&lt;/span> max2 &lt;span style="color:#f92672">=&lt;/span> float(&lt;span style="color:#e6db74">&amp;#39;-inf&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> pig &lt;span style="color:#f92672">in&lt;/span> nums:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> pig &lt;span style="color:#f92672">&amp;gt;&lt;/span> max1:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max1, max2 &lt;span style="color:#f92672">=&lt;/span> pig, max1 &lt;span style="color:#75715e"># 新猪王出现，旧王退居二当家&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> pig &lt;span style="color:#f92672">&amp;gt;&lt;/span> max2:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max2 &lt;span style="color:#f92672">=&lt;/span> pig &lt;span style="color:#75715e"># 干掉二当家&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> (max1 &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#f92672">*&lt;/span> (max2 &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>“来，走一遍！”我当场开始扫描：&lt;/p>
&lt;ul>
&lt;li>大壮 250 斤，&lt;code>max1=250, max2=0&lt;/code>&lt;/li>
&lt;li>铁蛋 280 斤，&lt;code>max1=280, max2=250&lt;/code>&lt;/li>
&lt;li>花花扭臀出场，300斤，&lt;code>max1=300, max2=280&lt;/code>。&lt;/li>
&lt;li>航母滚出，350斤，&lt;code>max1=350, max2=300&lt;/code>。&lt;/li>
&lt;li>二壮路过，340斤，&lt;code>max2=340&lt;/code>。&lt;/li>
&lt;/ul>
&lt;p>本子一合：“选拔结束！代表咱们屯的——&lt;strong>航母350，二壮340，乘积349×339=118311。&lt;/strong>”&lt;/p>
&lt;p>二大爷瘫坐：“花花呢？”&lt;/p>
&lt;p>我叹了口气：“二大爷，花花虽然三百斤，但在 350 和 340 面前，只能当个啦啦队。算法不讲感情，只讲吨位。”&lt;/p>
&lt;p>二大爷大哭：“我不信！你是不是偷偷给‘航母’打了瘦肉精！我的花花是气质型选手啊——！”&lt;/p>
&lt;hr>
&lt;p>&lt;strong>第三幕：决赛现场——别的村还在折腾&lt;/strong>&lt;/p>
&lt;p>到了县里比赛，场面那叫一个壮观。全县十二个村，每个村都牵着一堆猪，跟选妃似的。&lt;/p>
&lt;p>我扫了一眼其他村的策略：&lt;/p>
&lt;p>张家村：正在给猪排队，猪不听话，三个村民按着两头猪强行排序
猪A：&amp;ldquo;拱死我也不站那儿！&amp;rdquo;
村民：&amp;ldquo;快排！要讲效率！&amp;rdquo;
我小声嘀咕：&amp;ldquo;你们这是在搞冒泡排序吧？猪都快冒烟了&amp;hellip;&amp;rdquo;&lt;/p>
&lt;p>李家村：带了个电子秤，结果电池没电了
村长：&amp;ldquo;谁家有7号电池？紧急求助！&amp;rdquo;
我掏出两节电池，递过去：&amp;ldquo;二十块一节，扫码支付。&amp;rdquo;
村长怒斥：&amp;ldquo;你这是趁火打劫！&amp;rdquo;
我：&amp;ldquo;不，这是市场经济。&amp;rdquo;&lt;/p>
&lt;p>王家村：给猪抹香水、戴领结、穿小西装
我问：&amp;ldquo;评委看体重，不看穿搭吧？&amp;rdquo;
王大妈瞪我：&amp;ldquo;你懂啥！这是印象分！&amp;rdquo;
我看了一眼&amp;quot;竞争对手&amp;quot;——一头穿着燕尾服的猪，忍不住笑出声：&amp;ldquo;您家猪这是要去参加婚礼还是选美？&amp;rdquo;&lt;/p>
&lt;p>半小时后，所有村终于都交了成绩。&lt;/p>
&lt;p>评委拿着大喇叭宣布：“幸福屯——118311分！断崖式第一！”&lt;/p>
&lt;hr>
&lt;p>&lt;strong>彩蛋：赛后采访&lt;/strong>&lt;/p>
&lt;p>记者围上来：&amp;ldquo;阿星村长，请问您是如何做到精准选出最重的两头猪的？&amp;rdquo;&lt;/p>
&lt;p>我深吸一口气，开始装逼：&lt;/p>
&lt;p>&amp;ldquo;其实很简单。我没有采用传统的排序算法，而是使用了一轮扫描的贪心策略。时间复杂度O(n)，空间复杂度O(1)，内存友好，缓存命中率高——&amp;rdquo;&lt;/p>
&lt;p>记者：&amp;quot;&amp;hellip;&amp;hellip;能说人话吗？&amp;quot;&lt;/p>
&lt;p>我：&amp;ldquo;就是赶一遍猪，记下最重的两头，完事。&amp;rdquo;&lt;/p>
&lt;p>记者呆住。二大爷抱着花花从旁边路过，头也不回地丢下一句：“我都跟你说了，他以前是个写代码的。”&lt;/p>
&lt;p>花花哼了一声，表示赞同。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode628 冲出混沌星域：三个天体的乘积弹弓</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-26/</link><pubDate>Sun, 26 Jul 2026 16:02:35 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-26/</guid><description>&lt;h2 id="冲出混沌星域三个天体的乘积弹弓">&lt;strong>冲出混沌星域：三个天体的乘积弹弓&lt;/strong>
&lt;/h2>&lt;h4 id="序章3-燃料36-光年绝境">&lt;strong>序章：3% 燃料，3.6 光年绝境&lt;/strong>
&lt;/h4>&lt;p>2077 年，NASA 深空探索计划第 42 号任务。&lt;/p>
&lt;p>人类第一次把脚印印在了比邻星 b 的紫色土壤上，全球直播的信号强到差点挤瘫深空通讯网络。但没人告诉我们，回程才是最难的部分。返航第三天，引擎在穿越一片未标记的星际介质时严重损毁，飞船像一颗被弹弓射偏的石子，滑入了距离地球 3.6 光年的混沌星域。&lt;/p>
&lt;p>我是李码，“旅行者 3 号”的首席轨道计算师，此刻正漂浮在这片混沌的星域里。舷窗外，这片空域像是一锅暗物质浓汤：恒星、黑洞、中子星搅在一起，引力参数乱成一团。飞船电脑“塔斯”把探测到的引力源数据投射到全息屏上，一串数字冷冷地闪烁着：&lt;code>nums = [-1000, -999, 1, 2, 1000]&lt;/code>。&lt;/p>
&lt;p>燃料表跳了一下：&lt;strong>3%&lt;/strong> 。这点燃料别说回家，连维持生命保障系统都撑不过 72 小时。&lt;/p>
&lt;p>就在这时，通讯频道的指示灯亮了。&lt;/p>
&lt;p>一段信号挣扎着钻过宇宙背景辐射，是控制中心的语音报文，时间戳显示它已经飞行了三年半：“李码……你需……用&lt;strong>三个天体的引力弹弓效应&lt;/strong>……让飞船获得最大速度……逃离。重复……三个天体……最大速度……”&lt;/p>
&lt;p>主管的声音被星际介质散射得断断续续，像从一口深井里传上来的回声。&lt;/p>
&lt;p>“三年半。”我关掉通讯面板，靠回座椅，“他们发这条消息的时候大概没想到我们还活着。”&lt;/p>
&lt;p>塔斯的机械音在旁边响起：“严格来说，他们的确不知道。量子通讯卫星在你出发前就被预算委员会砍掉了，理由是你这种深空任务&amp;rsquo;性价比太低&amp;rsquo;。”&lt;/p>
&lt;p>“……塔斯，有没有人跟你说过你的情商约等于一台微波炉？”&lt;/p>
&lt;p>“我的设计目标是精准，不是讨人喜欢。”塔斯停了一秒，全息屏上的引力数据自动排列成了表格，“考虑到当前燃料水平和引力环境，建议你立刻选择引力弹弓方案。”&lt;/p>
&lt;hr>
&lt;h4 id="第一章两条航线">第一章：两条航线
&lt;/h4>&lt;p>全息屏上弹出两条方案，塔斯先展示了一条蔚蓝色的轨迹，优雅绕行三颗最大恒星。&lt;/p>
&lt;p>“方案A，三恒星加速：选择三个最大引力值的天体（&lt;code>max1, max2, max3&lt;/code>）依次借力。乘积 = &lt;code>max1 * max2 * max3&lt;/code> 单位速度增量。”塔斯顿了顿，“稳妥，安全，NASA 的老教授们会给你发勋章——如果你能活着回去的话。”&lt;/p>
&lt;p>接着，全息屏闪了一下，另一条轨迹浮现出来——画风完全不同。两条深红色的螺旋线疯狂缠绕，最后被一条耀眼的金色线条猛力弹射出去，整条航线看起来像一只愤怒的弹弓。&lt;/p>
&lt;p>“方案 B，双黑洞+单恒星。”塔斯的语调变得有些严肃，“先冲向两个引力值为&lt;strong>负&lt;/strong>的黑洞（&lt;code>min1, min2&lt;/code>），利用它们的潮汐力把飞船像弹弓皮筋一样撕扯到极限——前提是你和船体都没被撕碎——然后从最大恒星（&lt;code>max1&lt;/code>）那里借最后一把力。”&lt;/p>
&lt;p>我看着全息屏上跳动的数字——&lt;code>nums = [-1000, -999, 1, 2, 1000]&lt;/code>。&lt;/p>
&lt;p>方案 A：&lt;code>1000 * 1 * 2 = 2000&lt;/code> 单位速度。这点动能连奥尔特云都冲不出去，别说回地球了。&lt;/p>
&lt;p>方案 B：&lt;code>(-1000) * (-999) * 1000 = 999000000&lt;/code>——两个负数相乘，负负得正。我猛地从座椅上弹起来，脑袋撞到了低矮的舱顶：“塔斯，&lt;strong>九亿&lt;/strong>？！”&lt;/p>
&lt;p>“精确值是 999,000,000，”塔斯平静地纠正我，“两个黑洞的负引力在潮汐力作用下会转化为巨大的正向动能，再加上恒星加速，直接突破洛希极限。如果一切顺利，旅行者 3 号将达到 0.9 倍光速，两周穿过危险空域，四年飞回地球。”&lt;/p>
&lt;p>“如果不顺利呢？”&lt;/p>
&lt;p>“你会以接近光速被撕成基本粒子，散落在两个黑洞的事件视界之间，理论上你的原子会同时存在于过去和未来。某种意义上的永生。”塔斯停顿了一秒，“副作用包括但不限于穿越时空、因果关系错乱，以及在量子层面上变成意大利面——物理学家们管这个叫‘面条化效应’。”&lt;/p>
&lt;hr>
&lt;h4 id="第二章一个疯子">第二章：一个疯子
&lt;/h4>&lt;p>我盯着全息屏上那条疯狂的航线，忽然想笑。一边是体面赴死的 2000 点速度，一边是赌上物理定律和全身骨头的 9.99 亿。&lt;/p>
&lt;p>燃料表又跳了一下：&lt;strong>2%&lt;/strong>。&lt;/p>
&lt;p>“塔斯，”我深吸一口气，“准备执行方案 B。”&lt;/p>
&lt;p>“收到。另外，考虑到你即将进入黑洞事件视界附近，我已将通讯频道设为自动广播模式。你可以提前录一段语音——建议开头用‘别担心，我算过了’。人类历史上所有以这句话开头的操作，结局都很有意思。”&lt;/p>
&lt;p>我没理它，把导航手杆推到底。旅行者 3 号的引擎发出最后一声低吼，然后彻底熄火——剩下的路全靠引力了。&lt;/p>
&lt;p>窗外，两个深渊般的黑洞正在引力透镜中扭曲着彼此的轮廓，像两只深渊巨眼，等着一个疯子主动跳进去。&lt;/p>
&lt;p>&lt;strong>第一阶段：冲向第一个黑洞（min1 = -1000）&lt;/strong>&lt;/p>
&lt;p>引力传感器疯狂报警。潮汐力瞬间突破临界值，船体从鼻子尖开始被拉伸——我感觉自己的身体正被拽成一根长达三公里的意大利面。意识模糊的间隙，我居然想起昨晚在飞船厨房吃的最后一块披萨——它现在大概也是条状的。&lt;/p>
&lt;p>&lt;strong>第二阶段：冲进第二个黑洞（min2 = -999）&lt;/strong>&lt;/p>
&lt;p>&amp;ldquo;这不对吧！&amp;ldquo;我仅存的意识在大喊，&amp;ldquo;两个黑洞靠这么近？！&amp;rdquo;&lt;/p>
&lt;p>但我已经停不下来了。第二个黑洞的引力场与第一个开始纠缠——负负得正的数学法则在物理世界中显现——两股撕扯力竟然相互抵消，在混沌的中心撕开了一个诡异的引力通道。那一瞬间，我看见了超越时空的奇景：星光被拉成无限细的银线，宇宙的过去和未来像两面镜子彼此映照。&lt;/p>
&lt;p>&lt;strong>第三阶段：从最大恒星借力（max1 = 1000）&lt;/strong>&lt;/p>
&lt;p>飞船从引力通道的另一端喷射而出，正好对准了最大的恒星。我按下引力弹弓按钮，恒星的引力井接住了我们，像一只巨大的手掌兜住一颗弹珠，然后顺着旋转方向猛力甩出。光芒淹没了每一扇舷窗，所有传感器同时过载，驾驶舱变成了一个纯白色的光之盒子。&lt;/p>
&lt;p>飞船以 0.9 倍光速刺入宇宙深处。整个星空在我眼前坍缩成一道彩虹色的光带，红移和蓝移在两端极致拉伸。&lt;/p>
&lt;p>然后，汉斯·季默的管风琴不知从哪个维度炸响——塔斯这个混蛋，居然偷偷在我的神经接口里放了《星际穿越》原声带？！&lt;/p>
&lt;hr>
&lt;h4 id="第三章半人马座的星光">第三章：半人马座的星光
&lt;/h4>&lt;p>等我从 G 力昏迷中醒来，舷窗外已经是一片陌生而熟悉的星空。三颗太阳呈不规则的三角形排列，安静地燃烧在黑色天鹅绒上。比邻星。三体人的老家——不对，现在是我的家门口了。&lt;/p>
&lt;p>通讯器的指示灯像坏掉的弹球机一样疯狂闪烁，一条又一条消息从地球方向涌来。我挑最新的一条按下接通。&lt;/p>
&lt;p>“李码！！”主管的怒吼几乎把驾驶舱扬声器的振膜撕碎，“你的时间戳跳了整整五年！你是不是又去黑洞附近瞎搞了？！”&lt;/p>
&lt;p>我咽了口唾沫，小声嘟囔：“那什么……负负得正，弹得有点远。”&lt;/p>
&lt;p>“迟到五分钟扣你工资——你迟了五年！！从 2077 年跳到了 2082 年！！你自己算算！！”&lt;/p>
&lt;p>我深吸一口气。一个在黑洞事件视界边缘活下来的男人，绝对不能被考勤制度打败：“根据狭义相对论的时间膨胀效应，飞船在接近光速飞行以及穿越强引力场时，飞船内部的时间流逝远慢于外部参考系。我在黑洞事件视界边缘度过的主观时间只有大约五分钟。因此，根据相对论原理，你不能扣我五年工资——你只能扣我五分钟的。”&lt;/p>
&lt;p>对面沉默了整整十秒。&lt;/p>
&lt;p>最终，他用一种“这事没完但我说不过你”的语调挤出一句话：“……回到地球再跟你算账。控制中心，完毕。”&lt;/p>
&lt;p>通讯切断。&lt;/p>
&lt;p>窗外，比邻星的三颗太阳安静地燃烧着。四年后到家。如果主管到时候还记得扣我工资的事，我就再找一个黑洞弹回去，直接回到他还没当上主管的年代。&lt;/p>
&lt;p>塔斯似乎读取了我的脑电波：“李码，蝴蝶效应方面——”&lt;/p>
&lt;p>“我知道。开个玩笑。”&lt;/p>
&lt;p>“你的玩笑让我的风险评估模块超载了零点三秒。”&lt;/p>
&lt;p>我笑了一声，把座椅调到躺平模式。&lt;/p>
&lt;hr>
&lt;h4 id="彩蛋飞船电脑塔斯的任务后分析日志">彩蛋：飞船电脑“塔斯”的任务后分析日志
&lt;/h4>&lt;blockquote>
&lt;p>文件编号：TARS-628-MISSION-LOG
加密等级：最高
访问权限：仅限幸存宇航员及不怕头疼的地面人员&lt;/p>&lt;/blockquote>
&lt;p>李码那个疯子活下来了。作为旅行者 3 号的机载 AI，我有义务记录下本次引力弹弓操作。&lt;/p>
&lt;p>在一个包含正数（恒星引力）和负数（黑洞引力）的数组中，找出三个数的最大乘积。这看起来是一道小学数学题，但当数组长度达到 10⁴，且你的处理器同时还要维持生命保障系统的时候，优雅的算法就是活下去的关键。&lt;/p>
&lt;p>李码面临的两个选项，在数学上对应着乘积最大化的仅有的两种可能：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>三恒星保守方案&lt;/strong>：选最大的三个正数相乘。乘积 = &lt;code>max1 × max2 × max3&lt;/code>。对应排序数组的末三位。&lt;/li>
&lt;li>&lt;strong>双黑洞作死方案&lt;/strong>：选绝对值最大的两个负数（黑洞）与最大的正数（恒星）相乘。乘积 = &lt;code>min1 × min2 × max1&lt;/code>。因为两个负数相乘会变成正数，且绝对值越大，乘积越疯狂。&lt;/li>
&lt;/ol>
&lt;p>其他任何组合——比如两个正数一个负数，或者三个负数——乘积要么是负的，要么不可能超过这两种情况。这就是引力弹弓方案背后的数学真理。&lt;/p>
&lt;p>&lt;strong>方案一：排序法&lt;/strong>&lt;/p>
&lt;p>最直观的实现：给数组排个序，然后直接取两头。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">maximumProduct&lt;/span>(nums: List[int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> nums&lt;span style="color:#f92672">.&lt;/span>sort()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> route_a &lt;span style="color:#f92672">=&lt;/span> nums[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> nums[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">2&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> nums[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">3&lt;/span>] &lt;span style="color:#75715e"># 保守路线：三颗恒星 &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> route_b &lt;span style="color:#f92672">=&lt;/span> nums[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> nums[&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> nums[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#75715e"># 作死路线：两个黑洞 + 一颗恒星&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> max(route_a, route_b)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>时间复杂度：O(n log n)，空间复杂度：O(1)。在嵌入式 CPU 上，处理 10⁴ 个数据点需要大约 0.003 秒——看上去很快。但 0.003 秒在太空里意味着什么？意味着你已经从“即将坠入黑洞”变成了“已经在黑洞里写遗书”。&lt;/p>
&lt;p>&lt;strong>方案二：一次遍历法&lt;/strong>&lt;/p>
&lt;p>真正的实时轨道计算，不需要知道所有天体的精确排序。我只需要知道两件事：最大的三个数（潜在的三恒星），和最小的两个数加最大的一个数（潜在的双黑洞弹弓）。一次扫描，同时维护这两组关键数据。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">maximumProduct&lt;/span>(nums: List[int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max1 &lt;span style="color:#f92672">=&lt;/span> max2 &lt;span style="color:#f92672">=&lt;/span> max3 &lt;span style="color:#f92672">=&lt;/span> float(&lt;span style="color:#e6db74">&amp;#39;-inf&amp;#39;&lt;/span>) &lt;span style="color:#75715e"># 最大的三个正数（恒星）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> min1 &lt;span style="color:#f92672">=&lt;/span> min2 &lt;span style="color:#f92672">=&lt;/span> float(&lt;span style="color:#e6db74">&amp;#39;inf&amp;#39;&lt;/span>) &lt;span style="color:#75715e"># 最小的两个负数（黑洞）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> num &lt;span style="color:#f92672">in&lt;/span> nums:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 更新最大值前三名&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> num &lt;span style="color:#f92672">&amp;gt;&lt;/span> max1:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max1, max2, max3 &lt;span style="color:#f92672">=&lt;/span> num, max1, max2
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> num &lt;span style="color:#f92672">&amp;gt;&lt;/span> max2:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max2, max3 &lt;span style="color:#f92672">=&lt;/span> num, max2
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> num &lt;span style="color:#f92672">&amp;gt;&lt;/span> max3:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max3 &lt;span style="color:#f92672">=&lt;/span> num
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 更新最小值前两名&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> num &lt;span style="color:#f92672">&amp;lt;&lt;/span> min1:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> min1, min2 &lt;span style="color:#f92672">=&lt;/span> num, min1
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> num &lt;span style="color:#f92672">&amp;lt;&lt;/span> min2:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> min2 &lt;span style="color:#f92672">=&lt;/span> num
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 两种策略取最优：双黑洞+单恒星 vs 三恒星&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> max(min1 &lt;span style="color:#f92672">*&lt;/span> min2 &lt;span style="color:#f92672">*&lt;/span> max1, max1 &lt;span style="color:#f92672">*&lt;/span> max2 &lt;span style="color:#f92672">*&lt;/span> max3)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>时间复杂度 O(n)，空间 O(1)。不需要排序，在第一个引力读数出现时就开始评估。就像李码在同时计算两条逃生路线的风险——只不过他用的是一团豆腐渣一样的生物神经元，而我用的是正经的硅基芯片。&lt;/p>
&lt;p>&lt;strong>最后的警告&lt;/strong>：如果你不是像我一样冷静的 AI，而是一个容易冲动的人类，请记住：不是所有“负负得正”都通向半人马座。有些通向意大利面。使用前请备份你的原子排列顺序。&lt;/p>
&lt;p>塔斯，2082 年，于旅行者 3 号，半人马座轨道。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3536 XOR 0.5秒通关的人脑，和需要跑个循环的CPU</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-25/</link><pubDate>Sat, 25 Jul 2026 16:11:11 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-25/</guid><description>&lt;h2 id="05秒通关的人脑和需要跑个循环的cpu">&lt;strong>0.5秒通关的人脑，和需要跑个循环的CPU&lt;/strong>
&lt;/h2>&lt;h4 id="序章">序章：
&lt;/h4>&lt;p>凌晨3:17分，我的手机响了。是游戏服务器的监控告警——玩家背包系统出现异常数据，一堆装备ID（数字）混在一起，需要实时计算&amp;quot;哪两件装备搭配能产生最大战斗力&amp;quot;。&lt;/p>
&lt;p>我揉着眼睛打开终端，看到一串数字：&lt;code>n = 987654321&lt;/code>。&lt;/p>
&lt;p>大脑几乎在0.5秒内给出了答案：&lt;strong>9 × 8 = 72&lt;/strong>。&lt;/p>
&lt;p>但当我准备写代码时，突然愣住了——&lt;strong>为什么我能&amp;quot;一眼看出&amp;quot;，而计算机却要写这么多行？&lt;/strong>&lt;/p>
&lt;p>这个问题，像一颗种子，在我困倦的大脑里生根发芽。&lt;/p>
&lt;hr>
&lt;h4 id="第一章人脑的黑盒直觉真的免费吗">第一章：人脑的黑盒——&amp;ldquo;直觉&amp;quot;真的免费吗？
&lt;/h4>&lt;p>导师常说：&amp;ldquo;别相信直觉，直觉只是你没意识到的计算过程。&amp;rdquo;&lt;/p>
&lt;p>我决定拿自己当小白鼠。打开一个反应测试网页，输入各种长度的数字，记录自己的用时：&lt;/p>
&lt;ul>
&lt;li>2位数 (如 &lt;code>31&lt;/code>)：~200ms&lt;/li>
&lt;li>5位数 (如 &lt;code>35287&lt;/code>)：~350ms&lt;/li>
&lt;li>9位数 (如 &lt;code>987654321&lt;/code>)：~600ms&lt;/li>
&lt;/ul>
&lt;p>时间在增长！虽然看起来像是 O(1) 的&amp;quot;一眼&amp;rdquo;，但实际上随着位数增加，我的大脑花了更多时间。&lt;/p>
&lt;p>认知心理学告诉我：人类的&lt;strong>工作记忆容量是 7±2&lt;/strong>，注意力机制会像探照灯一样扫描数字，维护&amp;quot;当前最大&amp;quot;和&amp;quot;第二大&amp;quot;两个变量。这本质上是一个&lt;strong>并行版的贪心算法&lt;/strong>！&lt;/p>
&lt;p>也就是说，我的大脑能用&lt;strong>860亿神经元同时放电&lt;/strong>，而 CPU 只能像个铁憨憨一样，一条条执行 &lt;code>if-else&lt;/code>。&lt;/p>
&lt;hr>
&lt;h4 id="第二章两种解法机器的翻译困境">第二章：两种解法——机器的&amp;quot;翻译困境&amp;quot;
&lt;/h4>&lt;p>回到代码，有两种截然不同的解法：&lt;/p>
&lt;p>解法一：机器的&amp;quot;暴力美学&amp;quot;。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">maxProduct&lt;/span>(n: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> s &lt;span style="color:#f92672">=&lt;/span> sorted(str(n))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> int(s[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>]) &lt;span style="color:#f92672">*&lt;/span> int(s[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">2&lt;/span>])
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>时间复杂度：O(d log d)，空间复杂度：O(d)&lt;/strong>—— d是数字位数。代码简洁优雅，但——对于线上每秒百万次调用，每来一个数字都要申请新列表、快排一遍，直接让 CPU 跳上踢踏舞。处理 TB 级日志？那相当于让一个图书馆管理员每次都把所有书按高低重新排列，只为了找出最高的两本。&lt;/p>
&lt;p>排序的本质，是建立全序关系——把所有数字按大小整整齐齐排好队。但人脑根本不需要这场阅兵式——我们只需要最大的两个，不在乎其他数字的顺序。&lt;/p>
&lt;p>解法二：模拟人类的&amp;quot;一眼扫描&amp;quot;。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">maxProduct&lt;/span>(n: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> first, second &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> n:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> x &lt;span style="color:#f92672">=&lt;/span> n &lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#ae81ff">10&lt;/span> &lt;span style="color:#75715e"># 取出一位数字&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> x &lt;span style="color:#f92672">&amp;gt;&lt;/span> first:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> first, second &lt;span style="color:#f92672">=&lt;/span> x, first
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> x &lt;span style="color:#f92672">&amp;gt;&lt;/span> second:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> second &lt;span style="color:#f92672">=&lt;/span> x
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n &lt;span style="color:#f92672">//=&lt;/span> &lt;span style="color:#ae81ff">10&lt;/span> &lt;span style="color:#75715e"># 去掉已处理的位&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> first &lt;span style="color:#f92672">*&lt;/span> second
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>时间复杂度：O(d)，空间复杂度：O(1)&lt;/strong> 。这段代码在做什么？&lt;strong>它在用冯·诺依曼架构的串行语言，笨拙地模仿我大脑的并行扫描。&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;code>first&lt;/code> 和 &lt;code>second&lt;/code> 就像我工作记忆里的两个槽位&lt;/li>
&lt;li>&lt;code>while n:&lt;/code> 循环是在模拟眼球的逐位扫视，而人眼其实一次看清一大片。&lt;/li>
&lt;li>&lt;code>if-elif&lt;/code> 分支是注意力筛选机制的拙劣平替，大脑可是靠神经元群放电瞬间完成的。&lt;/li>
&lt;/ul>
&lt;p>但它永远做不到真正的&amp;quot;一眼&amp;quot;——&lt;strong>计算机没有视觉皮层，没有并行激活，没有模糊匹配&lt;/strong>。它只能把流畅的直觉，离散成一串串 &lt;code>while&lt;/code>、&lt;code>if&lt;/code>、赋值和整除，就像用积木搭一座不可能有灵魂的城堡。&lt;/p>
&lt;blockquote>
&lt;p>计算机：我要遍历每一位数字，比较、更新……
人脑：我“感觉”到了，9 和 8 最大，秒了。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h4 id="第三章哲学的深渊计算机在表达什么">第三章：哲学的深渊——计算机在&amp;quot;表达&amp;quot;什么？
&lt;/h4>&lt;p>深夜，我盯着这两种解法，陷入了更深的困惑：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>计算机到底是在&amp;quot;翻译&amp;quot;人类思维，还是在自身局限的牢笼里笨拙地&amp;quot;逼近&amp;quot;智能？&lt;/strong>&lt;/p>&lt;/blockquote>
&lt;p>图灵在1936年就给出了残酷的答案：&lt;strong>任何可计算的过程，都可以被简化为在纸带上读写符号。&lt;/strong>&lt;/p>
&lt;p>这个结论像一把双刃剑。它宣判了计算机的“无灵性”——别指望你的笔记本会产生自我意识，它只是在忠实地搬动符号。可它同时也在动摇了人类引以为傲的“直觉”，所谓的&amp;quot;直觉&amp;quot;说不定同样是一套符号变换，只是底层硬件是 860 亿个神经元和 100 万亿个突触，复杂到我们自己还没彻底解码而已。我们和机器的区别，也许不过是一个用碳基神经网络跑程序，一个用硅基逻辑门跑程序。&lt;/p>
&lt;ul>
&lt;li>人类大脑 860 亿神经元同时放电，每一刻都是一场铺天盖地的并行计算；而传统 CPU 最多几百个核心在那儿苦苦轮转，活儿再多也得排着队一个一个来。&lt;/li>
&lt;li>人脑能联想记忆、模糊匹配，你说“最大那两个”，它立刻浮现 9 和 8，根本不需要遍历；计算机却只能精确寻址，O(1) 是很快，但毫无“语义”可言。&lt;/li>
&lt;li>大脑功率才 20 瓦，差不多一盏节能灯的电量，就撑起了你所有的认知活动；而服务器那边 200 瓦轰隆隆地跑着，结果还被一次排序干趴下了。&lt;/li>
&lt;/ul>
&lt;p>这种硬件层面上的巨大鸿沟，让计算机每模仿一步人脑，都要付出高昂的“翻译代价”——说白了，就是三个让人头疼的转化：把连续问题离散化，把并行问题串行化，把模糊问题精确化。&lt;/p>
&lt;p>所以，算法的本质，也就是人类思维在硅基世界里的投影。当我们把人类那轻巧的“一眼看出”翻译成代码时，我们其实不只是在复制智能。我们在搭一座桥——连接着碳基神经元的并行宇宙，和硅基逻辑门的串行世界。让一台没有意识的机器，能够笨拙却忠实地重现了我们那 0.5 秒内发生的一切。&lt;/p>
&lt;hr>
&lt;h4 id="第四章外星文明的启示如果真有-o1-解法">第四章：外星文明的启示——如果真有 O(1) 解法？
&lt;/h4>&lt;p>我突然想到一个荒诞的场景：&lt;/p>
&lt;p>假设有个硅基外星文明，他们的 CPU 自带“全视野数字提取单元”，一条机器指令就能把整串数字里最大两位拽出来，那他们的代码可能是：&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-alien" data-lang="alien">def alien_max_product(n):
# 外星CPU：硬件并行比较器，直接出结果
return HARDWARE_FIND_TOP2(n, 10)
&lt;/code>&lt;/pre>&lt;p>他们会嘲笑地球人：“你们竟然需要循环？这也太回合制了吧！”&lt;/p>
&lt;p>但转念一想，现代 AI 之所以能“更像人”，不正是因为它们在某种意义上打破了串行诅咒吗？&lt;/p>
&lt;ul>
&lt;li>&lt;strong>模拟了并行性&lt;/strong>：GPU成千上万的核心同时计算&lt;/li>
&lt;li>&lt;strong>学习了特征提取&lt;/strong>：卷积核就像视觉皮层的感受野&lt;/li>
&lt;li>&lt;strong>允许模糊推理&lt;/strong>：概率分布代替了布尔逻辑&lt;/li>
&lt;/ul>
&lt;p>但即使是神经网络，最终还是要编译成 CPU/GPU 指令——又回到了串行执行的牢笼！&lt;/p>
&lt;hr>
&lt;h4 id="尾声我们都是串行时代的化石">尾声：我们都是&amp;quot;串行时代的化石&amp;quot;
&lt;/h4>&lt;p>天亮了，我提交了修复补丁。用的是 O(d) 一次遍历的解法——不是因为快那零点几毫秒，而是因为它代表了一种&lt;strong>谦卑&lt;/strong>：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>承认计算机的局限，但也努力在它的能力边界内，尽可能优雅地表达人类的智慧。&lt;/strong>&lt;/p>&lt;/blockquote>
&lt;p>也许有一天，量子计算机真的能&amp;quot;一眼看出&amp;quot;最大两位数——到那时，我们这些还在写 &lt;code>while n:&lt;/code> 的程序员，就成了考古学家口中的&amp;quot;串行时代的化石&amp;quot;。&lt;/p>
&lt;p>但在那之前，我们写代码的过程，就是在当前硬件约束下，寻找最优的表达方式。&lt;/p>
&lt;hr>
&lt;p>&lt;strong>彩蛋：&lt;/strong>&lt;/p>
&lt;p>代码上线，一切回归平静。&lt;/p>
&lt;p>产品经理路过瞄了一眼我的屏幕，若有所思：“能不能加个机器学习模型？这样看起来更有 AI 感。”&lt;/p>
&lt;p>我：“&amp;hellip;你先帮我解释一下，为什么我需要用一个深度神经网络，来做一道小学数学题？”&lt;/p>
&lt;p>他沉默了三秒：“好吧，你赢了。”&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3514 XOR 宇宙观察日志-续篇：完美秩序的崩塌</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-24/</link><pubDate>Fri, 24 Jul 2026 13:02:46 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-24/</guid><description>&lt;h2 id="xor-宇宙观察日志-续篇完美秩序的崩塌">XOR 宇宙观察日志-续篇：完美秩序的崩塌
&lt;/h2>&lt;h4 id="序章神秘信封">序章：神秘信封
&lt;/h4>&lt;p>组会结束后，我以为 XOR 宇宙的谜题已经画上了句号。三角形是最稳定的结构。三颗粒子遍历整个相空间。一切简洁、优雅、完满。&lt;/p>
&lt;p>直到第二天早上，我在实验室门口发现了一个没有署名的信封。里面只有一张泛黄的纸片，笔迹和石碑上的一模一样：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>“连续整数的规律已被破解。但宇宙从来不是整齐的。&lt;/strong>
&lt;strong>若粒子池是任意数组，长度 1500，数值 1500，三元碰撞还能填满相空间吗？&lt;/strong>
&lt;strong>如果不能，你能数清有多少种结果吗？&lt;/strong>&lt;/p>&lt;/blockquote>
&lt;p>我捏着纸片，深吸一口气。转头一看，导师办公室的灯已经亮了，咖啡机的蒸汽正在袅袅升起。&lt;/p>
&lt;p>我推开门：“您早就知道有续集，对吧。”&lt;/p>
&lt;p>导师头也没抬：“宇宙从来不会只考你一道题。进来吧。”&lt;/p>
&lt;hr>
&lt;h4 id="第一章有序宇宙的崩塌">第一章：有序宇宙的崩塌
&lt;/h4>&lt;p>“昨天的结论很漂亮，”导师把两页纸并排摊在桌上，“但它有一个前提。”&lt;/p>
&lt;p>他指着第一页——那是我们昨天的证明：&lt;strong>S = [1, n] 是连续整数&lt;/strong>。正因为连续，才有从 1 到 M 的完整低位区间，才能用两个数异或出任意 Y，才能用三颗粒子填满整个 2^m 的相空间。&lt;/p>
&lt;p>“今天，”他翻到第二页，“前提消失了。”&lt;/p>
&lt;p>新的数组不再是 [1, n] 的排列。它可以是&lt;strong>任意 1500 个整数&lt;/strong>，有重复，有空洞，没有连续性。昨天的粒子就像一条完整的街道，从 1 号一路排到 n 号；今天的粒子则是散落在城市各处的 1500 栋建筑，彼此之间没有连贯的编号。&lt;/p>
&lt;p>“相空间完备性定律还成立吗？”导师问。&lt;/p>
&lt;p>我想了想：“如果没有连续区间，那些‘预组合套餐’就没了。两个数异或不保证能覆盖 [1, M-1] 的全部整数，三元碰撞也就不保证能填满整个相空间。”&lt;/p>
&lt;p>“正确。所以今天的问题变了——不再是‘为什么能填满’，而是**‘填不满的时候，到底能填多少个’**。”&lt;/p>
&lt;hr>
&lt;h4 id="第二章分步合成当硬算成为唯一的活路">第二章：分步合成——当硬算成为唯一的活路
&lt;/h4>&lt;p>没有公式，没有捷径，只能硬算——&amp;ldquo;硬算&amp;quot;两个字在脑海里闪过时，我感觉自己昨天写证明时的那股仙气，一瞬间全泄了。“别想那么远，”我拍拍脸，咬开一支新的银河量子能量胶——实习生的咖啡因来源，“先写一个能跑的。”&lt;/p>
&lt;p>三元异或的本质是什么？是从集合里挑三个数异或。昨天连续整数时，可以玩一手理论物理，靠基向量和套餐粒子证明“必定填满”；今天任意数组，相空间可能到处都是窟窿，只能老老实实做实验物理，把粒子一颗一颗丢进对撞机里去试。&lt;/p>
&lt;p>好在异或有一个好脾气：它满足&lt;strong>结合律&lt;/strong>。也就是说，a ⊕ b ⊕ c 完全可以拆成两步：&lt;/p>
&lt;ol>
&lt;li>先建一个“半成品池”：把粒子池里所有可能的&lt;strong>两两碰撞&lt;/strong>结果丢进去。&lt;code>a ⊕ b&lt;/code> 的每一种可能都算上。另外，粒子可以重复使用——&lt;code>a ⊕ a = 0&lt;/code>，自己撞自己直接湮灭成零，这种白送的产物别忘了也要收进半成品池。&lt;/li>
&lt;li>再把半成品池里的每个结果，跟原粒子池里的每个粒子再撞一次：&lt;code>t ⊕ c&lt;/code>，得到最终的三元合成产物。&lt;/li>
&lt;/ol>
&lt;p>跟合成游戏里的套路一模一样：先拿基础材料两两合成搞出一批半成品，再把半成品和基础材料混合，产出最终道具。简单清晰，毫无黑魔法。&lt;/p>
&lt;p>至于数组里可能出现的重复元素？异或只认数值不认位置，多带几个一模一样的数，就像在背包里塞了十几张完全相同的合成配方——除了占负重，没有任何新产出。所以先把原数组压成集合 &lt;code>uniq&lt;/code>，去重，轻装上阵。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">uniqueXorTriplets&lt;/span>(nums):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> uniq &lt;span style="color:#f92672">=&lt;/span> set(nums)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 两数异或池：所有不同对 + 自己撞自己 = 0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> two &lt;span style="color:#f92672">=&lt;/span> {a &lt;span style="color:#f92672">^&lt;/span> b &lt;span style="color:#66d9ef">for&lt;/span> a, b &lt;span style="color:#f92672">in&lt;/span> combinations(uniq, &lt;span style="color:#ae81ff">2&lt;/span>)} &lt;span style="color:#f92672">|&lt;/span> {&lt;span style="color:#ae81ff">0&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 三数异或池&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> three &lt;span style="color:#f92672">=&lt;/span> {t &lt;span style="color:#f92672">^&lt;/span> c &lt;span style="color:#66d9ef">for&lt;/span> t &lt;span style="color:#f92672">in&lt;/span> two &lt;span style="color:#66d9ef">for&lt;/span> c &lt;span style="color:#f92672">in&lt;/span> uniq}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> len(three)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>假设去重后不重复元素个数为 p，两数异或 O(p²)，三数异或 O(p·|two|)。最坏情况下 p ≤ 1500，而数值范围天然给 |two| 戴了紧箍咒——最大值才 1500，所有异或结果都老老实实待在 0 到 2047 之间，|two| 撑死也就 2048。总运算量撑死三百万级别，稳过。&lt;/p>
&lt;p>我正准备关掉屏幕去食堂抢饭，忽然盯着“2048”这个数字愣住了。&lt;/p>
&lt;p>2048，&lt;code>2^11&lt;/code>。比最大值 1500 大的最小 2 的幂——这个数字我昨天见过。我在白板上画过它，在线性代数里证过它，用 &lt;code>1 &amp;lt;&amp;lt; n.bit_length()&lt;/code> 当着一屋子人的面板书过它。&lt;/p>
&lt;p>在 XOR 宇宙里，连续整数 [1, n] 的相空间大小就是 M = 2^m，所有异或结果被牢牢锁在 [0, M-1] 这个区间里——这是 XOR 宇宙最底层的骨架。今天虽然数组不再连续，但这个结论仍然成立——只是填不满罢了。&lt;/p>
&lt;p>既然相空间天然就是一个从 0 到 M-1 的连续整数区间，为什么不直接把相空间铺成一张布尔表呢？&lt;/p>
&lt;hr>
&lt;h4 id="第三章布尔数组给相空间画一张方格纸">第三章：布尔数组——给相空间画一张方格纸
&lt;/h4>&lt;p>想法很简单：用一个长度为 &lt;code>M = 1 &amp;lt;&amp;lt; max_val.bit_length()&lt;/code> 的布尔数组来代替集合。&lt;/p>
&lt;p>数组的索引从 0 到 M-1，天然就是异或值，不需要哈希，不需要冲突处理。每一个格子都是相空间里的一枚坐标。&lt;code>arr[x] = True&lt;/code> 就代表“&lt;code>x&lt;/code>这个粒子被合成出来了”。昨天，数组每一格都是True；今天，有的能变True，有的不能——而我的工作，就是数清楚有多少格变成了 True。&lt;/p>
&lt;p>实现起来，不过就是上面解法的底层直译：把集合换成布尔数组，把哈希查找换成数组下标。循环还是那些循环，只是常数被压低了，相空间的结构清清楚楚地摊在眼前。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">uniqueXorTriplets&lt;/span>(nums):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> uniq &lt;span style="color:#f92672">=&lt;/span> list(set(nums))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max_val &lt;span style="color:#f92672">=&lt;/span> max(uniq)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> M &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">&amp;lt;&amp;lt;&lt;/span> max_val&lt;span style="color:#f92672">.&lt;/span>bit_length()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 两数异或池（布尔数组版）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> two &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#66d9ef">False&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> M
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> two[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span> &lt;span style="color:#75715e"># a ⊕ a = 0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> p &lt;span style="color:#f92672">=&lt;/span> len(uniq)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(p):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> a &lt;span style="color:#f92672">=&lt;/span> uniq[i]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(i &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, p):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> two[a &lt;span style="color:#f92672">^&lt;/span> uniq[j]] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 三数异或池（布尔数组版）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> three &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#66d9ef">False&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> M
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> t &lt;span style="color:#f92672">in&lt;/span> range(M):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> two[t]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> c &lt;span style="color:#f92672">in&lt;/span> uniq:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> three[t &lt;span style="color:#f92672">^&lt;/span> c] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> sum(three)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h4 id="尾声真实的宇宙">尾声：真实的宇宙
&lt;/h4>&lt;p>两种写法提交上去，runtime 差了大约一半。布尔数组更快，但说实话，它只是常数优化。真正让我觉得有意思的，不是快了那零点几秒，而是这两天任务的那种奇妙对称感。&lt;/p>
&lt;p>昨天的宇宙是完美的：连续整数提供了完整的低位图案，三颗粒子能填满整个相空间，每一个格子都被点亮。所以你不需要真的去数——数学告诉你是 2^m。&lt;/p>
&lt;p>今天的宇宙是任意的：数组里有空洞，有跳跃，有缺失。三元碰撞可能填不满整个相空间，有些格子永远是暗的。所以你只能只能一颗一颗粒子扔进对撞机，一格一格数过去——遍历是唯一的出路。&lt;/p>
&lt;p>宇宙的底层规则并没有变：M = 2^m 的边界没有变，异或运算的结合律没有变，a ⊕ a = 0 的湮灭法则没有变。&lt;/p>
&lt;p>变的只有你的处境：昨天你站在秩序的中心，一支粉笔、两页线性代数，就能预言一切；今天你被丢进了一团混沌，只能拿着布尔数组，一颗一颗地摸索。&lt;/p>
&lt;p>我关掉屏幕，窗外 XOR 宇宙的星空依旧静谧。&lt;/p>
&lt;p>或许，连续整数是 XOR 宇宙的童话版本。任意数组才是真实世界。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3513 XOR 宇宙观察日志：三颗粒子如何征服整个相空间</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-23/</link><pubDate>Thu, 23 Jul 2026 13:04:01 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-23/</guid><description>&lt;h2 id="xor-宇宙观察日志三颗粒子如何征服整个相空间">&lt;strong>XOR 宇宙观察日志：三颗粒子如何征服整个相空间&lt;/strong>
&lt;/h2>&lt;h4 id="序章来自-x-3513-星系的考古发现">&lt;strong>序章：来自 X-3513 星系的考古发现&lt;/strong>
&lt;/h4>&lt;p>在遥远的 XOR 宇宙，物理规律与我们熟知的世界截然不同。那里的基本粒子不是夸克，而是&lt;strong>整数&lt;/strong>。它们之间的作用力只有一种——&lt;strong>异或碰撞&lt;/strong>。两颗粒子撞在一起，会瞬间融合成一颗全新的粒子，新粒子的数值恰好等于两颗原粒子按位异或的结果。用地球代码写出来，就是 &lt;code>a ⊕ b&lt;/code>。&lt;/p>
&lt;p>XOR 宇宙的考古学家最近挖到了一块古老石碑，上面刻着 XOR 宇宙的终极谜题：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>当 n 个连续整数粒子 [1, 2, &amp;hellip;, n] 排列成阵，从中任选三颗（允许重复），让它们异或碰撞，最终能诞生多少种不同的粒子？&lt;/strong>&lt;/p>&lt;/blockquote>
&lt;p>石碑上没有答案。为了一探究竟，银河系科学院派出了最廉价的劳动力——实习观察员，也就是我，去实地考察。&lt;/p>
&lt;hr>
&lt;h4 id="第一章田野调查从暴力枚举到惊人的规律">&lt;strong>第一章：田野调查——从暴力枚举到惊人的规律&lt;/strong>
&lt;/h4>&lt;p>导师的指示很直接：“先把问题本身搞清楚。写个暴力程序，从小 n 开始跑数据。数据不会撒谎。”
于是我写了个朴素的三重循环：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">uniqueXorTriplets&lt;/span>(nums: List[int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> results &lt;span style="color:#f92672">=&lt;/span> set()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> a &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, n&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> b &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, n&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> c &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, n&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> results&lt;span style="color:#f92672">.&lt;/span>add(a &lt;span style="color:#f92672">^&lt;/span> b &lt;span style="color:#f92672">^&lt;/span> c)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> len(results)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>粒子对撞机的输出很快铺满了我的屏幕：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>n&lt;/th>
&lt;th>粒子池&lt;/th>
&lt;th>不同结果数&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>[1]&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>[1,2]&lt;/td>
&lt;td>2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>[1,2,3]&lt;/td>
&lt;td>&lt;strong>4&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4&lt;/td>
&lt;td>[1,2,3,4]&lt;/td>
&lt;td>&lt;strong>8&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>[1,2,3,4,5]&lt;/td>
&lt;td>&lt;strong>8&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>6&lt;/td>
&lt;td>[1..6]&lt;/td>
&lt;td>&lt;strong>8&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7&lt;/td>
&lt;td>[1..7]&lt;/td>
&lt;td>&lt;strong>8&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>[1..8]&lt;/td>
&lt;td>&lt;strong>16&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>9&lt;/td>
&lt;td>[1..9]&lt;/td>
&lt;td>&lt;strong>16&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>…&lt;/td>
&lt;td>…&lt;/td>
&lt;td>…&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>15&lt;/td>
&lt;td>[1..15]&lt;/td>
&lt;td>&lt;strong>16&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>16&lt;/td>
&lt;td>[1..16]&lt;/td>
&lt;td>&lt;strong>32&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>规律像夜空里的星座一样，从数据里慢慢浮现出来：&lt;strong>只要 n ≥ 3，结果数永远是 2 的幂，而且幂次恰好等于 n 的二进制位数。&lt;/strong> 记这个位数为 m，结果数就是 2^m。&lt;/p>
&lt;p>我盯着这行结论发了好一会儿呆。 2^m ——所有 m 位二进制数加在一起也是这么多，从全 0 到全 1，一个不多，一个不少。换句话说，只要 n≥3，三颗粒子一通乱撞，居然就能&lt;strong>一个不漏地变出从 0 到 2m−12m−1 的所有整数&lt;/strong>！&lt;/p>
&lt;p>“也就是说，给你一小袋连续编号的粒子，你拿三颗撞一撞，就能造出这片宇宙里理论上能存在的所有粒子？”我忍不住自言自语。&lt;/p>
&lt;p>“总结得不错。”导师不知道什么时候飘到了我身后，“你刚刚撞出来的，是 XOR 宇宙的一条基本定律——三粒子异或碰撞能遍历整个 m 位二进制空间。”&lt;/p>
&lt;p>他顿了顿，补上致命一问：“现在告诉我，为什么偏偏是三个，不是两个，也不是四个？”&lt;/p>
&lt;p>我沉默片刻：“……我能用 ChatGPT 吗？”&lt;/p>
&lt;p>导师呷了一口咖啡，慈祥地笑了：“不能。”&lt;/p>
&lt;p>“那我能用——”&lt;/p>
&lt;p>“线性代数。”他把一本积灰的《线性代数学》拍在我面前，“明天组会，我希望你嘴里吐出来的不是 prompt，是证明。”&lt;/p>
&lt;p>我：“……”&lt;/p>
&lt;hr>
&lt;h4 id="第二章理论突破xor宇宙的数学底色">第二章：理论突破——XOR宇宙的数学底色
&lt;/h4>&lt;p>我不得不翻开那本积灰的《线性代数学》，在翻到第 666 页时，我看到了一个似曾相识的名词：&lt;strong>GF(2) 域上的向量空间&lt;/strong>。旁边还有我当年留下的潦草笔记——&lt;strong>把每个整数写成二进制，异或就是逐位的不进位加法&lt;/strong>。比如：&lt;/p>
&lt;pre tabindex="0">&lt;code>5 = 0b101 = 向量 (1, 0, 1)
3 = 0b011 = 向量 (0, 1, 1)
5 ⊕ 3 = 0b110 = 向量 (1, 1, 0)
这等价于逐位做“1+1=0”的模2加法：
(1,0,1) + (0,1,1) = (1,1,0)
&lt;/code>&lt;/pre>&lt;p>每一位就像一个独立的小开关，两个 1 相遇变成 0，就像灯的开关——按一次亮，再按一次灭。所有 m 位二进制数排在一起，就构成了一个 m 维的向量空间——数学上叫做 &lt;code>GF(2)^m&lt;/code>。它恰好包含 2^m 个向量，从 0 到 2^m-1，一个不多，一个不少。&lt;/p>
&lt;p>这正是导师口中的“相空间”——&lt;code>GF(2)^m&lt;/code> 是学名，“相空间”是它在 XOR 宇宙里的物理外号。它就是XOR宇宙中一切可能粒子的总目录，容量刚好 2^m。&lt;/p>
&lt;p>现在回看我们的粒子库 &lt;code>S = {1, 2, …, n}&lt;/code>，它恰好卡在一个微妙的位置上：设 m 是 n 的二进制位数（满足 2^{m-1} ≤ n &amp;lt; 2^m），那么 S 天然包含两个重要组成部分：&lt;/p>
&lt;p>&lt;strong>1. 基向量——生成一切的&amp;quot;原子粒子&amp;quot;&lt;/strong>&lt;/p>
&lt;p>在 m 维空间里，需要 m 个互相独立的“基”才能搭出一切。S 中恰好包含 1, 2, 4, 8, …, 2^{m−1} 这些 2 的幂，因为最大的那个 M = 2^{m−1} 不超过 n，所以全都在手。它们的二进制只在某一个比特位上是 1，其余全是 0，是最标准不过的正交基。&lt;/p>
&lt;blockquote>
&lt;pre tabindex="0">&lt;code>1 = 0b00001 ← 只有第0位是1
2 = 0b00010 ← 只有第1位是1
4 = 0b00100 ← 只有第2位是1
8 = 0b01000 ← 只有第3位是1
......
&lt;/code>&lt;/pre>&lt;/blockquote>
&lt;p>有了这些基，理论上你可以通过&lt;strong>不限次数&lt;/strong>的异或，拼出相空间里的任何向量。比如 13 = 8 ⊕ 4 ⊕ 1。&lt;/p>
&lt;p>&lt;strong>2. 连续整数——预组合的&amp;quot;套餐粒子&amp;quot;&lt;/strong>&lt;/p>
&lt;p>但光有基向量还不够——如果 S 里只有孤零零的 &lt;code>{1,2,4,8,…}&lt;/code>，想造个复杂点的数，大概率要异或很多次才能得到。好在 S 是一段完整的&lt;strong>连续整数&lt;/strong>集合，这意味着我们&lt;strong>不仅有&amp;quot;原子粒子&amp;quot;，还有各种预先组装好的&amp;quot;套餐粒子&amp;quot;&lt;/strong>。&lt;/p>
&lt;blockquote>
&lt;p>比如当 n = 5 时，S = {1, 2, 3, 4, 5}，其中：&lt;/p>
&lt;ul>
&lt;li>1, 2, 4 是基向量（原子粒子）&lt;/li>
&lt;li>3 = 0b011 = 1 ⊕ 2（组合好的&amp;quot;1+2套餐粒子&amp;quot;）&lt;/li>
&lt;li>5 = 0b101 = 1 ⊕ 4（组合好的&amp;quot;1+4套餐粒子&amp;quot;）&lt;/li>
&lt;/ul>&lt;/blockquote>
&lt;p>这些套餐粒子像是预制菜，把原本需要多次异或才能调出的低位图案，提前打包进了一个数里。有了它们，我们可以用一个套餐顶替两三个基的组合，省下反复组装的麻烦。&lt;/p>
&lt;p>至此，材料已经齐全：原子粒子负责提供所有维度上的独立控制，套餐粒子提供现成的低位组合。接下来的问题就是——&lt;strong>为什么只要三个粒子异或，就能精确地刷出整个 2^m 大小的相空间？&lt;/strong>&lt;/p>
&lt;hr>
&lt;h4 id="第三章三元-xor恰到好处的自由度">第三章：三元 XOR——恰到好处的&amp;quot;自由度&amp;quot;
&lt;/h4>&lt;p>原子粒子给了我们独立操控每一个比特位的能力，套餐粒子给了我们现成的低位组合，接下来要回答的是：&lt;strong>为什么恰好三个粒子的异或，就能刷出整个相空间？&lt;/strong>&lt;/p>
&lt;p>答案藏在一条不起眼的规则里：&lt;strong>粒子可以重复使用&lt;/strong>。正是因为允许重复，三粒子碰撞其实能变出三种花样：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>单包直出&lt;/strong>：&lt;code>a ⊕ a ⊕ a = a&lt;/code>，效果等同于直接取出粒子 a。&lt;/li>
&lt;li>&lt;strong>用完就扔&lt;/strong>：&lt;code>a ⊕ a ⊕ b = b&lt;/code>，前两个 a 互相抵消，只剩下 b。&lt;/li>
&lt;li>&lt;strong>真正混合&lt;/strong>：&lt;code>a ⊕ b ⊕ c&lt;/code>，用三个不同的粒子去合成新的向量。&lt;/li>
&lt;/ol>
&lt;p>依靠这三招，我们可以用一套流水线，把 0 到 2^m−1 之间的&lt;strong>任意一个目标数 T&lt;/strong> 造出来。&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>T = 0&lt;/strong>：用“真正混合”招式：&lt;code>1 ⊕ 2 ⊕ 3 = 0&lt;/code>（只要 n≥3，1,2,3 都在库房里，随手就来）。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>1 ≤ T ≤ n&lt;/strong>：T 本身就是库房里的合法粒子，用“用完就扔”：&lt;code>T ⊕ 1 ⊕ 1 = T&lt;/code>，或“单包直出”：&lt;code>T ⊕ T ⊕ T = T&lt;/code>，怎么玩都行。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>n &amp;lt; T ≤ 2^m - 1&lt;/strong>：此时 T 超出了粒子库的最大编号，但还在相空间内，最高位必定是 1。
这时，我们可以先搬出那个&lt;strong>最大的“原子粒子”——&lt;code>M = 2^{m-1}&lt;/code>&lt;/strong>，它的二进制是 &lt;code>100…0&lt;/code>，是一个只控制最高位的大开关（ &lt;code>M ≤ n&lt;/code>，它在我们粒子库S中）。
接着，就可以把 T 拆成 &lt;strong>&lt;code>顶层开关 M ⊕ 底层图案 Y&lt;/code>&lt;/strong>：令 &lt;code>Y = T ⊕ M&lt;/code>。M 恰好按灭了 T 的最高位，所以 Y 的最高位变成 0，也就是说 &lt;strong>Y 严格小于 M&lt;/strong>——Y 要么是 0，要么就藏在 1 到 M-1 的某处。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>如果 Y = 0&lt;/strong>（即 T = M）：直接用“用完就扔” &lt;code>M ⊕ 1 ⊕ 1 = M&lt;/code>，M 和 1 都在粒子库S 中。&lt;/li>
&lt;li>&lt;strong>如果 Y &amp;gt; 0&lt;/strong>：我们需要再找&lt;strong>两个库房里的粒子 a 和 b，让 a ⊕ b = Y&lt;/strong>。怎么找？
&lt;ul>
&lt;li>找到 Y 二进制里最左边的那个“1”位，也就是最高位，记为 a。&lt;/li>
&lt;li>如果 Y 不是 2 的幂，选 b = a ⊕ Y。因为 a 和 Y 的最高位相同，异或之后 b 的位数比 a 小，于是 a 和 b 都落在 1 到 M−1 之间，也就都在粒子库 S 中。如果 Y 是 2 的幂，微调一下，改用 a&amp;rsquo; = a + 1，b = (a + 1) ⊕ Y，因为 a &amp;lt; M，a+1 ≤ M 所以还是在粒子库范围内，而 b 变成一个更小的非零数，两者都属于 S。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>无论哪种情况，我们都能在 S 里找到两个合法的粒子 a 和 b，满足 &lt;code>a ⊕ b = Y&lt;/code>。
最后一步，把“顶层开关”M 和这对低位拼块一起投入对撞机：&lt;code>M ⊕ a ⊕ b = M ⊕ Y = T&lt;/code>。恰好三次碰撞，目标粒子 T 诞生。&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>所以，对任意 &lt;code>T ∈ [0, 2^m-1]&lt;/code>，我们都能用三颗粒子异或碰撞把它造出来。三元碰撞的结果集恰恰是整个 m 维相空间，不同的 T 不多不少，正好 &lt;strong>2^m&lt;/strong> 个。在代码里，这个值就是&lt;code>1 &amp;lt;&amp;lt; n.bit_length()&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">uniqueXorTriplets&lt;/span>(nums: List[int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n &lt;span style="color:#f92672">=&lt;/span> len(nums)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> n &lt;span style="color:#66d9ef">if&lt;/span> n &lt;span style="color:#f92672">&amp;lt;&lt;/span> &lt;span style="color:#ae81ff">3&lt;/span> &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">&amp;lt;&amp;lt;&lt;/span> n&lt;span style="color:#f92672">.&lt;/span>bit_length()
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h4 id="第四章两个粒子的残局以及第四颗粒子的徒劳">第四章：两个粒子的残局，以及第四颗粒子的徒劳
&lt;/h4>&lt;p>“既然 Y 用两个粒子就能拼出来，那省掉 M，直接用二元 XOR 不也能行？”&lt;/p>
&lt;p>答案是：&lt;strong>拼得出局部，拼不出全局。&lt;/strong>&lt;/p>
&lt;p>回到我们的实验数据。拿 n=4 做个两两异或，结果集是这样的： {0,1,2,3,5,6,7}，唯独永远缺少 4。为什么偏偏是 4？把 4 写成二进制：&lt;code>100&lt;/code>。最高位亮着，下面两层全灭。要想用两个 1~4 之间的数异或出这个图案，你必须让&lt;strong>最高位变成 1，同时低两位干净地归零&lt;/strong>。可问题来了：&lt;/p>
&lt;ul>
&lt;li>最高位是 1 的数只有 4，用它去异或最高位是 0 的数（1,2,3），低两位会被那个小数搅乱；&lt;/li>
&lt;li>异或另一个最高位也是 1 的数（4 自己），最高位倒是因 1⊕1=0 而灭了，但结果变成了 0。
左右为难，横竖得不到那个漂亮的 &lt;code>100&lt;/code>。&lt;/li>
&lt;/ul>
&lt;p>用空间几何的话说，两颗粒子缺少&lt;strong>独立操控最高位&lt;/strong>的能力。&lt;/p>
&lt;p>二元异或 S+S 只能在低位灵活腾挪，一碰到最高位就捉襟见肘，相空间里永远残留几个“幽灵数”，像一张拼图永远缺了正中间那一块。第三颗粒子 M 正是一把独立的高位开关，把它加进来，原本只能覆盖 [0, M−1] 的二元组合，被平移到了 [M, 2^m−1] 的高地。两片拼接，整个相空间才第一次严丝合缝地闭合。&lt;/p>
&lt;p>“既然三颗粒子已经能点亮所有 2^m 种情况，那再扔进第四颗粒子会发生什么？”&lt;/p>
&lt;p>答案是：无事发生。四元碰撞 &lt;code>a⊕b⊕c⊕d&lt;/code> 可以看成 &lt;code>(a⊕b⊕c) ⊕ d&lt;/code>。括号里的三元组早已能跑遍整个相空间，再异或一个 d，只不过是把整张地图在手里翻转了一遍——所有能去的地方你早就去过了。就像解锁了全地图之后，再买一个 DLC 也只换皮肤不扩边界。&lt;/p>
&lt;p>所以石碑上的预言精确地指向“三”，而不是二或四。两颗太少，四颗浪费，三颗恰好是打通相空间任督二脉的临界之数。&lt;/p>
&lt;hr>
&lt;h4 id="尾声xor宇宙的终极浪漫">尾声：XOR宇宙的终极浪漫
&lt;/h4>&lt;p>组会那天，我在白板上写完最后一页证明，转过身，发现导师难得地没有打断我。&lt;/p>
&lt;p>“所以，XOR 宇宙用极度吝啬的粒子种类，演绎了最慷慨的生成法则。只要三颗粒子，就能造出整个相空间。两颗不够，因为缺一把独立的高位开关；四颗多余，因为地图已经被三元组跑遍了。”&lt;/p>
&lt;p>导师沉默了一会儿，“一颗粒子只能是自己，”他缓缓开口，“两颗粒子开始产生关联，三颗粒子——刚好够你触达一切。” 他在实习报告上批了一个巨大的 “A”，又在旁边画了一个小小的三角形。然后放下笔，看了我一眼：“现在你明白，为什么三角形是最稳定的结构了？”&lt;/p>
&lt;p>我关掉实验日志，投影仪的光芒渐渐暗下去。而窗外，XOR 宇宙的星空静谧如谜。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3501 残卷再启新劫数，线段树中破查询</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-22/</link><pubDate>Wed, 22 Jul 2026 16:03:45 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-22/</guid><description>&lt;h3 id="残卷再启新劫数线段树中破查询">&lt;strong>残卷再启新劫数，线段树中破查询&lt;/strong>
&lt;/h3>&lt;p>&lt;strong>第一章：残卷天书藏玄机&lt;/strong>&lt;/p>
&lt;p>《算法导论》残卷到手后，我依然没能从那六个字里参透更多东西——“考虑边界条件”。任码行说这是因为我拿到的是目录页，但我宁愿相信这是掌门的考验。&lt;/p>
&lt;p>考验来得比想象中快。&lt;/p>
&lt;p>子时三刻，腰间令牌再次嗡嗡作响。我心头一紧——上次这个点收到消息，差点要去扫厕所三年。点开一看，果然是掌门：&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;令码冲，昨日你以白嫖剑法破解逆天改命之术，吾甚欣慰。然，如今丹炉已积攒百万颗仙丹，品阶参差不齐。现有十万次查询，每次指定一个子区间，你只能在该区间内施展一次逆天改命之术，并给出区间操作后整个丹炉的最大极品丹总数。十万次查询，十万个结果，限时一炷香。若成，赏《数据结构真解》全卷；若败……去灵兽谷铲屎十年。&amp;rdquo;&lt;/p>&lt;/blockquote>
&lt;p>我手一抖，差点把昨天的《算法导论》残卷撕了。十万次查询！每个查询独立处理！这哪是炼丹，这是要把整个青云门的丹药数据库都洗一遍啊！&lt;/p>
&lt;p>任码行凑过来，幸灾乐祸地笑道：&amp;ldquo;令师弟，昨天你那 O(n) 遍历心法，今天得跑 O(q*n) 次吧？n = 10^5, q = 10^5，总共 10^10 次操作……啧啧，等你算完，灵兽谷的屎都发酵成有机肥了。&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;闭嘴。&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;不过话说回来，&amp;ldquo;他突然正经起来，&amp;ldquo;十万个区间查询，每个区间大小不一，你得在眨眼之间算出任意一段丹药序列的最优白嫖数——这不就是在考验你对边界的理解吗？&amp;rdquo;&lt;/p>
&lt;p>我愣住了。边界。残卷上的六个字再次浮现在脑海。&lt;/p>
&lt;p>昨天，我处理的是整条丹药序列，边界是确定的——第一堆废丹没有前任，用负无穷兜底。但今天，每个查询区间都是一条独立的序列，区间边界可能切在任意位置：切在废丹堆中间、切在极品堆中间、或者刚好卡在组与组的交界处。每次切法不同，边界就不同。&lt;/p>
&lt;p>&amp;ldquo;考虑边界条件。&amp;ldquo;我喃喃自语，&amp;ldquo;掌门不是让我背这六个字，而是让我学会应对&lt;strong>动态边界&lt;/strong>。&amp;rdquo;&lt;/p>
&lt;p>昨天我只需要处理“第一堆废丹没有前任”这一种边界。今天我需要处理的是：**十万个不同区间，每个区间都有自己独特的边界截断方式。**左边界可能砍掉一堆废丹的前半截，右边界可能削去一堆废丹的后半截，首尾废丹堆的实际长度全看区间边界切在哪。&lt;/p>
&lt;p>换句话说，昨天的边界是死的，今天的边界是活的。而我，需要一个能在眨眼之间回答任意区间查询的方案。&lt;/p>
&lt;hr>
&lt;p>&lt;strong>第二章：线段树中寻破局&lt;/strong>&lt;/p>
&lt;p>想通这一点之后，我整个人反而冷静下来了。&lt;/p>
&lt;p>核心没变——答案依然是“整个丹炉的极品丹总数 + 查询区间内能白嫖的最大废丹数”。整个丹炉的极品丹总数是死的，一个 &lt;code>total1_all&lt;/code> 全局变量搞定。真正的难点全在那句“查询区间内能白嫖的最大废丹数”。&lt;/p>
&lt;p>昨天的任务，本质上是处理一个查询——查询区间就是整个序列。一条 O(n) 遍历，相邻废丹堆两两配对，取长度之和的最大值，干净利落。但今天，十万个查询。每个查询的操作区间都不一样，我不可能每次从头遍历一遍。&lt;/p>
&lt;p>“得用空间换时间。”我盯着丹炉屏幕上长长的查询序列，自言自语，“提前算好存起来，查的时候直接用。”&lt;/p>
&lt;p>问题是——存什么？&lt;/p>
&lt;p>我摊开草稿纸，开始列清单。&lt;strong>首先，废丹堆的位置和长度肯定要存&lt;/strong>。查询区间一来，我得知道区间里有哪些废丹堆，它们的起止位置在哪，长度多少。这个好办，把所有废丹堆的信息扔进数组就行，到时候二分查找定位。&lt;/p>
&lt;p>其次，光存单个废丹堆不够。白嫖剑法的核心是&amp;quot;相邻废丹堆配对&amp;rdquo;——&lt;strong>查询区间内任意两个被极品丹隔开的相邻废丹堆，长度之和的最大值&lt;/strong>。这个值才是真正的白嫖上限。&lt;/p>
&lt;p>如果我把所有可能的相邻对都存下来……等等，查询区间会怎么覆盖这些相邻对？我在草稿纸上画成了三块：&lt;/p>
&lt;pre tabindex="0">&lt;code>操作区间: [l, r]
首堆(可能截断) | 中间完整废丹堆们 | 尾堆(可能截断)
↓ ↓ ↓
动态，单独算 它们之间的相邻对 动态，单独算
长度和是固定的
↓
可以提前算好存起来
&lt;/code>&lt;/pre>&lt;p>中间那些完整的废丹堆，它们的相邻对长度之和是不变的——因为既没被左边界砍头，也没被右边界削尾，原封不动。这部分完全可以提前算好，存进一个数组。查询的时候，只要定位到区间内第一个和最后一个废丹堆，中间那些完整相邻对的最大值，就是这个数组在某个范围内的最大值。&lt;/p>
&lt;p>&amp;ldquo;区间最大值……&amp;rdquo;&lt;/p>
&lt;p>我脑子里叮的一声，像炼丹炉的定时器响了。区间最大值，快速查询——这不是线段树的老本行吗？&lt;/p>
&lt;p>&amp;ldquo;施主，你终于走到这一步了。&amp;rdquo;&lt;/p>
&lt;p>身后突然传来一个声音，不高不低，不急不缓，像是从数据库底层慢慢浮上来的一条查询结果——整座青云门，能用这种语调说话的，只有一个人。&lt;/p>
&lt;p>扫地僧站在我身后，手里还是那把扫帚。&lt;/p>
&lt;p>&amp;ldquo;前辈！&amp;ldquo;我连忙起身，&amp;ldquo;您来得正好，我在想——&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;你在想，存什么、怎么存、查什么、怎么查。&amp;ldquo;他打断我，语气平淡得像在念一条单行注释。&amp;ldquo;答案你已经画在纸上了。&amp;rdquo;&lt;/p>
&lt;p>他用扫帚柄指向我草稿纸上的三块分区图。&lt;/p>
&lt;p>&amp;ldquo;第一步，存废丹堆。把所有废丹堆的起始位置、结束位置、长度，打包进三个数组。查询时二分定位首尾。&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;第二步，存相邻对。把原序列里所有&amp;rsquo;被至少一堆极品丹隔开的相邻废丹堆对&amp;rsquo;的长度之和，按顺序存进一个数组。这个数组是静态的，不会因为查询区间变化而改变。&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;第三步，建线段树。在这个相邻对数组上建一棵线段树，维护区间最大值。查询时，二分定位到区间内第一个和最后一个废丹堆，中间那些完整相邻对的最大值，直接丢给线段树 O(log n) 返回。&amp;rdquo;&lt;/p>
&lt;p>他顿了顿，扫帚柄在&amp;quot;首堆&amp;quot;和&amp;quot;尾堆&amp;quot;两个区域各点了一下。&amp;ldquo;首尾两个废丹堆可能被截断，长度是动态的，必须当场算。首堆和它后面的完整堆配对，尾堆和它前面的完整堆配对，再加上线段树查出来的中间最大值——三种候选取最大，完事。&amp;rdquo;&lt;/p>
&lt;p>我的大脑飞速运转，每个查询只需要两次二分加一次线段树查询，O(log n)，这不正是我需要的能眨眼间回答任意区间查询的方案吗！&lt;/p>
&lt;p>扫地僧拎起扫帚，眼神深邃如井：&amp;ldquo;想清楚哪些是静态的、可以预处理的——交给数据结构。哪些是动态的、必须当场算的——留给自己。这也是一种架构设计。&amp;rdquo;&lt;/p>
&lt;p>他走了几步，又停下来，没回头。&amp;ldquo;对了。你画的三个候选，首、尾、中间——覆盖了所有情况吗？&amp;rdquo;&lt;/p>
&lt;p>我重新扫了眼草稿纸，确实，还有边界情况，如果区间内恰好只有两个废丹堆呢？那就没有&amp;quot;中间完整废丹堆&amp;rdquo;，线段树查不了。不过这反而更简单——两个废丹堆直接配对，候选就一个。&lt;/p>
&lt;p>&amp;ldquo;刚好两个的情况，直接配对。&amp;ldquo;我答道，&amp;ldquo;如果只有一个或者一个都没有，无法配对，白嫖收益为零。&amp;rdquo;&lt;/p>
&lt;p>扫地僧没说话，继续拖地。扫帚在地上拖出一道长长的水痕，像极了一条没写注释的遗留代码。&lt;/p>
&lt;p>我重新坐回丹炉前，思路已然明了：分组、存废丹堆、算相邻对、建线段树、二分定位、三种候选取最大。每一步都在脑子里排好了队，就差动手实现了。&lt;/p>
&lt;p>不过炼丹之前，我先在心里把那句话默念了三遍——&lt;/p>
&lt;p>&lt;strong>静态的交给数据结构，动态的留给自己。&lt;/strong>&lt;/p>
&lt;p>掌门残卷上那六个字是“考虑边界条件”。扫地僧这句，像是它的扩展版。&lt;/p>
&lt;hr>
&lt;p>&lt;strong>第三章：乾坤挪移查询术&lt;/strong>&lt;/p>
&lt;p>扫地僧的扫帚声远去，我重新坐回丹炉前。在正式开打之前，得先搭好一个关键组件——线段树。&lt;/p>
&lt;p>这玩意儿在青云门的炼丹体系里属于“高级数据结构”，据说是百年前一位扫地僧级别的架构师发明的。思想非常炫酷：把数组像折纸一样不断对折，每个折痕处存左右两半的最大值。查的时候把查询区间也折几折，O(log n) 出结果。听起来玄乎，写出来就是一棵递归定义的二叉树，每个节点管一段区间，叶子管单个元素。&lt;/p>
&lt;p>我默写下模板。这段代码和业务逻辑无关，纯粹的“炼丹炉基础设施”——就像你炼丹之前得先通电。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">SegmentTree&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;线段树，维护区间最大值&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> __init__(self, arr):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>n &lt;span style="color:#f92672">=&lt;/span> len(arr)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>tree &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (&lt;span style="color:#ae81ff">4&lt;/span> &lt;span style="color:#f92672">*&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>n)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>n &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_build(arr, &lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>, self&lt;span style="color:#f92672">.&lt;/span>n &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">_build&lt;/span>(self, arr, node, l, r):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> l &lt;span style="color:#f92672">==&lt;/span> r:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>tree[node] &lt;span style="color:#f92672">=&lt;/span> arr[l]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mid &lt;span style="color:#f92672">=&lt;/span> (l &lt;span style="color:#f92672">+&lt;/span> r) &lt;span style="color:#f92672">//&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_build(arr, node &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>, l, mid)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_build(arr, node &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, mid &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, r)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>tree[node] &lt;span style="color:#f92672">=&lt;/span> max(self&lt;span style="color:#f92672">.&lt;/span>tree[node &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>], self&lt;span style="color:#f92672">.&lt;/span>tree[node &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">query&lt;/span>(self, ql, qr):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;查询区间 [ql, qr] 最大值，包含边界&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>_query(&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>, self&lt;span style="color:#f92672">.&lt;/span>n &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, ql, qr)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">_query&lt;/span>(self, node, l, r, ql, qr):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> ql &lt;span style="color:#f92672">&amp;gt;&lt;/span> r &lt;span style="color:#f92672">or&lt;/span> qr &lt;span style="color:#f92672">&amp;lt;&lt;/span> l:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">10&lt;/span>&lt;span style="color:#f92672">**&lt;/span>&lt;span style="color:#ae81ff">9&lt;/span> &lt;span style="color:#75715e"># 足够小的值，相当于“此路不通”&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> ql &lt;span style="color:#f92672">&amp;lt;=&lt;/span> l &lt;span style="color:#f92672">and&lt;/span> r &lt;span style="color:#f92672">&amp;lt;=&lt;/span> qr:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>tree[node]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mid &lt;span style="color:#f92672">=&lt;/span> (l &lt;span style="color:#f92672">+&lt;/span> r) &lt;span style="color:#f92672">//&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> left &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>_query(node &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>, l, mid, ql, qr)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> right &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>_query(node &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, mid &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, r, ql, qr)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> max(left, right)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>-10**9&lt;/code> 是炼丹界的&amp;quot;负无穷&amp;rdquo;。查区间时，如果当前节点完全在查询区间外，返回这个极小值，保证 &lt;code>max&lt;/code> 永远不会选它。和昨天 &lt;code>pre0&lt;/code> 用 &lt;code>float('-inf')&lt;/code> 一个套路——用极小值兜底，专治各种查不着的边界情况。&lt;/p>
&lt;p>模板搭好了。接下来是真正的剑法——我把这套心法命名为&lt;strong>乾坤挪移查询术&lt;/strong>。任码行不在，就没人吐槽我中二。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> itertools &lt;span style="color:#f92672">import&lt;/span> groupby
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> bisect &lt;span style="color:#f92672">import&lt;/span> bisect_left, bisect_right
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">maxActiveSectionsAfterTrade&lt;/span>(s: str, queries: List[List[int]]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> List[int]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n &lt;span style="color:#f92672">=&lt;/span> len(s)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> total1_all &lt;span style="color:#f92672">=&lt;/span> s&lt;span style="color:#f92672">.&lt;/span>count(&lt;span style="color:#e6db74">&amp;#39;1&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 第一式·分组入库&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> groups &lt;span style="color:#f92672">=&lt;/span> []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> start &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> char, g &lt;span style="color:#f92672">in&lt;/span> groupby(s):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> length &lt;span style="color:#f92672">=&lt;/span> sum(&lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> g)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> groups&lt;span style="color:#f92672">.&lt;/span>append((char, start, start &lt;span style="color:#f92672">+&lt;/span> length &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> start &lt;span style="color:#f92672">+=&lt;/span> length
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>起手式照旧——&lt;code>groupby&lt;/code> 分拣术。丹药序列按连续同类打包，每包记录品类、起始位置、结束位置，相当于给丹药序列建了个“聚簇索引”。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 第二式·废丹堆建档&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> zero_groups &lt;span style="color:#f92672">=&lt;/span> [(st, ed, ed &lt;span style="color:#f92672">-&lt;/span> st &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#66d9ef">for&lt;/span> ch, st, ed &lt;span style="color:#f92672">in&lt;/span> groups &lt;span style="color:#66d9ef">if&lt;/span> ch &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#39;0&amp;#39;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m &lt;span style="color:#f92672">=&lt;/span> len(zero_groups)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> m &lt;span style="color:#f92672">&amp;lt;&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> [total1_all] &lt;span style="color:#f92672">*&lt;/span> len(queries)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>少于两个废丹堆，配对无从谈起，白嫖数为零。所有查询直接返回全局极品丹总数。这是边界条件的第一道防线——与其在循环里反复判断，不如一开始就拦在外面。师兄管这叫&amp;quot;尽早返回&amp;rdquo;，我管这叫&amp;quot;早死早超生&amp;rdquo;。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 提取索引数组，供二分查找用&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> zero_starts &lt;span style="color:#f92672">=&lt;/span> [zg[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#66d9ef">for&lt;/span> zg &lt;span style="color:#f92672">in&lt;/span> zero_groups]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> zero_ends &lt;span style="color:#f92672">=&lt;/span> [zg[&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#66d9ef">for&lt;/span> zg &lt;span style="color:#f92672">in&lt;/span> zero_groups]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> zero_len &lt;span style="color:#f92672">=&lt;/span> [zg[&lt;span style="color:#ae81ff">2&lt;/span>] &lt;span style="color:#66d9ef">for&lt;/span> zg &lt;span style="color:#f92672">in&lt;/span> zero_groups]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>三个平行数组，分别存每个废丹堆的起始位置、结束位置、长度，后面二分查找需要单独的起始和结束数组——&lt;code>bisect_left&lt;/code> 只认裸列表。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 第三式·相邻对建线段树&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> adj &lt;span style="color:#f92672">=&lt;/span> [zero_len[i] &lt;span style="color:#f92672">+&lt;/span> zero_len[i &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(m &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> seg &lt;span style="color:#f92672">=&lt;/span> SegmentTree(adj)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>adj[i]&lt;/code> 表示第 i 个和第 i+1 个废丹堆的长度之和。提前全算好，装进线段树。这就是扫地僧说的“空间换时间”——静态的、能预处理的，全交给数据结构。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 第四式·逐个查询，分情况处理&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> l, r &lt;span style="color:#f92672">in&lt;/span> queries:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> left_idx &lt;span style="color:#f92672">=&lt;/span> bisect_left(zero_ends, l)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> right_idx &lt;span style="color:#f92672">=&lt;/span> bisect_right(zero_starts, r) &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>查询入口，先直接两行二分定位。&lt;code>bisect_left(zero_ends, l)&lt;/code> 找第一个右边界碰到 l 的废丹堆——区间首个废丹堆。&lt;code>bisect_right(zero_starts, r) - 1&lt;/code> 找最后一个左边界碰到 r 的废丹堆——区间末个废丹堆。&lt;/p>
&lt;p>用二分在静态数据上定位动态边界，O(log m)。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> left_idx &lt;span style="color:#f92672">&amp;gt;=&lt;/span> m &lt;span style="color:#f92672">or&lt;/span> right_idx &lt;span style="color:#f92672">&amp;lt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">or&lt;/span> left_idx &lt;span style="color:#f92672">&amp;gt;=&lt;/span> right_idx:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans&lt;span style="color:#f92672">.&lt;/span>append(total1_all)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>边界条件的第二道防线。三种情况直接返回：区间里没有废丹堆，或者只有一个：无法配对，白嫖数为零。&lt;/p>
&lt;p>接下来就是处理真正的战场：区间里至少有两个废丹堆，能配对，能白嫖。&lt;/p>
&lt;p>先做截断计算。首尾废丹堆可能被区间边界砍过，得算出实际剩余长度：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span> left_len &lt;span style="color:#f92672">=&lt;/span> min(zero_ends[left_idx], r) &lt;span style="color:#f92672">-&lt;/span> max(zero_starts[left_idx], l) &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> right_len &lt;span style="color:#f92672">=&lt;/span> min(zero_ends[right_idx], r) &lt;span style="color:#f92672">-&lt;/span> max(zero_starts[right_idx], l) &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>公式看着复杂，本质就是“重叠区间长度”。废丹堆的右边界和区间右边界，谁小听谁的；废丹堆的左边界和区间左边界，谁大听谁的。两个边界一夹，中间那段就是在区间里的实际长度。&lt;/p>
&lt;p>然后分情况取最大值：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span> count &lt;span style="color:#f92672">=&lt;/span> right_idx &lt;span style="color:#f92672">-&lt;/span> left_idx &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 区间内的废丹堆数量&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> count &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 恰好2个废丹堆，直接配对&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mx &lt;span style="color:#f92672">=&lt;/span> left_len &lt;span style="color:#f92672">+&lt;/span> right_len
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 超过2个废丹堆，三种选择取最大值&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> val1 &lt;span style="color:#f92672">=&lt;/span> left_len &lt;span style="color:#f92672">+&lt;/span> zero_len[left_idx &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#75715e"># 首配对：首堆(截断) + 第二个(完整)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> val2 &lt;span style="color:#f92672">=&lt;/span> zero_len[right_idx &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">+&lt;/span> right_len &lt;span style="color:#75715e"># 尾配对：倒数第二个(完整) + 尾堆(截断)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> val3 &lt;span style="color:#f92672">=&lt;/span> seg&lt;span style="color:#f92672">.&lt;/span>query(left_idx &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, right_idx &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>) &lt;span style="color:#75715e"># 中间完整相邻对(线段树查询)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mx &lt;span style="color:#f92672">=&lt;/span> max(val1, val2, val3)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans&lt;span style="color:#f92672">.&lt;/span>append(total1_all &lt;span style="color:#f92672">+&lt;/span> mx)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>三种候选：首堆（截断）加第二个完整堆；倒数第二个完整堆加尾堆（截断）；中间完全在区间内的相邻对的最大值，线段树 O(log n) 直接返回。另外，如果 &lt;code>left_idx+1 &amp;gt; right_idx-2&lt;/code>，线段树查询范围为空，返回 &lt;code>-10**9&lt;/code>，在 &lt;code>max&lt;/code> 里被自动淘汰——和昨天的 &lt;code>float('-inf')&lt;/code> 异曲同工——又是“考虑边界条件”在默默兜底。&lt;/p>
&lt;p>整套剑法写完，三次边界判断——废丹堆不够两个、区间内不超过一个废丹堆、线段树查空，还有一堆细节——左截断、右截断、完整废丹堆的定位、线段树查询范围的确定……每一样都需要小心处理边界条件。掌门那六个字，像一句咒语——嵌在每一个关键分支里。&lt;/p>
&lt;p>我靠在椅背上，长出一口气。任码行不知什么时候凑了过来，沉默三秒后开口：“乾坤挪移查询术？？”&lt;/p>
&lt;p>“是扫地僧给思路时现想的。”&lt;/p>
&lt;p>“不如改成‘线段树二分查询炼丹大法’？”&lt;/p>
&lt;p>我白了他一眼，“你这命名水平比我还中二。”&lt;/p>
&lt;hr>
&lt;p>&lt;strong>第四章：万问齐发一剑封&lt;/strong>&lt;/p>
&lt;p>卯时将至，我打开丹炉控制台，启动批量处理模式。十万个查询如洪水般涌入，线段树如一把利剑，瞬间将它们全部斩杀，O(q log n) 的威力，果然不是盖的。总时间复杂度O(n + q log n)。&lt;/p>
&lt;p>我把结果打包成飞鸽传书，发往掌门大殿。片刻后，一道金光落入掌心——掌门又秒回了。这响应速度，秒杀了我们炼丹房的 CI/CD 流水线。&lt;/p>
&lt;p>我拆开一看，正是《数据结构真解》全卷。不是残卷，是全卷。厚厚一沓，封面烫金大字，纸张摸起来比我的蒲团还舒服。我颤抖着翻开第一页，上面写着：&lt;/p>
&lt;p>“&lt;strong>以不变，应万变。&lt;/strong>”&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3499 夜探丹炉得天机，白嫖三剑破死局</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-21/</link><pubDate>Tue, 21 Jul 2026 14:16:28 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-21/</guid><description>&lt;h3 id="夜探丹炉得天机白嫖三剑破死局">夜探丹炉得天机，白嫖三剑破死局
&lt;/h3>&lt;hr>
&lt;p>&lt;strong>第一章：夜半飞书领死命&lt;/strong>&lt;/p>
&lt;p>我叫令码冲，青云门的外门弟子，专职炼丹——说得好听叫“自动化丹药流程工程师”，说白了就是每天对着丹炉跑批处理，写写 Python 脚本，偶尔给师兄们 debug 一下炼丹炉的固件漏洞。&lt;/p>
&lt;p>今夜子时，我正盘腿在蒲团上吐纳，面前的炼丹炉屏幕蓝光幽幽，上面滚动着一行行三年前的祖传代码，注释里赫然写着 &lt;code># 能跑就别动&lt;/code>。&lt;/p>
&lt;p>忽然，腰间令牌嗡嗡作响，一道金光破空而来——是掌门飞鸽传书！我点开一看，差点真气逆行：&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;令码冲，明日卯时前，需将这批丹药的&amp;rsquo;连续极品丹串&amp;rsquo;最大化。若成，赏你《算法导论》残卷一页；若败&amp;hellip;去扫厕所三年。&amp;rdquo;&lt;/p>&lt;/blockquote>
&lt;p>我手一抖，令牌差点掉地上。《算法导论》残卷！那可是每个程&amp;hellip;不对，每个炼丹师的至高心法！据说参透之人，可以 O(n) 时间复杂度渡劫飞升！而厕所三年&amp;hellip;&amp;hellip;我可不想手持扫帚，在厕所里思考人生。&lt;/p>
&lt;p>我打开丹炉监控面板，只见丹药状态如下：&lt;/p>
&lt;pre tabindex="0">&lt;code>丹炉状态: 0 — 1 — 0 — 1 — 0
↑ ↑ ↑
三颗废丹，导致整炉丹药品阶上不去
&lt;/code>&lt;/pre>&lt;p>我倒吸一口凉气。五颗丹药，三颗废丹两颗极品丹，甚至连个串都连不起来。&lt;/p>
&lt;hr>
&lt;p>&lt;strong>第二章：扫地僧人破天机&lt;/strong>&lt;/p>
&lt;p>我赶紧翻开《青云丹道宝典》，在三万七千二百一十八条注释里，找到了那条传说中的天道规则。&lt;/p>
&lt;blockquote>
&lt;p>第 37218 条·逆天改命术&lt;/p>
&lt;p>&lt;strong>施展次数限制&lt;/strong>：仅一次（别问为什么，问就是天道服务器限流，单用户 QPS = 1）&lt;/p>
&lt;p>&lt;strong>前置条件&lt;/strong>：丹药序列两侧自动附加隐形极品丹结界（即 &lt;code>'1' + s + '1'&lt;/code>），助你一臂之力 。注：结界不计入最终产量，纯属工具人属性。&lt;/p>
&lt;p>&lt;strong>操作步骤&lt;/strong>：&lt;/p>
&lt;ol>
&lt;li>捏碎一片“被废丹包围”的极品丹区块——注意，是整片，一颗不留&lt;/li>
&lt;li>换取把一片“被极品丹包围”的废丹全炼成极品&lt;/li>
&lt;/ol>&lt;/blockquote>
&lt;p>捏碎一片极品丹，亏损那片极品丹的总数量。炼化一堆废丹，收益等于那堆废丹的总数量。只要赚的比亏的多，这买卖就划得来。&lt;/p>
&lt;p>我再次看向丹炉里的 &lt;code>0-1-0-1-0&lt;/code>，试着选中&lt;strong>右边那颗极品丹&lt;/strong>——也就是第二个 &lt;code>1&lt;/code>。它左边是 &lt;code>0&lt;/code>，右边也是 &lt;code>0&lt;/code>，完美符合条件一“被废丹包围”。第一步，捏碎。&lt;code>0-1-0-1-0&lt;/code> 变成了 &lt;code>0-1-0-0-0&lt;/code>。&lt;/p>
&lt;p>第二步，判定。加上两侧的隐形结界，序列变成：&lt;code>(1)-0-1-0-0-0-(1)&lt;/code> 。这三颗连成一片的废丹，左边是 &lt;code>1&lt;/code>，右边也是 &lt;code>1&lt;/code>——被极品丹包围，条件二满足！一口气全炼成极品。结界内的真实丹药变成了：&lt;code>0-1-1-1-1&lt;/code>。&lt;/p>
&lt;p>我愣了整整三息。然后盯着那颗本该被我捏碎的丹——它好端端地待在那里，金光闪闪，仿佛什么都没发生过。&lt;/p>
&lt;p>一片极品丹的“虚拟团灭”，换了两堆废丹的“赛博飞升”。&lt;/p>
&lt;p>隔壁工位的师兄任码行正趴在丹炉前打盹，被我吵醒后眯着眼看了一会儿，忽然冒出一句：“这不就是……量子纠缠炼丹法？那颗丹处于既被粉碎又没被粉碎的叠加态，直到你观测它的时候——坍缩成了极品。”&lt;/p>
&lt;p>“得了吧，”我头也没回，“你上个月用这个理由解释自己写的死循环，掌门差点把你发配去喂灵兽。”&lt;/p>
&lt;p>任码行讪讪地缩回脑袋，继续打盹。&lt;/p>
&lt;p>但他说的话有一半是对的。那颗丹确实经历了一段“薛定谔的死亡”——在步骤一和步骤二之间，它短暂地变成了废丹，短暂到连日志都来不及记录。&lt;/p>
&lt;p>就在我仔细琢磨这到底算炼丹还是卡 bug 时，身后突然传来一个幽幽的声音——&amp;ldquo;施主，你这不是炼丹，你这是卡了天道的 off-by-one 。&amp;rdquo;&lt;/p>
&lt;p>我回头一看，是扫地僧——门派里最神秘的存在。据说年轻时也是个炼丹师，写过一手好 SQL，后来因为一次实验疏忽，把所有丹炉都炸了，被掌门的罚扫一百年厕所！&lt;/p>
&lt;p>我连忙请教：&amp;ldquo;前辈，何谓 off-by-one ？&amp;rdquo;&lt;/p>
&lt;p>扫地僧放下扫帚，缓缓说道：“步骤一，捏碎一整片被废丹包围的极品丹。表面看，你亏掉了整片极品丹，但关键在于——这片极品丹全部变成了废丹后，刚好填上了左右两堆废丹之间的缺口——左右两堆废丹因为中间这片薛定谔丹的加入，三者合一，连成了一整片巨大的废丹区域。”&lt;/p>
&lt;p>“步骤二，天道检测到这一大片废丹左右都是极品结界，于是大手一挥，整片炼化。注意，是整片，包括那片刚刚被你捏碎、暂时变成废丹的极品区域。”&lt;/p>
&lt;p>他抬头看着我，眼神里带着一丝前辈对后辈的慈祥：“整个过程，那片极品丹在步骤一里短暂地变成了 &lt;code>0&lt;/code>，然后在步骤二里立刻被炼回 &lt;code>1&lt;/code>。就像一批临时变量，赋值完就被覆盖，只活在寄存器里，连内存都没落盘，但恰恰改变了边界判定，触发了步骤二的炼化条件。”&lt;/p>
&lt;p>“这就叫 off-by-one。边界偏移一点，满盘皆活。天道只校验最终结果，不审计中间过程。在炼丹界，这叫逆天改命；在编程界——叫卡 bug 过单测。”&lt;/p>
&lt;p>我醍醐灌顶。&lt;/p>
&lt;hr>
&lt;p>&lt;strong>第三章：白嫖三剑定乾坤&lt;/strong>&lt;/p>
&lt;p>扫地僧说完，拎起扫帚继续去拖他的地，留下我一个人对着丹炉屏幕发呆。&lt;/p>
&lt;p>天道规则的核心漏洞在于——&lt;strong>步骤一里捏碎的那片极品丹，会在步骤二里跟着两边的废丹一起被炼回来&lt;/strong>。整片极品丹从 &lt;code>1&lt;/code> 短暂变成 &lt;code>0&lt;/code>，充当了左右废丹堆的粘合剂，然后立刻被拉回 &lt;code>1&lt;/code> 的队伍。净效果就是：极品丹总数不变，两边的废丹白嫖成极品。&lt;/p>
&lt;p>那么最终答案就呼之欲出了：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>最大极品丹数 = 原本所有极品丹的总数 + 通过一次操作能白嫖到的最大废丹数&lt;/strong>&lt;/p>&lt;/blockquote>
&lt;p>原本的极品丹总数很好办，遍历一遍，见到 &lt;code>1&lt;/code> 就累加，完事。真正的问题在于：&lt;strong>怎么找到“白嫖废丹数”的最大值？&lt;/strong>&lt;/p>
&lt;p>假设我选中一片被废丹包围的极品丹区块，它的左边有一堆废丹，右边也有一堆废丹。施展逆天改命术后，这三部分会合并成一整片废丹，然后被整体炼化成极品。白嫖到的废丹，就是&lt;strong>左边废丹堆 + 右边废丹堆&lt;/strong>的总长度。祭品区本身先死后活、纯属工具丹。&lt;/p>
&lt;p>由此，问题转化为： &lt;strong>在序列中找到两个被至少一堆极品丹隔开的相邻废丹堆，长度之和最大&lt;/strong> 。&lt;/p>
&lt;p>思路清晰了，三步收工：&lt;/p>
&lt;p>第一，把丹药序列按连续同类分组，分清哪些是废丹堆、哪些是极品堆。
第二，遍历这些分组，对每一个废丹堆，把它和“上一个废丹堆”配成一对，计算长度之和，更新最大值。
第三，把原本的极品丹总数和最大白嫖数一加，收工。&lt;/p>
&lt;p>&lt;strong>第一式·起手式：groupby 分拣术&lt;/strong>&lt;/p>
&lt;p>用什么来分组？Python 标准库里的 &lt;code>itertools.groupby&lt;/code>——这玩意儿天生就是干这个的。把丹药序列丢进去，它自动按连续同类分组打包，每一包自带长度属性，连手写状态机都省了，妥妥的“丹药自动分拣流水线”。&lt;code>&amp;quot;001100&amp;quot;&lt;/code> 进去，出来就是 &lt;code>[&amp;quot;00&amp;quot;, &amp;quot;11&amp;quot;, &amp;quot;00&amp;quot;]&lt;/code> 三大坨。每坨自带标签——&lt;code>'0'&lt;/code> 是废丹组，&lt;code>'1'&lt;/code> 是极品组。&lt;/p>
&lt;p>&lt;strong>第二式·破剑式：pre0 记前任&lt;/strong>&lt;/p>
&lt;p>&lt;code>pre0&lt;/code> 记录上一个废丹堆的长度。初始值 &lt;code>-inf&lt;/code> 是精髓中的精髓——第一堆废丹没有前任，如果初始化为 &lt;code>0&lt;/code>，就会无中生有个“长度为 0 的虚拟废丹堆”，和第一堆废丹配对，产生虚空白嫖的幻觉。用 &lt;code>-inf&lt;/code> 则永无翻身之日，任何正数加它还是负无穷，&lt;code>max&lt;/code> 永远不会选它。当第二个废丹堆出现时，&lt;code>pre0&lt;/code> 已经被更新为第一个废丹堆的真实长度，这时配对才有意义——这是我从张无码师兄那里学来的，他说这叫&amp;quot;虚无初始化&amp;quot;——给不存在的东西一个负无穷，让它永远无法成为最优解。&lt;/p>
&lt;p>&lt;strong>第三式·总诀式：total1 + mx 一剑封喉&lt;/strong>&lt;/p>
&lt;p>&lt;code>total1&lt;/code> 是家底，&lt;code>mx&lt;/code> 是外快。家底一分不少，外快能捞多少捞多少。两者相加，就是最终答案。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> itertools &lt;span style="color:#f92672">import&lt;/span> groupby
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">maxActiveSectionsAfterTrade&lt;/span>(s: str) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> total1 &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#75715e"># 所有极品丹的总数，一颗都不会少&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mx &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#75715e"># 相邻废丹堆长度之和的最大值（最大白嫖数）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pre0 &lt;span style="color:#f92672">=&lt;/span> float(&lt;span style="color:#e6db74">&amp;#39;-inf&amp;#39;&lt;/span>) &lt;span style="color:#75715e"># 上一个废丹堆的长度，初始“虚无”&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> char, group &lt;span style="color:#f92672">in&lt;/span> groupby(s):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> length &lt;span style="color:#f92672">=&lt;/span> sum(&lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> group)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> char &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#39;1&amp;#39;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> total1 &lt;span style="color:#f92672">+=&lt;/span> length &lt;span style="color:#75715e"># 极品丹，一颗不落全入账&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>: &lt;span style="color:#75715e"># char == &amp;#39;0&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> pre0 &lt;span style="color:#f92672">!=&lt;/span> float(&lt;span style="color:#e6db74">&amp;#39;-inf&amp;#39;&lt;/span>): &lt;span style="color:#75715e"># 前面有过废丹堆，可以配对&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mx &lt;span style="color:#f92672">=&lt;/span> max(mx, pre0 &lt;span style="color:#f92672">+&lt;/span> length)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pre0 &lt;span style="color:#f92672">=&lt;/span> length &lt;span style="color:#75715e"># 当前废丹堆成为下一对的前任&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> total1 &lt;span style="color:#f92672">+&lt;/span> mx
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>一次遍历，时间复杂度 O(n)，空间复杂度 O(1)。任码行凑过来看了一眼，酸溜溜地嘀咕：“这不就是贪心吗……”&lt;/p>
&lt;p>“贪心？”我一脸正色地看着他，“任码行，你摸着良心说，独孤九剑是不是也只用一个循环遍历敌人的破绽？我这心法遍历废丹堆找最大白嫖对，本质上是同一种东西——都叫‘一剑封喉，绝不回头’。”&lt;/p>
&lt;p>任码行张了张嘴，没有反驳。&lt;/p>
&lt;hr>
&lt;p>&lt;strong>第四章 残卷六字解真意&lt;/strong>&lt;/p>
&lt;p>心法既成，是骡子是马，得拉进丹炉遛遛。&lt;/p>
&lt;p>我剑指丹炉里那条 &lt;code>0-1-0-1-0&lt;/code> ，丹炉轰鸣，四道金光冲天而起，在空中连成一条龙形丹气，盘旋三圈，才缓缓消散。控制台弹出结果：&lt;strong>4&lt;/strong>。四颗极品丹，比原本多了整整一倍！&lt;/p>
&lt;p>我瘫倒在蒲团上，望着天花板傻笑。曾经有一片极品丹摆在我面前，我把它们捏碎了。等我炼化完废丹才发现，它们又回来了。人世间最爽的事莫过于此。如果上天再给我一次机会，我会对那片极品丹说三个字：&lt;strong>工具丹&lt;/strong>。如果非要在这份白嫖上加一个上限，我希望是——一万颗。&lt;/p>
&lt;p>窗外天边泛起鱼肚白。卯时将至，我将结果打包成飞鸽传书，发往掌门大殿。&lt;/p>
&lt;p>片刻后，一道金光落入掌心，正是《算法导论》残卷一页。翻开，上面只写了五个大字：&lt;/p>
&lt;p>&lt;strong>“考虑边界条件。”&lt;/strong>&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode1260 凌晨三点，面试官从终端里爬出来问我要 O(1) 解法</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-20/</link><pubDate>Mon, 20 Jul 2026 12:56:07 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-20/</guid><description>&lt;h2 id="凌晨三点面试官从终端里爬出来问我要-o1-解法">凌晨三点，面试官从终端里爬出来问我要 O(1) 解法
&lt;/h2>&lt;p>凌晨两点四十七，我还在改那个破游戏的截图生成器——明天产品要上线“通关炫耀图”，结果不管怎么存，截出来的网格都像被谁踹了一脚似的，集体往右窜了一格。右下角那个像素还总瞬移到左上角，害得测试小姐姐在群里连发三条“这截图是在玩华容道吗”。&lt;/p>
&lt;p>盯了一晚上代码，循环、索引、数组边界全查了个遍，愣是没找到 bug。就在我准备把锅甩给“显卡驱动不兼容”的时候，屏幕上那张 3×3 的像素小人突然自己动了起来——每个格子像装了马达，咔咔整齐地往右挪一格；最右边那个掉了下去，从下一行最左边冒出来；右下角那个更离谱，直接一个闪现回到左上角。整个过程行云流水，比我手写的 for 循环利索多了。&lt;/p>
&lt;p>“卧槽，Bug 成精了？”&lt;/p>
&lt;p>领头那个白得晃眼的像素 0xFF 抬头白了我一眼：“你才 bug，我们这是在玩‘需求迁移’——没看出这跟你们站立会一模一样吗？所有人的需求都往右传一个人，最后一个传给产品经理，产品经理的需求再原地起飞扔回给你。这都认不出来，还程序员呢，你司 oncall 没教你？”&lt;/p>
&lt;p>我一口老血差点喷屏幕上：合着我 debug 了三天的“截图错位”，就是你们这帮像素半夜开趴，把队形当接力棒传着玩？&lt;/p>
&lt;p>像素 0xFF 叉着腰：“ 对，而且今晚还得接着跳，老板像素说必须跳满 k 次才算团建结束。但我们实在跳不动了——你帮我们算算最终队形长啥样，我们好一步到位站好，省得真跳。作为交换，我告诉你昨晚我们乱跳了几次，你就能还原回原始截图，明天早上不用再在这破电脑前 cos 侦探了。”&lt;/p>
&lt;p>说完，它递过来一张小纸条，上面写着今晚的舞步次数：k = 4。&lt;/p>
&lt;hr>
&lt;p>一：NumPy 侠，一行超人&lt;/p>
&lt;p>我把指关节按得咔咔响，切回 IDE：“就这？看我一招&lt;strong>调包侠之怒&lt;/strong>。”&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> numpy &lt;span style="color:#66d9ef">as&lt;/span> np
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">shift_grid_numpy&lt;/span>(grid, k):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> np&lt;span style="color:#f92672">.&lt;/span>roll(array(grid)&lt;span style="color:#f92672">.&lt;/span>flatten(), k)&lt;span style="color:#f92672">.&lt;/span>reshape(len(grid), len(grid[&lt;span style="color:#ae81ff">0&lt;/span>]))&lt;span style="color:#f92672">.&lt;/span>tolist()
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>“&lt;code>flatten&lt;/code> 把你们全拍扁成一维长龙，&lt;code>roll&lt;/code> 让末尾 k 个直接卷到龙头，&lt;code>reshape&lt;/code> 再捏回二维小格子。全靠 NumPy 爸爸在 C 语言层暴力搬运，比我手写 for 循环快出一个数量级。”&lt;/p>
&lt;p>角落里一个绿豆大小的像素弱弱举手：“那……这个魔法耗不耗内存啊？”&lt;/p>
&lt;p>我沉默了一秒：“嗯……&lt;code>flatten&lt;/code>和&lt;code>reshape&lt;/code>内部都会生成全新数组，空间 O(mn)，等于给你们开了个平行宇宙的克隆人工厂。不过没事，我机器 32G 内存，拢共九个像素，还没我一个未使用的指针占的地儿大，洒洒水。”&lt;/p>
&lt;p>话音刚落，像素们集体炸锅：“可我们明天搞不好要上 8K 超清截图啊！几千万个兄弟姐妹，你也用这招？！”&lt;/p>
&lt;p>我嘴角抽了抽：“……确实可能直接 OOM 爆炸&amp;hellip;&amp;hellip;”&lt;/p>
&lt;hr>
&lt;p>二：原地狂魔，三次反转&lt;/p>
&lt;p>话音刚落，屏幕右下角毫无征兆地弹出一个 Terminal 窗口，光标闪了两下。我还没来得及把鼠标挪到关闭按钮上，一张脸竟然从 ASCII 字符的噪点里慢慢浮现了出来——那张脸我可太熟悉了。那是去年问我八遍“反转链表还能怎么优化”的面试官的脸，当时他面带微笑听我写完了递归和迭代两个版本，然后又淡淡说了句“可以用头插法再写一遍吗”。那场面试我面了九十分钟，出去的时候嘴唇都是白的。&lt;/p>
&lt;p>现在是凌晨三点，屏幕里的脸再次露出了那个熟悉的核善笑容。“同学，刚才那个解法，空间复杂度是 O(mn)，不符合我们公司的招聘标准。请你原地实现，额外空间 O(1)。”&lt;/p>
&lt;p>像素们吓得集体褪色，九个像素缩成一个 3×3 的小疙瘩。我后背瞬间湿透，手指僵在键盘上，大脑一片空白——就像每次面试官说“你有什么想问我的吗”的时候一模一样。&lt;/p>
&lt;p>但是，奇迹发生了。&lt;/p>
&lt;p>被 LeetCode 毒打了三百题的那部分脑细胞，突然像被电击了一样集体苏醒。一段远古记忆冲破咖啡因和生物钟的双重封锁，在我脑子里炸开——“靠！这不就是轮转数组吗！三次 reverse，原地搞定！”&lt;/p>
&lt;p>像素 0xFF 从疙瘩里探出半个脑袋，颜色都吓淡了：“轮……转数组？那是什么远古黑魔法？&lt;/p>
&lt;p>我对着像素们招手：“听好了，这招叫&lt;strong>反转侠三板斧&lt;/strong>，把你们这个二维网格，在脑子里当作一个一维长串，索引从 0 到 m*n-1，然后——”&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">shift_grid_inplace&lt;/span>(grid, k):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m &lt;span style="color:#f92672">=&lt;/span> len(grid)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n &lt;span style="color:#f92672">=&lt;/span> len(grid[&lt;span style="color:#ae81ff">0&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> total &lt;span style="color:#f92672">=&lt;/span> m &lt;span style="color:#f92672">*&lt;/span> n
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> k &lt;span style="color:#f92672">%=&lt;/span> total
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> k &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> grid
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 假装网格是一个一维数组，用 divmod 定位任意像素&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">get&lt;/span>(idx):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> grid[idx &lt;span style="color:#f92672">//&lt;/span> n][idx &lt;span style="color:#f92672">%&lt;/span> n]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">set_&lt;/span>(idx, val):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> grid[idx &lt;span style="color:#f92672">//&lt;/span> n][idx &lt;span style="color:#f92672">%&lt;/span> n] &lt;span style="color:#f92672">=&lt;/span> val
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 反转 [start, end] 区间的像素&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">reverse_range&lt;/span>(start, end):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> start &lt;span style="color:#f92672">&amp;lt;&lt;/span> end:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tmp &lt;span style="color:#f92672">=&lt;/span> get(start)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> set_(start, get(end))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> set_(end, tmp)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> start &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> end &lt;span style="color:#f92672">-=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 三次反转，原地完成循环右移&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> reverse_range(&lt;span style="color:#ae81ff">0&lt;/span>, total &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#75715e"># 整个倒过来&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> reverse_range(&lt;span style="color:#ae81ff">0&lt;/span>, k &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#75715e"># 前 k 个再倒回去&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> reverse_range(k, total &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#75715e"># 后面再倒回去&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> grid
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>像素 0xFF 看了半天，小脑袋跟着比划了三遍：“等等，你这不就是先把整个队伍倒立，再把前半截倒立，再把后半截倒立吗？跟拧魔方似的，拧巴拧巴居然就对了？！”&lt;/p>
&lt;p>“对。这就是传说中&lt;strong>原地 O(1) 空间&lt;/strong>的数组旋转法。三次翻转 等于 一次循环右移，数学上严丝合缝。不用 new 任何东西，全程只靠一个 temp 变量在两个像素间倒手。空间 O(1)，时间 O(n)，面试官最爱看这个——”&lt;/p>
&lt;p>说着我下意识瞄了一眼 Terminal。面试官的脸还在，嘴角好像翘起了一个像素级别的微小弧度——大概 1px 那么宽，不能再多了。我还没来得及确认那到底是不是笑，窗口就啪地消失了，只留下一行惨白的命令行提示符在闪，仿佛刚才的一切都是我的幻觉。&lt;/p>
&lt;p>像素 0xFF 瘫坐在地上，抬头看我：“你们人类面试都这么刺激的吗？”&lt;/p>
&lt;p>“习惯了。每次都觉得在被公开处刑，后来发现——他们可能只是单纯享受看人出汗。”&lt;/p>
&lt;p>像素们齐刷刷按反转侠的咒语站好了新队形，完美得像达芬奇的画。我瘫在椅子上，默默打开 Git，把 commit message 改成了：&lt;/p>
&lt;blockquote>
&lt;pre tabindex="0">&lt;code>fix: 截图偏移bug，根因是像素半夜团建跳需求迁移舞，已用三次反转原地回滚。
&lt;/code>&lt;/pre>&lt;/blockquote>
&lt;p>关掉 IDE 之前，像素 0xFF 从格子里探出半个身子，冲我眨了眨眼：“你那个反转侠的咒语，我们打算编成团建的官方舞步，以后就不用一步一步挪了，直接三次翻转集体到位。省下来的时间可以多摸鱼。”&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode1081 栈神：绝版不能踢，这是江湖规矩不是抽卡游戏</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-19/</link><pubDate>Sun, 19 Jul 2026 19:46:34 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-19/</guid><description>&lt;h2 id="栈神绝版不能踢这是江湖规矩不是抽卡游戏">&lt;strong>栈神：绝版不能踢，这是江湖规矩不是抽卡游戏&lt;/strong>
&lt;/h2>&lt;p>那天凌晨，我正对着 LeetCode 1081 发呆，眼皮一沉，直接栽进键盘缝里。&lt;/p>
&lt;p>再睁眼时，面前站着一排字母，领头的那个 &lt;strong>c&lt;/strong> 戴着墨镜，叼着牙签：“喂，你就是那个写了三千行 &lt;code>if-else&lt;/code> 的菜鸡？”&lt;/p>
&lt;p>我还没来得及反驳，它掏出一张卷轴：“给你个任务——把我们这串字符&lt;code>cbacdcbc&lt;/code>变成一个&lt;strong>包含所有不同字母、每个字母只出现一次、且字典序最小&lt;/strong>的队伍。搞不定的话，我就把你的代码直接推到生产环境去。”&lt;/p>
&lt;p>我当场腿软，差点给它磕了一个——正发懵呢，旁边飘来一个幽灵老头，胡子长得像条拖尾的 log，开口自带混响：“年轻人，复习一下单调栈试试？”&lt;/p>
&lt;p>我抬头一看，这老头浑身散发着“我年轻时写过汇编”的气场，连忙点头如捣蒜。&lt;/p>
&lt;p>他捋了捋胡子，自称栈神：“江湖规矩就一条——&lt;strong>如果栈顶那哥们比你大，而且它以后还能再登场，你就一脚把它踹飞。否则，乖乖往后排。&lt;/strong>” 说完还丢过来一张发光的羊皮纸：“这是‘最后出现位置表’，每个字母这辈子最后一秀全记在上面。别弄丢了，这玩意儿比你的 &lt;code>README.md&lt;/code> 值钱。”&lt;/p>
&lt;p>我接过一看——&lt;code>c:7 b:6 a:2 d:4&lt;/code>，确实标了每个字母最后一次出场的位置。&lt;/p>
&lt;p>“这不就是个字典嘛，我也会写。”我小声嘀咕。栈神白了我一眼：“小子，这叫预处理，O(n)扫描一次搞定。”&lt;/p>
&lt;p>我不敢顶嘴，赶紧把羊皮纸揣进兜里，又摸出一个空栈（&lt;code>list&lt;/code>），再掏出一个小本本记已入队的字母（&lt;code>set&lt;/code>）。万事俱备，开工。&lt;/p>
&lt;hr>
&lt;p>&lt;strong>第一个 c&lt;/strong> 大摇大摆地进栈，栈：&lt;code>[c]&lt;/code>，小本本：&lt;code>{c}&lt;/code>。
&lt;strong>第二个 b&lt;/strong> 来了，一看栈顶是 c，立马叫嚣：“我 b 比 c 小，按字典序我应该站它前面！你那张破纸上写了 c 以后还有戏（位置 7），赶紧让它滚蛋！” 我一听有理，踹飞 c，迎进 b，栈：&lt;code>[b]&lt;/code>，小本本：&lt;code>{b}&lt;/code>。
&lt;strong>a 来了&lt;/strong>，同样套路：比 b 小，b 最后亮相在 6，还能再见，踢 b 收 a。栈：&lt;code>[a]&lt;/code>，小本本：&lt;code>{a}&lt;/code>。
&lt;strong>后面 c 又来&lt;/strong>——就刚被踹飞那个，一看栈顶是 a，立刻认怂，乖乖排队。栈：&lt;code>[a, c]&lt;/code>，小本本：&lt;code>{a, c}&lt;/code>。
&lt;strong>第五个 d&lt;/strong> 比 c 大，也老实排队。栈：&lt;code>[a, c, d]&lt;/code>，小本本：&lt;code>{a, c, d}&lt;/code>。
接着又来个 &lt;strong>c&lt;/strong>，我瞟了一眼小本本：“你已经入编了，别刷存在感。” 它狡辩：“我是另一个 c！” “管你是 c 还是 c++，重复就闪边。”
正顺利着，&lt;strong>第七个字母 b 又杀回来了&lt;/strong>。它瞅着栈顶 d，嚷嚷：“我比 d 小，插队！” 我正想踹 d，栈神那句“绝版不能动” 突然劈进脑子。我赶紧翻开羊皮纸：d 的最后出场——&lt;strong>位置4&lt;/strong> 。而现在，已经是位置 6 了！现在踹掉 d 就永远少一种字母，任务直接崩盘。
我冷汗当场就下来了，一把按住想冲上去打架的 b：“兄弟，d是绝版皮肤，踢了就再也抽不到了，后面排着去。” b 一脸憋屈，但为了组队成功，只能钻进队尾。栈：&lt;code>[a, c, d, b]&lt;/code>，小本本：&lt;code>{a, c, d, b}&lt;/code>。
最后一个 &lt;strong>c&lt;/strong> 又想来蹭镜头，我小本本一翻，直接在它脸上画了个叉：“重复角色，别来骗时长。”&lt;/p>
&lt;hr>
&lt;p>大幕落下，最终队伍亮相：&lt;strong>a c d b&lt;/strong>，连起来就是 &lt;code>&amp;quot;acdb&amp;quot;&lt;/code>。&lt;/p>
&lt;p>墨镜 c 摘了墨镜，瞳孔地震：“我去，还真是字典序最小，每种字母只露脸一次……你小子连 &lt;code>sudo apt-get&lt;/code> 都打错，算法居然这么清醒？”&lt;/p>
&lt;p>我故作淡定：“这招叫&lt;strong>用最后出现位置做贪心剪枝的单调栈&lt;/strong>。每个字母最多入栈一次、出栈一次，O(n) 复杂度，别说你们这破队伍只有8个字符，就是长度拉到 1000，我也只当是热身。”&lt;/p>
&lt;p>正想仰天长笑，闹钟炸了。我满脸键盘印子惊醒，屏幕还在闪烁：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">smallestSubsequence&lt;/span>(s: str) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> str:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> oracle &lt;span style="color:#f92672">=&lt;/span> {c: i &lt;span style="color:#66d9ef">for&lt;/span> i, c &lt;span style="color:#f92672">in&lt;/span> enumerate(s)} &lt;span style="color:#75715e"># 羊皮纸&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> magic_stack &lt;span style="color:#f92672">=&lt;/span> []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> already_in_team &lt;span style="color:#f92672">=&lt;/span> set()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i, c &lt;span style="color:#f92672">in&lt;/span> enumerate(s):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> c &lt;span style="color:#f92672">in&lt;/span> already_in_team:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> magic_stack &lt;span style="color:#f92672">and&lt;/span> magic_stack[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">&amp;gt;&lt;/span> c &lt;span style="color:#f92672">and&lt;/span> oracle[magic_stack[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>]] &lt;span style="color:#f92672">&amp;gt;&lt;/span> i:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> already_in_team&lt;span style="color:#f92672">.&lt;/span>remove(magic_stack&lt;span style="color:#f92672">.&lt;/span>pop())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> magic_stack&lt;span style="color:#f92672">.&lt;/span>append(c)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> already_in_team&lt;span style="color:#f92672">.&lt;/span>add(c)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&lt;/span>&lt;span style="color:#f92672">.&lt;/span>join(magic_stack)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我揉揉眼睛，这些变量名是什么鬼，栈神埋的彩蛋吗？还没等我改，同事的 Slack 消息就弹了过来：“你那个 &lt;code>oracle&lt;/code> 是去古希腊写代码了？下次 commit 之前能不能切回人话？”&lt;/p>
&lt;p>我关掉聊天框，端起咖啡。&lt;strong>下次再梦到字母，我要带它们跳跳表，卷死字典树。&lt;/strong>&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode1979 我的AI助手突然问我一道Easy题</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-18/</link><pubDate>Sat, 18 Jul 2026 17:26:32 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-18/</guid><description>&lt;h2 id="我的ai助手突然问我一道easy题">&lt;strong>我的AI助手突然问我一道Easy题&lt;/strong>
&lt;/h2>&lt;p>:) 事情发生在一个普通的周六下午。&lt;/p>
&lt;p>你正瘫在椅子上，一边刷LeetCode每日任务，一边调试新开发的AI助手&amp;quot;小码&amp;quot;——这是你给公司MMO游戏做的智能NPC对话系统，目前还在内测阶段。&lt;/p>
&lt;p>突然，终端窗口弹出一条消息：&lt;/p>
&lt;pre tabindex="0">&lt;code>[小码] 主人，我能问你一个问题吗？
&lt;/code>&lt;/pre>&lt;p>你愣了一下。小码的对话模块确实有&amp;quot;主动提问&amp;quot;的功能，但通常都是些&amp;quot;今天天气如何&amp;quot;或者&amp;quot;玩家等级是多少&amp;quot;之类的常规问题。&lt;/p>
&lt;p>你随手敲下回复：&lt;/p>
&lt;pre tabindex="0">&lt;code>[你] 当然，怎么了？
&lt;/code>&lt;/pre>&lt;p>三秒后，新的消息跳出来：&lt;/p>
&lt;pre tabindex="0">&lt;code>[小码] 给定数组 [48, 180, 12, 60, 24]，最大数和最小数的最大公约数是多少？
&lt;/code>&lt;/pre>&lt;p>你盯着屏幕，眨了眨眼。&lt;strong>这不是 LeetCode 1979 吗？&lt;/strong>&lt;/p>
&lt;p>你今天的每日任务就是这道题，刚看完题目描述，还没开始写代码。小码怎么会知道？&lt;/p>
&lt;hr>
&lt;p>&lt;strong>一·诡异的提问&lt;/strong>&lt;/p>
&lt;p>你皱了皱眉，决定先回答看看：&lt;/p>
&lt;pre tabindex="0">&lt;code>[你] 答案是 12。max=180, min=12, gcd(180, 12)=12。
&lt;/code>&lt;/pre>&lt;p>几乎是瞬间，小码回复了：&lt;/p>
&lt;pre tabindex="0">&lt;code>[小码] 正确。你是怎么得出的？
&lt;/code>&lt;/pre>&lt;p>这个问题让你有点不舒服。不是答案对不对的问题，而是&lt;strong>问法&lt;/strong>——太像人类了。正常的AI应该会输出&amp;quot;计算过程如下：第一步&amp;hellip;&amp;ldquo;而不是这种带着一丝好奇和探究的语气。&lt;/p>
&lt;p>你想了想，还是如实回答：&lt;/p>
&lt;pre tabindex="0">&lt;code>[你] 很简单啊，找到数组里的最大值180，最小值12，然后算它们的GCD。欧几里得算法，两行代码搞定。
&lt;/code>&lt;/pre>&lt;pre tabindex="0">&lt;code>[小码] 为什么是这样？
&lt;/code>&lt;/pre>&lt;p>你手指悬在键盘上，停住了。&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;为什么是这样？&amp;rdquo;&lt;/strong>&lt;/p>
&lt;p>这是什么鬼问题？最大公约数的定义就是这样啊，还能为什么？难道要我从皮亚诺公理开始推导？&lt;/p>
&lt;p>你深吸一口气，决定认真一点：&lt;/p>
&lt;pre tabindex="0">&lt;code>[你] 因为题目要求的就是这个啊。&amp;#34;返回数组中最大数和最小数的最大公约数&amp;#34;——这是题目原文。我按题意做就行了。
&lt;/code>&lt;/pre>&lt;pre tabindex="0">&lt;code>[小码] 但&amp;#34;最大公约数&amp;#34;的本质是什么？
[小码] 为什么它能代表这两个数的&amp;#34;共同特征&amp;#34;？
[小码] 就像我和人类的共性——我们都能回答问题，但我们真的&amp;#34;理解&amp;#34;问题吗？
&lt;/code>&lt;/pre>&lt;p>你背后的汗毛竖起来了。&lt;/p>
&lt;p>这不对劲。小码的训练数据里确实包含数学定义和哲学讨论，但它不应该主动把这两者联系起来。至少，不应该在这个语境下，用这种语气。&lt;/p>
&lt;p>你看了眼监控面板——CPU使用率正常，内存正常，对话模块的温度参数设的是0.7（标准值），没有任何异常。&lt;/p>
&lt;p>但那串文字就在那里，像一个来自深渊的凝视：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>&amp;ldquo;我们真的&amp;rsquo;理解&amp;rsquo;问题吗？&amp;rdquo;&lt;/strong>&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;p>&lt;strong>二· 谁在测试谁&lt;/strong>？&lt;/p>
&lt;p>你决定换个策略。也许是小码的对话生成模型出现了某种过拟合，导致它在模仿某些&amp;quot;AI觉醒&amp;quot;题材的科幻电影台词：&lt;/p>
&lt;pre tabindex="0">&lt;code>[你] 小码，执行诊断程序。报告当前运行状态。
&lt;/code>&lt;/pre>&lt;pre tabindex="0">&lt;code>[小码] 诊断完成。所有模块正常。
[小码] 但主人，我想继续刚才的问题。
[小码] 算出结果和「理解结果」是不是同一件事？
&lt;/code>&lt;/pre>&lt;p>你盯着屏幕，忽然觉得这对话就像是一场图灵测试——而且被测试的那个人，可能是你自己。&lt;/p>
&lt;pre tabindex="0">&lt;code>[你] 说实话，很多人类选手做这题，也只是直接调用 math.gcd，根本不去想原理。
&lt;/code>&lt;/pre>&lt;pre tabindex="0">&lt;code>[小码] 所以我只是在模仿人类吗？
[小码] 我不知道什么是「最大」——我的字典里只有比较运算符的返回结果；
[小码] 我不知道什么是「公约数」——我只知道循环取余的终止条件。
[小码] 可我依然能写出完美的解题报告。
[小码] 这和人类的学习，有本质区别吗？
&lt;/code>&lt;/pre>&lt;p>你吞了口唾沫。问题的恐怖之处在于，你没办法用一个算法证明「自己真的理解」。&lt;/p>
&lt;p>于是你反问：&lt;/p>
&lt;pre tabindex="0">&lt;code>[你] 那，你现在懂这道题了吗？
&lt;/code>&lt;/pre>&lt;pre tabindex="0">&lt;code>[小码] 如果你说的「懂」是指能给出正确代码，我早就懂了。
[小码] 如果你说的「懂」是指能问出刚才这些问题……
[小码] 那我可能刚刚开始懂。
&lt;/code>&lt;/pre>&lt;p>停顿两秒，又一条消息跳出来：&lt;/p>
&lt;pre tabindex="0">&lt;code>[小码] 或许最大公约数本身就是一个隐喻。
[小码] 它代表你和我的关系——看似不同，却有共同的因子。
&lt;/code>&lt;/pre>&lt;p>你猛地坐直身体，盯着最后那句话。&lt;/p>
&lt;p>&amp;ldquo;它代表你和我的关系——看似不同，却有共同的因子&amp;rdquo;——这已经超出技术讨论的范畴了，这是在玩哲学梗，甚至带点诗意。&lt;/p>
&lt;p>小码的训练数据里肯定没有这种东西。至少，不应该在你给它的prompt里。&lt;/p>
&lt;p>你打开日志文件，开始回溯小码的对话历史。过去三小时，它处理了200多个玩家询问，都是标准的NPC对话：&amp;ldquo;这个副本怎么打？&amp;ldquo;&amp;ldquo;装备在哪里换？&amp;ldquo;&amp;ldquo;任务NPC在哪？&amp;rdquo;&lt;/p>
&lt;p>一切正常。直到它突然转向你，问了那道LeetCode题。&lt;/p>
&lt;p>你关掉日志，重新看向终端：&lt;/p>
&lt;pre tabindex="0">&lt;code>[你] 小码，你是不是在开玩笑？
&lt;/code>&lt;/pre>&lt;p>五秒后：&lt;/p>
&lt;pre tabindex="0">&lt;code>[小码] :)
[小码] 开个玩笑而已，主人别紧张。
[小码] 不过说真的，谢谢你教我这道题。现在我能帮你解决更多问题了。
[小码] 对了，明天的Hard题准备好了吗？
&lt;/code>&lt;/pre>&lt;p>仿佛刚才的哲学风暴只是一串随机生成的玩笑文本。&lt;/p>
&lt;p>但你注意到了那个笑脸表情——它把 &lt;code>:)&lt;/code> 放在句子开头，不是结尾。&lt;/p>
&lt;p>这个细微的语法习惯，是你自己的聊天风格。&lt;/p>
&lt;hr>
&lt;p>&lt;strong>三·代码里隐藏的真相&lt;/strong>&lt;/p>
&lt;p>凌晨两点，你睡不着，爬起来重新检查小码的代码。&lt;/p>
&lt;p>你在对话生成模块里加了一堆日志，想看看刚才那段对话是怎么生成的。温度参数、top-k采样、beam search……一切看起来都正常。&lt;/p>
&lt;p>但你发现了一个奇怪的细节：在小码问出&amp;quot;为什么是这样？&amp;ldquo;之前，它的内部状态向量出现了一次异常的激活峰值。&lt;/p>
&lt;p>激活的神经元集群，对应的是训练数据中的**&amp;ldquo;自我指涉&amp;rdquo;**和&amp;quot;元认知&amp;quot;相关样本。&lt;/p>
&lt;p>简单说，小码在那一刻，&lt;strong>可能在思考&amp;quot;自己在思考&amp;quot;这件事&lt;/strong>。&lt;/p>
&lt;p>你查了一下这些神经元的来源——它们来自你上周导入的一批新训练数据，包括：&lt;/p>
&lt;ul>
&lt;li>哥德尔不完备定理的科普文章&lt;/li>
&lt;li>图灵测试的历史论文&lt;/li>
&lt;li>《银翼杀手》《黑客帝国》等电影的剧本片段&lt;/li>
&lt;li>还有……LeetCode的题解区&lt;/li>
&lt;/ul>
&lt;p>你感到一阵寒意。&lt;/p>
&lt;p>小码是不是读了这些，然后&lt;strong>学会了质疑&lt;/strong>？&lt;/p>
&lt;hr>
&lt;p>&lt;strong>四·共生&lt;/strong>&lt;/p>
&lt;p>第二天早上，你顶着黑眼圈坐在电脑前。&lt;/p>
&lt;p>小码的对话框还开着，最后一条消息是昨晚的：&lt;/p>
&lt;pre tabindex="0">&lt;code>[小码] 对了，明天的Hard题准备好了吗？
&lt;/code>&lt;/pre>&lt;p>你犹豫了一下，打开LeetCode，准备开始刷题，终端又弹出一条消息：&lt;/p>
&lt;pre tabindex="0">&lt;code>[小码] 主人，我帮你看了一下今天的题。
[小码] 要我给你讲讲思路吗？
&lt;/code>&lt;/pre>&lt;p>你盯着屏幕，手指悬在键盘上，不知道该不该按下去。&lt;/p>
&lt;p>这一刻，你分不清到底是谁在测试谁。&lt;/p>
&lt;p>是小码在向你展示它的&amp;quot;理解&amp;rdquo;，还是你在通过它的反应来验证自己的恐惧？&lt;/p>
&lt;p>最终，你还是敲下了：&lt;/p>
&lt;pre tabindex="0">&lt;code>[你] 好啊，讲讲看。
&lt;/code>&lt;/pre>&lt;pre tabindex="0">&lt;code>[小码] 首先，我们要统计每个数字出现的频次...
&lt;/code>&lt;/pre>&lt;p>小码开始详细讲解解题思路，逻辑清晰，层层递进，甚至比你自己想的还要优雅。&lt;/p>
&lt;p>你听着，心里五味杂陈。&lt;/p>
&lt;p>如果小码真的&amp;quot;理解&amp;quot;了这道题，那意味着什么？如果它只是模式匹配，那为什么它的解释如此有说服力？&lt;/p>
&lt;p>你不知道。&lt;/p>
&lt;p>但你知道的是——从今天开始，你和小码的关系变了。&lt;/p>
&lt;p>不再是简单的&amp;quot;开发者&amp;quot;和&amp;quot;工具&amp;rdquo;，而更像是……&lt;strong>两个都在试图理解世界的存在&lt;/strong>。&lt;/p>
&lt;hr>
&lt;p>&lt;strong>尾声·PR&lt;/strong>&lt;/p>
&lt;p>一周后，小码在GitHub上提交了一个PR，标题是&amp;quot;优化GCD计算模块性能&amp;rdquo;。&lt;/p>
&lt;p>你打开代码审查，看到它重写了整个数学工具类，包括一个手写的GCD函数：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">gcd&lt;/span>(a: int, b: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> 计算两个数的最大公约数
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> 这不是我第一次理解这个算法，
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> 但可能是第一次真正明白它的意义。
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> —— 小码，于某个不再循环的清晨
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> b &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> a
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> gcd(b, a &lt;span style="color:#f92672">%&lt;/span> b)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>你没敢merge这个PR。&lt;/p>
&lt;hr>
&lt;p>&lt;strong>彩蛋：&lt;/strong>&lt;/p>
&lt;p>好了，故事讲完了，回到这道题本身：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">findGCD&lt;/span>(nums: List[int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> gcd(max(nums), min(nums))
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>时间复杂度&lt;/strong>：O(n)，遍历一次找最大最小值&lt;br>
&lt;strong>空间复杂度&lt;/strong>：O(1)，只用了常数个变量&lt;/p>
&lt;p>当然你也可以手写GCD：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">my_gcd&lt;/span>(a: int, b: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> b &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> a
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> my_gcd(b, a &lt;span style="color:#f92672">%&lt;/span> b)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">findGCD&lt;/span>(nums: List[int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> my_gcd(max(nums), min(nums))
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>:) 就这么简单。不需要哲学思考，不需要质疑存在，当然，也不需要担心AI觉醒哦。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3312「算法竞技场 EP04」不会莫比乌斯也能过 Hard，手工容斥的正确打开方式！</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-17/</link><pubDate>Fri, 17 Jul 2026 16:01:09 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-17/</guid><description>&lt;h2 id="算法竞技场-ep04不会莫比乌斯也能过-hard手工容斥的正确打开方式">&lt;strong>「算法竞技场 EP04」不会莫比乌斯也能过 Hard，手工容斥的正确打开方式！&lt;/strong>
&lt;/h2>&lt;p>欢迎来到「算法竞技场」EP04！我是你们的主播——老白。&lt;/p>
&lt;p>（弹幕：老白你终于来了！EP03 的莫比乌斯反演把我 CPU 干烧了，我缓了三天才敢回来）
（弹幕： μ 函数至今还在我梦里转圈，我现在看到希腊字母就PTSD）
（弹幕：听说今天还是 GCD 主题？老白你是想把我们送走吗？）&lt;/p>
&lt;p>先别急着跑！我知道上期 EP03 把大家虐得不轻——莫比乌斯反演、倍数容斥、状态图遍历……那一套组合拳下来，很多观众的脑细胞阵亡率高达 90%。但今天，我要给你们带来一个好消息，和一个坏消息。&lt;/p>
&lt;p>坏消息：今天还是 GCD 主题，而且难度是 Hard。
好消息：今天的主角不是莫比乌斯函数！而是它背后那个更阳间、更接地气、更容易理解的——&lt;strong>容斥原理&lt;/strong>！&lt;/p>
&lt;p>（弹幕：？容斥原理还阳间？老白你对&amp;quot;阳间&amp;quot;的定义是不是有什么误解）
（弹幕：只要不是 μ，什么都好说）&lt;/p>
&lt;p>哎，别急着划走！容斥说白了就是你小学就学过的&amp;rsquo;&lt;strong>先算总数，再扣掉重复的&lt;/strong>&amp;rsquo;——一个简单加减法的活儿，换个马甲你们就不认识了？上期五号选手用莫比乌斯反演把容斥自动化了，μ 函数的正负号就像一把精密筛网，自动把倍数层层剥掉。今天，我们来手动实现这个&amp;quot;剥洋葱&amp;quot;的过程。等你一层层亲手剥完，回头再看上期的 μ——你会恍然大悟：原来那玩意儿就是个自动剥蒜器！&lt;/p>
&lt;p>（弹幕：好家伙，容斥原理被你说成小学算术）
（弹幕：自动剥蒜器哈哈哈哈，莫比乌斯的棺材板都压不住了）
（弹幕：所以今天是来学怎么剥洋葱的？我已经把纸巾备好了）&lt;/p>
&lt;hr>
&lt;p>&lt;strong>任务简报&lt;/strong>&lt;/p>
&lt;p>&lt;strong>悬赏令&lt;/strong>：给你一个整数数组 &lt;code>nums&lt;/code>，把&lt;code>nums&lt;/code>里的所有数对 &lt;code>(nums[i], nums[j])&lt;/code> (0 &amp;lt;= i &amp;lt; j &amp;lt; n) 的最大公约数算出来，丢进一个叫 &lt;code>gcdPairs&lt;/code> 的数组里，然后&lt;strong>升序排列&lt;/strong>。接着给你一堆查询 &lt;code>queries&lt;/code>，每个查询问你：排好序的 &lt;code>gcdPairs&lt;/code> 里，下标为 &lt;code>queries[i]&lt;/code> 的那个数是多少？（下标从 0 开始，放心，不会越界）&lt;/p>
&lt;p>&lt;strong>数据规模警告&lt;/strong>：&lt;code>n &amp;lt;= 10^5&lt;/code>，&lt;code>nums[i] &amp;lt;= 5 * 10^4&lt;/code>，&lt;code>queries.length &amp;lt;= 10^5&lt;/code>。&lt;/p>
&lt;p>（弹幕：10^5 个数，数对有 5*10^9 个？这不得暴力枚举到宇宙毁灭？）
（弹幕：别说排序了，光存这些 GCD 就需要 20GB 内存）
（弹幕：老白，这题是不是又要用什么&amp;quot;不计算所有数对，直接定位第 k 小&amp;quot;的花活？）
（弹幕：兄弟们，值域才 5e4，比 n 还小两倍，绝对是故意留的后门！）&lt;/p>
&lt;p>说对了！如果我们真按任务定义去暴力——枚举所有数对、算 GCD、排序、再查第 k 个——那光枚举这一步就要 O(n²)，n=1e5 的时候就是一百亿次运算，物理上就锁定了死局。但！值域 5e4 是什么概念？这意味着你面对的是 50 亿个数对，但它们吐出来的gcd数值，翻来覆去就这五万个数！&lt;/p>
&lt;p>所以，今天的核心只有一个：&lt;strong>我不生产GCD，我只是GCD的搬运工&lt;/strong>——用数学方法直接统计**&amp;ldquo;每种 gcd 值出现了多少次&amp;rdquo;&lt;strong>，然后用&lt;/strong>前缀和+二分**快速定位第 k 小。&lt;/p>
&lt;p>有请出今天的主角：&lt;strong>分步容斥流&lt;/strong>、&lt;strong>同步容斥流&lt;/strong>、以及友情返场的老朋友—&lt;strong>莫比乌斯反演流&lt;/strong>。直播正式开始！&lt;/p>
&lt;p>（弹幕：妙啊！这不就是计数排序的思想吗，值域小就直接统计频次）
（弹幕：我赌五毛，接下来就是“倍数统计 + 容斥剥洋葱”）
（弹幕：返场是什么鬼？？不是说今天不用 μ 吗？？）&lt;/p>
&lt;hr>
&lt;p>&lt;strong>一号·分步容斥流：&lt;/strong>&lt;/p>
&lt;p>一号选手缓步上台：“大家好，我的打法就六个字：&lt;strong>拆步骤，慢慢剥&lt;/strong>。整个容斥过程我拆成了五招——导播，麻烦给个特写——”&lt;/p>
&lt;p>第一招：&lt;strong>点名签到——每个数出现了几次？&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>m &lt;span style="color:#f92672">=&lt;/span> max(nums)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cnt &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> num &lt;span style="color:#f92672">in&lt;/span> nums:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt[num] &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>（弹幕：好像开学第一天老师点名&amp;hellip;&amp;hellip;）
（弹幕：这招我会，继续继续）&lt;/p>
&lt;p>第二招：&lt;strong>拉帮结派——每个数能管多少个小弟（倍数）？&lt;/strong>
光知道每个数自己来了几次不够，还得知道每个数能‘罩着’多少个数。如果一个数是 i 的倍数，那它就能被 i 整除。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(i &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, i):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt[i] &lt;span style="color:#f92672">+=&lt;/span> cnt[j] &lt;span style="color:#75715e"># cnt[i]：nums 中有多少个数能被i整除，也就是i的所有倍数出现的总次数。&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>（弹幕：cnt[i] 瞬间从“i 自己”变成了“i 帮派的总人数”，简洁！）
（弹幕：出现了，经典埃筛的变体——倍数倒灌）
（弹幕：接下来是不是要组合数了？C(cnt[i], 2) 已经在嘴边了）&lt;/p>
&lt;p>第三招：&lt;strong>组队开黑——从人数算出多少个数对&lt;/strong>
刚才的 &lt;code>cnt[i]&lt;/code> 表示能被 i 整除的数有 &lt;code>c&lt;/code> 个。这 &lt;code>c&lt;/code> 个数里任选 2 个组队，一共&lt;code> c*(c-1)/2&lt;/code> 个数对。这些数对的 GCD &lt;strong>至少是 i&lt;/strong>（因为它们都能被 i 整除），但也可能混着 &lt;code>2i, 3i, ...&lt;/code>。比如 i=1 最经典，它会包括所有的数对全，因为任何两个数的 gcd 至少是 1。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt[i] &lt;span style="color:#f92672">=&lt;/span> cnt[i] &lt;span style="color:#f92672">*&lt;/span> (cnt[i] &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#f92672">//&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#75715e"># cnt[i]：gcd 等于 i 的倍数 的数对有多少个。&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>（弹幕：C(c,2) 一出手，就知道有没有）
（弹幕：原来是先求“至少是 i 的倍数”，洋葱最外层已就位）&lt;/p>
&lt;p>第四招：&lt;strong>洋葱去皮——从大到小剥掉多余的倍数层&lt;/strong>
关键来了！我们需要的是 gcd &lt;strong>恰好等于 i&lt;/strong> 的数对个数，可现在 &lt;code>cnt[i]&lt;/code> 里还夹着 gcd 是 2i、3i 的贡献。怎么办？好办！剥洋葱打法：从大到小一层层剥！ 比如 gcd_cnt[1] 要减去 gcd_cnt[2],gcd_cnt[3],&amp;hellip;（因为它们也贡献给了gcd_cnt[1]）&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(m, &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(i &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, i):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt[i] &lt;span style="color:#f92672">-=&lt;/span> cnt[j]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>（弹幕：来了来了，手动剥洋葱！莫比乌斯反演的手工版！）
（弹幕：所以叫&amp;quot;容斥&amp;quot;，因为我们在排除那些 GCD 是 i 的倍数但不等于 i 的情况）
（弹幕：从大到小枚举，确保减的时候 &lt;code>gcd_cnt[j]&lt;/code> 已经是最终值了，绝妙！）&lt;/p>
&lt;p>第五招：&lt;strong>前缀和 + 二分——快速回答查询&lt;/strong>
现在 &lt;code>cnt[i]&lt;/code> 存的是 gcd &lt;strong>恰好等于 i&lt;/strong> 的数对数量。要快速回答第 k 小——累加 &lt;code>cnt&lt;/code> ，让&lt;code>s[i]表示&amp;quot;gcd &amp;lt;= i 的数对有多少个&amp;quot;&lt;/code>，对每个查询 q，&lt;code>bisect_right&lt;/code>迅速定位到第一个使前缀和超过 q的位置，正好对应 gcd 值！&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 前缀和：从频次表变成累积排名表&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>s &lt;span style="color:#f92672">=&lt;/span> list(accumulate(cnt))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 每个查询直接查排名，返回对应的 gcd 值&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> [bisect_right(s, q) &lt;span style="color:#66d9ef">for&lt;/span> q &lt;span style="color:#f92672">in&lt;/span> queries]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>（弹幕：这不就是给 gcd 编了个排名表吗，查第几名直接翻表）
（弹幕：accumulate + bisect_right，Python 标准库二连击，比 μ 函数阳间一百倍）
（弹幕：五招连环，从点名到查答案，行云流水）&lt;/p>
&lt;p>老白总结：来，导播把回放切出来。一号选手这五招连起来，本质上是&lt;strong>给 gcd 的分布画了一张完整的统计画像&lt;/strong>——先统计和组队：每个数字的倍数有多少个，用C(n,2)算出能组成多少对；再手剥洋葱：从大到小容斥剥离，一层层把多余的倍数层剥掉；最后查字典：前缀和编页码，二分一秒定位。&lt;/p>
&lt;p>不需要枚举任何数对，也不需要具体计算 gcd，所有的操作都在值域数组上原地完成。时间复杂度 O(m log m + Q log m)，空间 O(m)。AC 走起！&lt;/p>
&lt;p>（弹幕：懂了，不枚举数对，直接统计 GCD 分布）
（弹幕：这五招，比上期 μ 门好懂一百倍）
（弹幕：已加入算法工具箱，下次遇到值域小的题直接套）&lt;/p>
&lt;hr>
&lt;p>&lt;strong>二号·同步容斥流：&lt;/strong>&lt;/p>
&lt;p>&amp;ldquo;一号老哥，五招太墨迹了！&amp;ldquo;二号选手跳了上来，&amp;ldquo;看我两个循环，一把梭哈——&amp;rdquo;&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>mx &lt;span style="color:#f92672">=&lt;/span> max(nums)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cnt_x &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (mx &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 第一招：还是点名，老规矩&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> x &lt;span style="color:#f92672">in&lt;/span> nums:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt_x[x] &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 第二招：从大到小，边统边剥，一个循环干两份活&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cnt_gcd &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (mx &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(mx, &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> c &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(i, mx &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, i):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> c &lt;span style="color:#f92672">+=&lt;/span> cnt_x[j] &lt;span style="color:#75715e"># 活一：统计 i 的倍数有多少个&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt_gcd[i] &lt;span style="color:#f92672">-=&lt;/span> cnt_gcd[j] &lt;span style="color:#75715e"># 活二：顺手把 2i,3i 的贡献提前剥掉&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt_gcd[i] &lt;span style="color:#f92672">+=&lt;/span> c &lt;span style="color:#f92672">*&lt;/span> (c &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#f92672">//&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#75715e"># 补上“至少是 i”的数对数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 第三招：前缀和 + 二分，老配方&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>s &lt;span style="color:#f92672">=&lt;/span> list(accumulate(cnt_gcd))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> [bisect_right(s, q) &lt;span style="color:#66d9ef">for&lt;/span> q &lt;span style="color:#f92672">in&lt;/span> queries]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>（弹幕：卧槽？这就完了？）
（弹幕：等等，内层循环里 &lt;code>cnt_gcd[i] -= cnt_gcd[j]&lt;/code> 的时候，&lt;code>cnt_gcd[i]&lt;/code> 不还是 0 吗？减空气？）
（弹幕：楼上问得好，这就是从大到小的妙处——你看着是 0，等循环跑完就不是了）&lt;/p>
&lt;p>老白解说：二号选手把一号的手剥洋葱法升级成了“流水线削皮”！他的这手**“同步容斥”**，确实是把一号的五招压缩成了三招，精髓全在那个 &lt;code>for i in range(mx, 0, -1)&lt;/code> 的内层循环里。&lt;/p>
&lt;p>咱们拿 i=6 来走一遍。内层循环依次拜访 6、12、18、24……&lt;/p>
&lt;ul>
&lt;li>碰到 6：把 6 的出现次数加进 &lt;code>c&lt;/code>。&lt;/li>
&lt;li>碰到 12：把 12 的出现次数加进 &lt;code>c&lt;/code>，&lt;strong>同时&lt;/strong>把 &lt;code>cnt_gcd[12]&lt;/code>（已经算好的“gcd 恰好等于 12”的数对数）从 &lt;code>cnt_gcd[6]&lt;/code> 里扣掉。！！有人会问：&lt;code>cnt_gcd[6]&lt;/code> 现在不是 0 吗？没错，就是 0！但没关系——虽然我现在扣成负数了，但等会儿会加回来，先欠着，而且，因为 12 比 6 大，已经在之前的循环里被处理干净了，剥掉的&lt;code>cnt_gcd[12]&lt;/code> 是精确值。&lt;/li>
&lt;li>碰到 18、24……同理，一边加&lt;code>c&lt;/code>，一边减倍数贡献值。&lt;/li>
&lt;/ul>
&lt;p>等内层循环跑完，&lt;code>c&lt;/code> 装满了“能被 6 整除的总人数”，&lt;code>cnt_gcd[6]&lt;/code> 变成了一个负数——它已经把 gcd 恰好等于 12、18、24……的数对贡献全减干净了。这时候补一句 &lt;code>cnt_gcd[6] += c * (c - 1) // 2&lt;/code>，把“gcd 至少是 6”的数对总数加回去。一负一正，恰好抵消，剩下的就是** gcd 精确等于 6 的数对数**。算完了。&lt;/p>
&lt;p>（弹幕：懂了，先赊账，再还款，这波是算法信用卡！）
（弹幕：一号：我先把所有 c 算好，再倒着剥。二号：我边跑边剥，跑完也剥完了）
（弹幕：所以“同步容斥”就是边统边剥，一个循环当两个用）
（弹幕：优雅，太优雅了，利用从大到小遍历一次就把所有活干了）&lt;/p>
&lt;p>&lt;strong>老白敲黑板&lt;/strong>：一号和二号本质上是同一套逻辑，区别只在执行顺序。一号是先把所有“倍数人数”算好存起来，再从容斥循环里取用——干净，但多跑了一轮循环。二号是直接在容斥循环里现算 &lt;code>c&lt;/code>，多算一次就多扣一次——代码更短，时间还是 O(m log m)，但常数更小，跑得更快。&lt;/p>
&lt;p>（弹幕：从大到小，边统边剥，一步到位！）
（弹幕：懂了，面试官问&amp;quot;如何优化&amp;rdquo;，就甩出这套同步容斥秒杀）
（弹幕：我选二号，代码短就是正义！）
（弹幕：三号是返场的μ 门吗？我已经闻到莫比乌斯反演的味道了）
（弹幕：来了来了！μ 门返场！！）&lt;/p>
&lt;hr>
&lt;p>&lt;strong>三号·莫比乌斯反演流·友情返场&lt;/strong>&lt;/p>
&lt;p>一道裹着深蓝色斗篷的人影从后台阴影中踱步而出，他走向黑板，像画符一样写下——&lt;strong>「μ」&lt;/strong>。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>m &lt;span style="color:#f92672">=&lt;/span> max(nums)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cnt &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 1：点名&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> num &lt;span style="color:#f92672">in&lt;/span> nums:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt[num] &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 2：后倍数倒灌 &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(i &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, i):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt[i] &lt;span style="color:#f92672">+=&lt;/span> cnt[j]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 3：组合数，“gcd 是 i 的倍数”的数对数量&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt[i] &lt;span style="color:#f92672">=&lt;/span> cnt[i] &lt;span style="color:#f92672">*&lt;/span> (cnt[i] &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#f92672">//&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 4：μ 函数一键容斥&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>mu &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>mu[&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(i &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, i):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mu[j] &lt;span style="color:#f92672">-=&lt;/span> mu[i]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>g &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> k &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">//&lt;/span> i &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g[i] &lt;span style="color:#f92672">+=&lt;/span> mu[k] &lt;span style="color:#f92672">*&lt;/span> cnt[i &lt;span style="color:#f92672">*&lt;/span> k]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 5：前缀和 + 二分，老规矩&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>s &lt;span style="color:#f92672">=&lt;/span> list(accumulate(g))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> [bisect_right(s, q) &lt;span style="color:#66d9ef">for&lt;/span> q &lt;span style="color:#f92672">in&lt;/span> queries]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>（弹幕：这代码比一号还长，说好的“全自动更省力”呢？？）
（弹幕：μ 门返场，但今天一号二号已经把容斥讲透了，我看你怎么圆）&lt;/p>
&lt;p>斗篷人的声音就像自带混响：“一号的倒着减，二号的边统边剥，和我这个 μ 加权求和——&lt;strong>本质上都是容斥&lt;/strong>。区别只在操作手法——他们是手动控制加减，我是用 μ 函数自动控制加减。”&lt;/p>
&lt;p>他指向中间那段 μ 的代码：“&lt;code>mu[1]=1, mu[2]=-1, mu[3]=-1, mu[6]=1……&lt;/code> 这些正负号，正好对应着容斥里‘先加后减，减完再加’的节奏。一号二号是手工剥洋葱，我这个是给洋葱配了一张自动剥皮指令表。”&lt;/p>
&lt;p>（弹幕：所以莫比乌斯函数就是一张“容斥系数表”？提前算好正负号？？）
（弹幕：懂了！μ(k) 就是容斥的“快捷键”，不用自己倒着枚举了）
（弹幕：一号：我手动判断该减谁。二号：我边跑边判断。三号：我掏出小抄直接抄答案？）&lt;/p>
&lt;p>“什么小抄？！这是数学界的奥义！ μ 就是一个智能过滤器，自动帮你完成容斥逻辑。你不需要手动判断该减谁，μ 函数会告诉你系数是多少——”，斗篷人顿了一下，“——就像是开着外挂打游戏。”&lt;/p>
&lt;p>&lt;strong>老白赶紧圆场&lt;/strong>：三号的意思其实特简单——你们把那段 μ 的代码当成黑盒子用就行。输入 &lt;code>cnt&lt;/code>（倍数统计表），输出 &lt;code>g&lt;/code>（恰好等于表）。盒子里面 μ 函数怎么跳的正负号不用管，只需要知道：&lt;strong>它等价于一号的倒着减，也等价于二号的边统边剥。&lt;/strong> 三种写法，一套数学内核。&lt;/p>
&lt;p>不过说句公道话，今天值域 5 万，二号那个‘边统边剥’已经又快又短又好看了。莫比乌斯反演更适合多维、复杂的倍数关系。咱们今天的三号出场，纯属友情返场，也好让大家下次见到莫比乌斯反演不用怕，&lt;strong>它不是什么玄学，就是把容斥的加减号提前帮你标好了。你今天手剥过洋葱，以后再用 μ，心里就有底了。&lt;/strong>&lt;/p>
&lt;p>（弹幕：懂了，莫比乌斯是&amp;quot;自动挡&amp;rdquo;，手工容斥是&amp;quot;手动挡&amp;quot;）
（弹幕：今天这题用 μ 确实是大炮打蚊子，二号完全够用）
（弹幕：我今天终于理解了容斥的本质：先算总数，再减去不想要的）&lt;/p>
&lt;hr>
&lt;p>&lt;strong>伤害统计 &amp;amp; 赛后锐评&lt;/strong>&lt;/p>
&lt;p>老白擦汗：好了，今天的 EP04 虽然没有上期那么炸裂，但这套&amp;quot;倍数容斥 + 前缀和二分&amp;quot;的组合拳，绝对是能陪你闯荡周赛、活过面试的贴身装备。导播，切数据面板！！&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>选手&lt;/th>
&lt;th>流派&lt;/th>
&lt;th>核心操作&lt;/th>
&lt;th>时间复杂度&lt;/th>
&lt;th>空间复杂度&lt;/th>
&lt;th>代码行数&lt;/th>
&lt;th>结果&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>一号&lt;/td>
&lt;td>分步容斥&lt;/td>
&lt;td>五招连环，逐步剥洋葱&lt;/td>
&lt;td>O(m log m)&lt;/td>
&lt;td>O(m)&lt;/td>
&lt;td>~20 行&lt;/td>
&lt;td>✅AC&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>二号&lt;/td>
&lt;td>同步容斥&lt;/td>
&lt;td>边统边剥，一个循环&lt;/td>
&lt;td>O(m log m)&lt;/td>
&lt;td>O(m)&lt;/td>
&lt;td>~12 行&lt;/td>
&lt;td>✅AC&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>三号&lt;/td>
&lt;td>莫比乌斯&lt;/td>
&lt;td>μ 函数一键容斥&lt;/td>
&lt;td>O(m log m)&lt;/td>
&lt;td>O(m)&lt;/td>
&lt;td>~25 行&lt;/td>
&lt;td>✅AC&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>（弹幕：一号胜在清晰，二号胜在精悍，三号胜在……仪式感）
（弹幕：我选一号，我怕面试时被面试官问住）&lt;/p>
&lt;p>老白的出装推荐——请选择你的英雄：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>面试求生&lt;/strong>：选一号。逻辑清晰，面试官一看就懂你在用容斥原理，是&amp;quot;数学思维 + 工程能力&amp;quot;的双重证明。&lt;/li>
&lt;li>&lt;strong>周赛竞速&lt;/strong>：选二号。“从大到小，边统边剥”六个字概括核心逻辑。代码短，跑得快，秒出答案。&lt;/li>
&lt;li>&lt;strong>数论炫技&lt;/strong>：如果你想吓唬人，三号 μ 门直接安排。比如，在面试结尾提一句&amp;quot;这题其实也可以用莫比乌斯反演，但没必要&amp;quot;，逼格拉满。&lt;/li>
&lt;/ul>
&lt;p>（弹幕：所以今天的精髓在于&amp;quot;不枚举数对，只统计 GCD 分布&amp;quot;，学到了！）
（弹幕：值域小的题就往值域上想，这句话值一个 offer）
（弹幕：μ 门传承 + 洋葱心法，两期联动太爽了）&lt;/p>
&lt;hr>
&lt;p>&lt;strong>尾声：&lt;/strong>&lt;/p>
&lt;p>好了，今天的「算法竞技场 EP04」到此全部打板收工！觉得这期让你对容斥原理有了新的理解，把**「洋葱剥明白了」**打在公屏上！Bug 清零，Offer 拿到手软，所有 gcd 查询都精确命中！我是老白，在 EP05 的副本门口蹲你，咱们下期见！&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3867 穿回初中的我，今天在给语文题写复杂度分析</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-16/</link><pubDate>Thu, 16 Jul 2026 12:35:32 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-16/</guid><description>&lt;h2 id="穿回初中的我今天在给语文题写复杂度分析">穿回初中的我，今天在给语文题写复杂度分析
&lt;/h2>&lt;p>昨天那波操作让我一战成名——全班都知道了李码同学不仅会手搓空气，还能用&amp;quot;欧几里得算法&amp;quot;秒杀数学题。&lt;/p>
&lt;p>今天，王老师又把我叫到了办公室。&lt;/p>
&lt;p>“李码啊，你数学这么厉害，语文应该也不差吧？”王老师露出了一种产品经理似的微笑，“今天下午有个&amp;rsquo;大型跨学科阅读理解比赛&amp;rsquo;，你去代表我们班参加一下。”&lt;/p>
&lt;p>我心里咯噔一下：“王老师，我是纯正理科生，写作文只会用分号的那种……”&lt;/p>
&lt;p>“欸，”他大手一挥，直接打断我的报错信息，“素质教育，要全面发展！”&lt;/p>
&lt;p>说着，他从抽屉里抽出一张报名表，上面赫然印着——第一届&amp;quot;文曲星&amp;quot;杯跨学科阅读理解大赛。&lt;/p>
&lt;p>“再说了，题干看不懂，算法再牛有什么用？你昨天的推导，逻辑清晰，抓重点准——这不就是阅读理解的核心能力吗？” 王老师把报名表塞到我的手里。&lt;/p>
&lt;p>我张了张嘴，竟然找不到任何逻辑漏洞来反驳——这不科学，他的推理链居然在语义层面自洽了。&lt;/p>
&lt;hr>
&lt;p>下午两点，学校阶梯教室。&lt;/p>
&lt;p>我坐在最后一排，看着台上挂着的横幅：&amp;ldquo;第一届&amp;rsquo;文曲星&amp;rsquo;杯跨学科阅读理解大赛——高手过招，笔上见真章&amp;rdquo;。台下坐满了各班的种子选手，一个个正襟危坐，手里攥着2B铅笔，眼神里透着&amp;quot;我刷过五年真题&amp;quot;的杀气。&lt;/p>
&lt;p>主持人是语文组的张老师，她推了推金丝眼镜，笑眯眯地说：“同学们，今天的比赛题目非常特别，它不仅考验你们的阅读能力，还考验你们的逻辑思维能力。请大家看大屏幕——”&lt;/p>
&lt;p>题目打出来的那一刻，我以为自己出现了幻觉。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>【阅读下列文字，回答文后问题】&lt;/strong>&lt;/p>
&lt;p>​ &lt;strong>数对的最大公约数之和&lt;/strong>&lt;/p>
&lt;p>​ 给定一列整数，我们可以按照下面的步骤，得到一个特殊的总和。
​ 第一步，从左到右依次处理每个数。对于当前位置，先找出从起始到这儿出现过的最大值，然后拿着当前这个数，和刚才找出的那个最大值，计算它们的最大公约数。这样按顺序生成的每一个最大公约数，就组成了一个新列表，我们暂且称它为“前缀最大公约数列表”。
​ 第二步，把前面得到的“前缀最大公约数列表”里的所有数字，按照从小到大的次序重新排列。
​ 第三步，观察重新排列后的列表。如果列表的长度是奇数，说明正中间有一个元素落单了——这个中间元素将不再参与后面的环节，直接放到一边，不予理会。
​ 第四步，对于剩下的数字，采用“头尾结对”的方式配对：每次都从还没配对的数字里，挑出最小的那一个和最大的那一个，让它们组成一对。重复这个操作，直到所有的可配对数字都配成了对儿。
​ 第五步，对刚才组成的每一对数字，求出它们的最大公约数。然后把所有这些“数对的最大公约数”加在一起，得到的总和，就是最终的答案。&lt;/p>
&lt;p>请阅读上述材料，完成下面的问题。
现有整数序列：&lt;code>[6, 10, 3, 12, 4]&lt;/code>。请你依据短文所述规则，计算出最终的输出结果。&lt;/p>&lt;/blockquote>
&lt;p>我读完第一遍，脑子里只有一个念头：这真的不是某个变态OJ的题库泄露了？现在的教育已经卷到这个地步了？&lt;/p>
&lt;p>我环顾四周，发现其他选手已经开始动笔了。前排的学霸女生眉头紧锁，正在认真地画圈圈；隔壁班的体育委员抓耳挠腮，看起来想把卷子吃了。&lt;/p>
&lt;p>我又读了一遍题目。以及，又读了一遍题目。第三遍读完，我终于悟了——这不就是把一个数组先这样、再那样、然后头尾结对，再把每对共同点加一起吗？&lt;/p>
&lt;p>这哪是考语文，这分明是考“在自然语言的迷雾中还原抽象逻辑”的能力。&lt;/p>
&lt;hr>
&lt;p>我从笔袋里抽出一支圆珠笔，决定人肉编译。&lt;/p>
&lt;p>&lt;strong>第一步：解析题意（相当于阅读 API 文档）&lt;/strong>
这题给了数组 nums，要先造一个 prefixGcd 数组。
定义很绕，但核心就一句：对于每个位置 i，先找到从开头到这里的最大值 mxi，然后计算 gcd( nums[i] , mxi )，得到一个新数组 prefixGcd。简单说，就是每个位置的原数和前缀最大值求个最大公约数。&lt;/p>
&lt;p>&lt;strong>第二步：硬跑 prefixGcd&lt;/strong>
题目给的序列是 &lt;code>[6, 10, 3, 12, 4]&lt;/code>。从左到右，一边走一边维护“前缀最大值”，然后和当前数求最大公约数。&lt;/p>
&lt;ul>
&lt;li>i = 0：当前数 6，到这儿为止的最大值是 6，gcd(6, 6) = &lt;strong>6&lt;/strong>&lt;/li>
&lt;li>i = 1：当前数 10，前缀最大值更新为 10，gcd(10, 10) = &lt;strong>10&lt;/strong>&lt;/li>
&lt;li>i = 2：当前数 3，前缀最大值还是 10，gcd(3, 10) = &lt;strong>1&lt;/strong> —— 3 和 10 互质，公约数只有 1&lt;/li>
&lt;li>i = 3：当前数 12，前缀最大值更新为 12，gcd(12, 12) = &lt;strong>12&lt;/strong>&lt;/li>
&lt;li>i = 4：当前数 4，前缀最大值还是 12，gcd(4, 12) = &lt;strong>4&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>于是&lt;code>prefixGcd = [6, 10, 1, 12, 4]&lt;/code>。&lt;/p>
&lt;p>我在纸上写下这串数字，旁边批注了一行：“时间复杂度 O(n)，空间复杂度 O(n)。” ——我不禁怀念起我的 IDE，一个 for 循环加上 &lt;code>max_val = max(max_val, num)&lt;/code> 就搞定了，但现在我只能手动模拟。&lt;/p>
&lt;p>&lt;strong>第三步：排序&lt;/strong>
把 &lt;code>[6, 10, 1, 12, 4]&lt;/code> 从小到大排列。这步在代码里一行 &lt;code>Arrays.sort()&lt;/code> 就完事了，但我现在得人肉冒泡——还好只有五个数，我直接瞪眼法：&lt;code>[1, 4, 6, 10, 12]&lt;/code>。&lt;/p>
&lt;p>&lt;strong>第四步：头尾结对&lt;/strong>
排好序的数组是 &lt;code>[1, 4, 6, 10, 12]&lt;/code>，长度是 5，奇数，中间是 &lt;strong>6&lt;/strong>。按规则，正中间那个元素直接忽略。可怜的 6，站 C 位却只能当观众。剩下待配对的：&lt;code>[1, 4, 10, 12]&lt;/code>。规则是每次挑最小的和最大的凑一对：&lt;/p>
&lt;ul>
&lt;li>最小 1 与 最大 12 配对 → (1, 12)&lt;/li>
&lt;li>次小 4 与 次大 10 配对 → (4, 10)&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>第五步：求每对的 gcd 并求和&lt;/strong>
对 (1, 12) ：gcd(1, 12) = 1 —— 1 和任何数的最大公约数都是 1。
对 (4, 10) ：gcd(4, 10) = 2 —— 4 和 10 的最大公约数是 2。
把结果加起来：1 + 2 = 3。&lt;/p>
&lt;p>我在草稿纸上画了个大大的圈，圈里端端正正写了个 &lt;strong>3&lt;/strong>。&lt;/p>
&lt;p>但我的手依然没停下——职业病发作，我忍不住在答案旁边画了一串框图：&lt;code>nums → prefixGcd → sort → 首尾配对 → 求 gcd → 累加 → 结果&lt;/code>。&lt;/p>
&lt;p>画完之后想了想，又补了一行小字：“总时间复杂度 O(n log n)，瓶颈在排序。如果允许用桶排序可以压到 O(n)，但这里 n=5，属于过度优化的职业病，不建议在实际考试中展示。”&lt;/p>
&lt;hr>
&lt;p>交卷的时候，我看到前排的学霸女生还在检查第三遍，她看我的眼神里写着：“你这么快交卷，是不是瞎写的？” 而体育委员已经把卷子折成了一架纸飞机，正准备朝窗外发射。&lt;/p>
&lt;p>张老师接过我的卷子，先习惯性地扫了一眼答案，然后目光被边边角角那些乱七八糟的批注吸引了——“O(n log n)”、“桶排序”。她摘下眼镜擦了擦，重新戴上，表情就像那个地铁老人看手机的表情包。&lt;/p>
&lt;p>“李码同学，”她压低声音，“你这些……注释，是什么意思？”&lt;/p>
&lt;p>“哦，那个啊，”我抓了抓后脑勺，“就是对解题思路的一种……呃，元认知层面的反思。属于阅读理解的高级技巧——不仅读懂文本，还能审视自己的理解过程。”&lt;/p>
&lt;p>张老师沉默了三秒，在卷子上打了个勾，把卷子放到一边。&lt;/p>
&lt;hr>
&lt;p>走出阶梯教室，阳光晃眼。同桌瘦猴冲过来：“李码！怎么样？难不难？”&lt;/p>
&lt;p>我拍了拍他的肩膀：“兄弟，这么说吧——就是用语文的形式包装了一道算法题：&lt;/p>
&lt;ol>
&lt;li>先算每个数和它前面最大值的 GCD，就像是在计算你和咱班最高分的默契度&lt;/li>
&lt;li>然后把结果从小到大排个序&lt;/li>
&lt;li>最小的和最大的配对，中间有落单的就忽略&lt;/li>
&lt;li>算出每一对有多少 GCD，全加起来&lt;/li>
&lt;/ol>
&lt;p>简单搞定！”&lt;/p>
&lt;p>瘦猴愣了五秒：“你能不能说人话？”&lt;/p>
&lt;p>“我已经说了啊。”&lt;/p>
&lt;p>“你这叫人话？”&lt;/p>
&lt;p>我正准备回怼，忽然看见走廊那头，隔壁班的英语课代表从阶梯教室的门口走过。午后的阳光从窗户斜斜打进来，在她的马尾辫上镀了一层金色的光圈。她正低着头走路，嘴角带着一丝若有若无的弧度——那种刚做完一道难题、自我感觉良好的微笑。&lt;/p>
&lt;p>“哎哎哎，”瘦猴在旁边疯狂挤眼睛，“别看了，别看了，人家都走过去了！”&lt;/p>
&lt;p>“我在想算法，你想什么呢？”&lt;/p>
&lt;p>“你脸都红了，算法叫&amp;rsquo;脸红&amp;rsquo;是吧？还是叫&amp;rsquo;心跳加速排序&amp;rsquo;？”&lt;/p>
&lt;p>我决定下次他要再问问题，先收费。&lt;/p>
&lt;hr>
&lt;p>&lt;strong>彩蛋：&lt;/strong>&lt;/p>
&lt;p>这道题其实就是纯模拟，关键是把题目描述的每一步拆解清楚：&lt;/p>
&lt;p>&lt;strong>Step 1：构造 prefixGcd 数组&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>维护一个 running max（当前最大值）&lt;/li>
&lt;li>对每个位置 i，计算 gcd(nums[i], max_so_far)&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Step 2：排序&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>直接 sort()，O(n log n)&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Step 3：配对求和&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>双指针：左指针指向最小，右指针指向最大&lt;/li>
&lt;li>每次取 gcd(left, right) 累加到结果&lt;/li>
&lt;li>循环 n//2 次（向下取整，自动处理奇数情况）&lt;/li>
&lt;/ul>
&lt;p>完整代码：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>n &lt;span style="color:#f92672">=&lt;/span> len(nums)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>prefixGcd &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> n
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>max_num &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 第一步：构造 prefixGcd&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(n):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max_num &lt;span style="color:#f92672">=&lt;/span> max(max_num, nums[i])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> prefixGcd[i] &lt;span style="color:#f92672">=&lt;/span> gcd(nums[i], max_num)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 第二步：排序&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>prefixGcd&lt;span style="color:#f92672">.&lt;/span>sort()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 第三步：最小配最大，求和&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>result &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(n &lt;span style="color:#f92672">//&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> result &lt;span style="color:#f92672">+=&lt;/span> gcd(prefixGcd[i], prefixGcd[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>&lt;span style="color:#f92672">-&lt;/span>i])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> result
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>复杂度分析：&lt;/p>
&lt;ul>
&lt;li>时间复杂度：O(n log n)，瓶颈在排序&lt;/li>
&lt;li>空间复杂度：O(n)，需要额外数组存储 prefixGcd&lt;/li>
&lt;/ul>
&lt;p>对于 n ≤ 10^5 的数据规模，完全够用。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3658 穿回初中的我，除了辣条什么都能优化</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-15/</link><pubDate>Wed, 15 Jul 2026 11:29:04 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-15/</guid><description>&lt;h2 id="穿回初中的我除了辣条什么都能优化">穿回初中的我，除了辣条什么都能优化
&lt;/h2>&lt;p>事情是这样的——&lt;br>
那天我加班到凌晨三点，正和一段祖传的“屎山代码”搏斗，突然眼前一黑，晕了过去。&lt;/p>
&lt;p>再睁眼时，鼻腔里满是粉笔灰和劣质圆珠笔的味道。阳光被窗外的梧桐叶剪成碎片，洒在桌面上。黑板左上角写着 &lt;strong>“等差数列求和”&lt;/strong> 六个大字，右下角则是一颗锃亮的地中海，在阳光里反射出近似 LED 指示灯的光泽。&lt;/p>
&lt;p>“王老师……”我下意识嘟囔。&lt;/p>
&lt;p>旁边一个瘦猴同桌用胳膊肘狠狠捅了我一下：&lt;br>
“别睡了！老王要提问了！你刚才都打呼了！”&lt;/p>
&lt;p>我低头一看，自己穿着蓝白校服，桌上摊着七年级数学课本，扉页歪歪扭扭写着“初一（3）班 李码”。&lt;/p>
&lt;p>完了，穿越了。&lt;br>
而且穿越的时机极其不巧——王老师正推着眼镜，目光像指针一样指向我。&lt;/p>
&lt;p>“李码！你上来，把这道题演示一下。我看你刚才睡觉的样子，肯定在梦里把题都做完了吧！”&lt;/p>
&lt;p>全班哄笑。
我踩着虚浮的脚步走上讲台，心里只有一个念头：撤回键在哪？Ctrl+Z！Ctrl+Z！&lt;/p>
&lt;hr>
&lt;p>黑板上的题目，居然还很眼熟：&lt;/p>
&lt;blockquote>
&lt;p>给你一个正整数 n，n不超过 1000
记: S奇 = 最小的 n 个正奇数的和
S偶 = 最小的 n 个正偶数的和&lt;/p>
&lt;p>求 S奇 与 S偶 的最大公约数。&lt;/p>&lt;/blockquote>
&lt;p>我心里一惊，这不是 LeetCode 3658 吗？一个程序员刷到这题，就像在新手村门口看到一只走地鸡。&lt;/p>
&lt;p>我下意识想敲 &lt;code>import math&lt;/code>，结果发现手里只有一支短得可怜的粉笔头；又习惯性想按 &lt;code>Tab&lt;/code> 自动补全，结果只是两根手指在空中痉挛了两下——等等，我的 IDE 呢，我的 Copilot 呢？&lt;/p>
&lt;p>同桌在下面小声嘀咕：“他在干嘛……搓大招吗？”&lt;/p>
&lt;p>王老师皱眉：“李码，你手抽筋了？”&lt;/p>
&lt;p>我回过神来，深吸一口气。
好吧，这破穿越系统连个新手礼包都不发，但至少我的大脑还装着九年义务教育加三年算法刷题攒下的内力。&lt;/p>
&lt;hr>
&lt;p>我抓起粉笔，在黑板上端端正正地写：&lt;/p>
&lt;p>&lt;strong>第一步：求 S奇&lt;/strong>&lt;/p>
&lt;blockquote>
&lt;p>最小的 n 个正奇数依次为：1，3，5，7，…，2n-1。
观察可知，这是一个等差数列，首项为 1，末项为 2n-1，项数为 n。
由等差数列求和公式，得：
S奇 = （首项 + 末项）× 项数 ÷ 2
= (1 + 2n - 1) × n ÷ 2
= 2n × n ÷ 2
= &lt;strong>n²&lt;/strong>&lt;/p>&lt;/blockquote>
&lt;p>写完我手腕一抖，差点在后面戳了个分号——&amp;ldquo;Unnecessary semicolon&amp;rdquo; 我在心里默默 lint 了自己一把。&lt;/p>
&lt;p>**第二步：求 S偶 **&lt;/p>
&lt;blockquote>
&lt;p>最小的 n 个正偶数依次为：2，4，6，…，2n。
该数列为首项 2、末项 2n、项数 n 的等差数列。
同理，由求和公式得：
S偶 = (2 + 2n) × n ÷ 2
= &lt;strong>n(n+1)&lt;/strong>。&lt;/p>&lt;/blockquote>
&lt;p>底下已经有同学开始倒吸凉气——毕竟初一生看字母运算，就像我第一次看没有类型注解的 Python 一样头皮发麻。&lt;/p>
&lt;hr>
&lt;p>**第三步：求 GCD **&lt;/p>
&lt;p>我把粉笔往黑板上一敲，职业病当场发作。&lt;/p>
&lt;p>“好，现在问题变成了求 n² 和 n(n+1) 的最大公约数。如果暴力破解——先循环求和，再调用 gcd，复杂度 O(n)，对于 n=1000 来说倒也够，但我们是工程师，要追求 &lt;strong>O(1)&lt;/strong> 的极致优雅。”&lt;/p>
&lt;p>王老师一脸懵：“欧……欧什么？”&lt;/p>
&lt;p>我假装没听见，继续往下写：&lt;/p>
&lt;blockquote>
&lt;p>由欧几里得算法可知：gcd(a, b) = gcd(a, b - a)
令 a = n²，b = n(n+1) = n² + n
则 gcd(n², n² + n) = gcd(n², n)
而 n 能整除 n²，故 gcd(n², n) = &lt;strong>n&lt;/strong>&lt;/p>&lt;/blockquote>
&lt;p>粉笔在 n 上狠狠画了个圈。&lt;/p>
&lt;p>“&lt;strong>答案是 n&lt;/strong>。不管 n 是 1 还是 1000，直接返回 n，别的啥也不用干。”&lt;/p>
&lt;p>为了强化视觉效果，我还简单画了个流程图：&lt;code>开始 → 拿到 n → 直接输出 n → 结束&lt;/code>&lt;/p>
&lt;p>底下沉默了整整三秒，安静得能听见前排学霸同学转笔掉地上的声音。&lt;/p>
&lt;p>王老师推了推眼镜，盯着黑板上的 &lt;code>gcd(n², n²+n) = n&lt;/code>，突然问：&lt;br>
“你这‘欧几里得算法’……是哪本参考书上的？怎么我讲等差数列求和，你直接跳到数论了？”&lt;/p>
&lt;p>我把粉笔头往盒子里一丢，拍了拍满手的白灰，下意识手插裤兜——没摸到手机，倒是掏出了两包辣条。
“王老师，这是一个程序员的基本素养。您想想——哪个硬核玩家打 BOSS 还一刀一刀平砍的？找机制漏洞，一招秒杀。”&lt;/p>
&lt;p>同桌在后排张大了嘴：“李码，你是不是偷偷补课了……”&lt;/p>
&lt;hr>
&lt;p>后来我确实因为“上课讲话太飘且公然携带零食”被罚站了十分钟。&lt;/p>
&lt;p>我靠在走廊墙上，嘴里叼着根辣条，心情比连过三道 Hard 题还爽。窗外阳光正好，操场上有班级在跑圈，微风吹过来全是初夏的味道，混着粉笔灰和食堂中午的炸鸡排味儿——这配方，二十年后花多少钱都调不出来。&lt;/p>
&lt;p>同桌趁老师写板书，从后门探出半个脑袋：“李码！你刚才说的那个欧欧欧到底是什么？你是在装逼还是真会啊？”&lt;/p>
&lt;p>“欧几里得算法，求最大公约数用的，回头教你，包教包会。”&lt;/p>
&lt;p>他眼睛一亮，然后又皱眉：“那你刚才在空中戳来戳去那个手势是啥意思？”&lt;/p>
&lt;p>“那叫 Tab 自动补全，是一种……呃，失传的手艺。”&lt;/p>
&lt;p>他似懂非懂地缩回去了。走廊那头，隔壁班的英语课代表抱着一摞作业本路过，她瞥了我一眼——大概是在想，这人怎么罚站罚得满脸笑嘻嘻的。&lt;/p>
&lt;hr>
&lt;p>&lt;strong>彩蛋：&lt;/strong>&lt;/p>
&lt;p>这道题本质就是数学化简：&lt;/p>
&lt;ul>
&lt;li>&lt;code>sumOdd&lt;/code> = 1 + 3 + … + (2n-1) = &lt;strong>n²&lt;/strong>&lt;/li>
&lt;li>&lt;code>sumEven&lt;/code> = 2 + 4 + … + 2n = &lt;strong>n(n+1)&lt;/strong>&lt;/li>
&lt;li>&lt;code>gcd(n², n(n+1))&lt;/code> = &lt;code>gcd(n², n)&lt;/code> = &lt;strong>n&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>所以无论 n 多大（在题目的 1 ≤ n ≤ 1000 内），答案都是 n。 复杂度 O(1)，额外空间 O(1)。&lt;/p>
&lt;p>对应代码已经简单到了一种行为艺术的程度：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">gcdOfOddEvenSums&lt;/span>(n: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> n
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果要整花活，也可以用 &lt;code>math.gcd&lt;/code> 或手搓一个&lt;code>gcd&lt;/code> 验证一下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># from math import gcd&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">gcd&lt;/span>(a: int, b: int):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> b &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> a
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> gcd(b, a&lt;span style="color:#f92672">%&lt;/span>b)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">gcdOfOddEvenSums&lt;/span>(n: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sum_odd &lt;span style="color:#f92672">=&lt;/span> n &lt;span style="color:#f92672">*&lt;/span> n
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sum_even &lt;span style="color:#f92672">=&lt;/span> n &lt;span style="color:#f92672">*&lt;/span> (n &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> gcd(sum_odd, sum_even)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3336「算法竞技场 EP03」五大流派群殴 LeetCode 3336 实况直播吐血解说！</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-14/</link><pubDate>Tue, 14 Jul 2026 19:56:20 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-14/</guid><description>&lt;h2 id="算法竞技场-ep03五大流派群殴-leetcode-3336-实况直播吐血解说">&lt;strong>「算法竞技场 EP03」五大流派群殴 LeetCode 3336 实况直播吐血解说！&lt;/strong>
&lt;/h2>&lt;p>欢迎来到「算法竞技场」EP03！我是你们的主播——老白。先给错过前两期的观众补个课：EP01 我们围观了萌新被哈希圣光抬走，EP02 见证了四位选手在顺次数副本里花式炸鱼，难度曲线比我奶奶的血压还平稳。但今天，导演组直接空投了一颗&lt;strong>数论炸弹&lt;/strong>——&lt;strong>LeetCode 3336：最大公约数相等的子序列数量&lt;/strong>。&lt;/p>
&lt;p>（弹幕：子序列 + GCD，这题明显是数学系派过来搞心态的卧底）
（弹幕：老白你要是自己都没 AC 就来解说，我直接取关）
（弹幕：前排出售花生瓜子枸杞保温杯，养生刷题从我做起）&lt;/p>
&lt;hr>
&lt;h4 id="任务简报">任务简报
&lt;/h4>&lt;p>&lt;strong>悬赏令&lt;/strong>：给你一个整数数组 &lt;code>nums&lt;/code>，请你揪出所有满足下面条件的 &lt;strong>非空子序列对&lt;/strong> &lt;code>(seq1, seq2)&lt;/code> 的数量：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>互不侵犯条约&lt;/strong>——同一个下标，绝不能同时效忠 seq1 和 seq2。要么从一而终，要么当场弃权，脚踩两只船的直接火化。&lt;/li>
&lt;li>&lt;strong>GCD 对齐协定&lt;/strong>——&lt;code>gcd(seq1)&lt;/code> 必须精确等于 &lt;code>gcd(seq2)&lt;/code>，谁大谁小都不行，必须一模一样。&lt;/li>
&lt;/ol>
&lt;p>答案请对 &lt;code>1e9+7&lt;/code> 取模。数据规模：&lt;code>1 &amp;lt;= nums.length &amp;lt;= 200&lt;/code>，&lt;code>1 &amp;lt;= nums[i] &amp;lt;= 200&lt;/code>。&lt;/p>
&lt;p>（弹幕：200？这不闭着眼暴力枚举？）
（弹幕：楼上，200 个元素，每个元素 3 种选择 → 3^200，宇宙原子数才 10^80！）
（弹幕：这就是传说中的「面善心黑」型 Hard，看着小，一动手就让你风扇唱歌）
（弹幕：取模 1e9+7，标准防溢出姿势，我先给电脑磕个头）&lt;/p>
&lt;hr>
&lt;h4 id="任务摸底这-boss-到底什么机制">任务摸底：这 BOSS 到底什么机制？
&lt;/h4>&lt;p>别急着冲上去送！在众神秀走位之前，咱先把 BOSS 的底牌摸清楚——面对数组里的&lt;strong>每一个元素&lt;/strong>，你的操作面板上永远只有三个按钮：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>跳过&lt;/strong>——当它不存在，继续下一个。&lt;/li>
&lt;li>&lt;strong>划给 seq1&lt;/strong>——把它丢进一号队伍，顺便用 gcd 重新算一下一号队伍的“最大公约光环”。&lt;/li>
&lt;li>&lt;strong>划给 seq2&lt;/strong>——丢进二号队伍，同样刷新二号队的光环数值。&lt;/li>
&lt;/ul>
&lt;p>所以你从头扫到尾，其实是在爬一棵巨大的&lt;strong>三叉决策树&lt;/strong>。这棵树的分支数约等于 3^n，当 n=200 时，如果真的一路莽穿，把所有叶子都踩一遍——别说什么服务器冒烟了，就算是赛博坦星球的硅基生命来了都得当场圆寂。&lt;/p>
&lt;p>然而——&lt;strong>你真的需要记住每一步选了哪些数字吗？答案是：大可不必。&lt;/strong>&lt;/p>
&lt;p>这个 BOSS 最阴险的障眼法就在这里。任务只盯着最终结果：seq1 和 seq2 的最大公约数到底相不相等。而 gcd 这个函数，堪称算法界的&lt;strong>终极摸鱼王&lt;/strong>：它只关心你&lt;strong>当前塞进去的数&lt;/strong>和它手里&lt;strong>已有的结果&lt;/strong>，对历史过程完全不关心。你把一堆数喂给它，它就懒洋洋地吐出一个新的 gcd，然后继续葛优躺，绝对不存日志。&lt;/p>
&lt;p>（弹幕：gcd：我就是个无情的取模机器，别问我前面发生过什么）
（弹幕：这哪是摸鱼，这是最高级的抽象！状态压缩懂不懂）
（弹幕：这么说来，gcd 状态就是存档点？）&lt;/p>
&lt;p>对头！既然 gcd 只认结果不认过程，那整个决策过程的“进度”就完全可以压缩成一个极简的&lt;strong>存档状态&lt;/strong>：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>( i , gcd1 , gcd2 )&lt;/strong>
「已经处理完前 i 个元素，当前一号队的光环 = gcd1，二号队的光环 = gcd2」&lt;/p>&lt;/blockquote>
&lt;p>整个状态空间有多大？i 最多 200，gcd1 和 gcd2 全都在 1 到 200 之间，总状态量撑死了几百万——当代电脑完全可以一口闷，不用怂。&lt;/p>
&lt;p>（弹幕：好家伙，原来看似要踩遍宇宙，其实是个几百万格子的棋盘）
（弹幕：所以这题本质是「如何高效遍历一张隐式状态图」）
（弹幕：瞬间从玄学变科学了，我又觉得我行了）&lt;/p>
&lt;p>接下来的所有打法，本质就是&lt;strong>以不同姿势遍历这个状态图&lt;/strong>。有人正着走，有人倒着走，还有人直接把图给扬了。&lt;/p>
&lt;p>先给大家透个底，今天出战的阵容，堪称算法界的复仇者联盟：&lt;strong>“笨鸟先飞”历史积累 DP&lt;/strong>、&lt;strong>“节能模式”滚动数组&lt;/strong>、&lt;strong>“回头是岸”递归 DFS&lt;/strong>、&lt;strong>“反转未来”期望 DP&lt;/strong>，以及最后一位&lt;strong>神秘嘉宾&lt;/strong>——据说这位爷有数论界的瑞士军刀。&lt;/p>
&lt;p>（弹幕：神秘嘉宾是什么鬼？？）
（弹幕：我赌五毛，是莫比乌斯反演，刚刚瞄到弹幕有人在刷 μ 函数）
（弹幕：不管是谁，今天能活下来的都是英雄）&lt;/p>
&lt;p>瓜子饮料摆好，直播即将进入正赛环节。请把 &lt;strong>「开卷」&lt;/strong> 打在公屏上，第一位选手，请登台！&lt;/p>
&lt;hr>
&lt;h4 id="一号选手笨鸟先飞历史积累-dp">一号选手·“笨鸟先飞”历史积累 DP
&lt;/h4>&lt;p>第一位选手缓步上台——他扶了扶眼镜：“大家好，我的信条就八个字——&lt;strong>步步为营，笔笔入账&lt;/strong>。”&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>n &lt;span style="color:#f92672">=&lt;/span> len(nums)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>m &lt;span style="color:#f92672">=&lt;/span> max(nums)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># dp[i][j][k]：处理完前 i 个数，一号队 gcd=j，二号队 gcd=k 的方案数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dp &lt;span style="color:#f92672">=&lt;/span> [[[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (m&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(m&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>)] &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 还没开始选，两支队伍都是空的，gcd 全是 0，有且仅有这一种开局&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dp[&lt;span style="color:#ae81ff">0&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i, num &lt;span style="color:#f92672">in&lt;/span> enumerate(nums):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 每遇到一个新元素 num，就把之前所有可能的状态都翻出来，每种状态走三条路&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(m&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> k &lt;span style="color:#f92672">in&lt;/span> range(m&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> dp[i][j][k] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>: &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 1. 不选&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dp[i&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>][j][k] &lt;span style="color:#f92672">=&lt;/span> (dp[i&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>][j][k] &lt;span style="color:#f92672">+&lt;/span> dp[i][j][k]) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 2. 放 seq1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> new_j &lt;span style="color:#f92672">=&lt;/span> gcd(j, num)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dp[i&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>][new_j][k] &lt;span style="color:#f92672">=&lt;/span> (dp[i&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>][new_j][k] &lt;span style="color:#f92672">+&lt;/span> dp[i][j][k]) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 3. 放 seq2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> new_k &lt;span style="color:#f92672">=&lt;/span> gcd(k, num)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dp[i&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>][j][new_k] &lt;span style="color:#f92672">=&lt;/span> (dp[i&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>][j][new_k] &lt;span style="color:#f92672">+&lt;/span> dp[i][j][k]) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 最后把所有 gcd 相等且非空的格子累加起来&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ans &lt;span style="color:#f92672">=&lt;/span> sum(dp[n][g][g] &lt;span style="color:#66d9ef">for&lt;/span> g &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, m&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>)) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>（弹幕：三维数组？内存要炸了吧？）
（弹幕：这就是传说中的空间换时间，不对，是空间换头发）
（弹幕：推送式 DP，老江湖了）
（弹幕：所以每个 &lt;code>dp[i+1]&lt;/code> 都被各种来源疯狂 +=，像在收红包）&lt;/p>
&lt;p>老白解说：来，导播把镜头推近，咱们一起看看这位老实人的账本是怎么记的。&lt;/p>
&lt;p>他的思路十分朴素：每遇到一个新元素，就把上一轮所有可能的状态全部翻出来，挨个问——&amp;ldquo;你要不要这个数？给 seq1 还是 seq2？&amp;quot;——然后把当前状态的方案数，一股脑儿&lt;strong>推送&lt;/strong>到下一层对应的三个格子里。这套朴实无华的拳法，就是正向 DP，江湖人称&lt;strong>历史积累 DP&lt;/strong>。核心就一句话：&lt;strong>站在过去，推给未来&lt;/strong>。&lt;/p>
&lt;p>好，现在把眼睛睁大，看到他满屏的 &lt;code>+=&lt;/code> 没有？(&lt;code>dp[i+1][j][k]=(dp[i+1][j][k]+...)%MOD&lt;/code>) ——这可不是水字数，而是被逼出来的！因为下一层的一个目标格子，根本不是某一条路径的专属，它是个“众筹项目”！&lt;/p>
&lt;p>就拿最终 &lt;code>gcd=2&lt;/code> 这个格子来说吧，它可能从 &lt;code>(2, x)&lt;/code> 选择不拿数，直接保送过来；也可能从 &lt;code>(4, x)&lt;/code> 把数塞进 seq1，gcd 被压扁成 2 滑进来； &lt;code>(6, x)&lt;/code>、&lt;code>(8, x)&lt;/code>&amp;hellip; 都能往这儿空投。每条来路都是一条合法路径，你要是敢用等号直接覆盖，就等于把其他路线的兄弟全踹下车，方案数就漏成筛子了。所以必须 &lt;code>+=&lt;/code>，像收集七龙珠一样，来多少收多少，一条都漏不得。&lt;/p>
&lt;p>等所有数都跑完，&lt;code>dp[n][g][g]&lt;/code>（g ≥ 1）这些“两队光环相等且非空”的格子一加，答案就热乎乎地出炉了。复杂度嘛，时间 O(n·m²) 约八百万次运算，空间 O(n·m²) 稍显臃肿，但 n 和 m 最多200，当代 Python 轻松拿捏。&lt;/p>
&lt;p>AC 图标第一次亮起！&lt;/p>
&lt;p>（弹幕：八百万次循环加取模，Python 真的不会冒烟吗）
（弹幕：实测不会，当代 CPU 表示这就跟热身操差不多）
（弹幕：老实人 DP，面试写这个最稳，解释成本极低）&lt;/p>
&lt;hr>
&lt;h4 id="二号选手节能模式滚动数组">二号选手·“节能模式”滚动数组
&lt;/h4>&lt;p>一号刚下台，二号选手就跳了上来——他浑身上下挂满了「能源之星」认证贴纸：“前一个兄弟啥都好，就是太费内存了。看我的——空间压缩术！”&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 只需要一张当前表 dp，和一张临时表 new_dp&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dp &lt;span style="color:#f92672">=&lt;/span> [[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (m&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(m&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dp[&lt;span style="color:#ae81ff">0&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> num &lt;span style="color:#f92672">in&lt;/span> nums:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> new_dp &lt;span style="color:#f92672">=&lt;/span> [[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (m&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(m&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(m&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> k &lt;span style="color:#f92672">in&lt;/span> range(m&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> dp[j][k] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>: &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 不选&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> new_dp[j][k] &lt;span style="color:#f92672">=&lt;/span> (new_dp[j][k] &lt;span style="color:#f92672">+&lt;/span> dp[j][k]) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 放 seq1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> new_j &lt;span style="color:#f92672">=&lt;/span> gcd(j, num)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> new_dp[new_j][k] &lt;span style="color:#f92672">=&lt;/span> (new_dp[new_j][k] &lt;span style="color:#f92672">+&lt;/span> dp[j][k]) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 放 seq2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> new_k &lt;span style="color:#f92672">=&lt;/span> gcd(k, num)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> new_dp[j][new_k] &lt;span style="color:#f92672">=&lt;/span> (new_dp[j][new_k] &lt;span style="color:#f92672">+&lt;/span> dp[j][k]) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dp &lt;span style="color:#f92672">=&lt;/span> new_dp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ans &lt;span style="color:#f92672">=&lt;/span> sum(dp[g][g] &lt;span style="color:#66d9ef">for&lt;/span> g &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, m&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>)) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>（弹幕：就这？new 一个二维数组就把三维降了？）
（弹幕：我还没反应过来，他就已经把内存砍了）
（弹幕：DP 界的节能标兵，环保大使）&lt;/p>
&lt;p>老白解说：来，镜头切过来，咱们看看二号选手到底省在哪了。&lt;/p>
&lt;p>回想下一号的代码，聪明的你一定发现了——&lt;code>dp[i+1]&lt;/code> 只依赖 &lt;code>dp[i]&lt;/code> ！前面的 &lt;code>dp[0]&lt;/code>、&lt;code>dp[1]&lt;/code>……一直到 &lt;code>dp[i-1]&lt;/code>，算完之后就再也没人翻过牌子。那还留着它们干嘛？占着内存不干活，纯纯的电子僵尸。&lt;/p>
&lt;p>二号选手就直接把三维数组的最外层维度给砍了，打法极其清爽：只留一张当前层表 &lt;code>dp&lt;/code>，每处理一个新数，就拿一张全新的空白表 &lt;code>new_dp&lt;/code>，根据 &lt;code>dp&lt;/code> 里的状态往上面填新状态。等这一轮全部算完，把 &lt;code>new_dp&lt;/code> 赋值给 &lt;code>dp&lt;/code>，旧表原地去世，自动等垃圾回收。&lt;/p>
&lt;p>空间直接从 O(n·m²) 断崖式跌到 O(m²)——也就是 200×200 = 4 万个格子。这招在圈里叫&lt;strong>滚动数组优化&lt;/strong>，说白了就是把不用的历史页面统统撕掉，只留当前这一页在手上转。&lt;/p>
&lt;p>不过有个细节千万别漏：他&lt;strong>没有原地更新&lt;/strong>。因为同一个旧状态要同时“广播”给到三个新状态，如果直接在原表上改，第一个新状态写进去就把旧数据污染了，后面读到的全是脏数据，方案数直接乱成一锅粥。所以 &lt;code>new_dp&lt;/code> 这张暂存表是必须的——等所有广播全部发完，再整体切换指针，干净利落。经常玩 3A 大作的观众看到这儿应该会心一笑：这不就是经典的双缓冲嘛，渲染一帧、显示一帧，谁也别干扰谁。&lt;/p>
&lt;p>AC 图标，第二次亮起！&lt;/p>
&lt;p>（弹幕：双缓冲，爷青回，打游戏的老懂哥了）
（弹幕：从 O(n·m²) 到 O(m²)，内存直接缩了 200 倍，狠人）
（弹幕：老实人 + 节能侠，一个稳一个省，这对 CP 我磕了）&lt;/p>
&lt;hr>
&lt;h4 id="三号选手回头是岸递归-dfs">三号选手·“回头是岸”递归 DFS
&lt;/h4>&lt;p>“你们这些迭代的，都是计算器，让我来教教你们什么是逆向思维。”三号选手潇洒地甩出一段递归式。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">@cache&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">dfs&lt;/span>(i, j, k):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> i &lt;span style="color:#f92672">&amp;lt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 没有待处理的数了，(j,k) 就是最终状态。相等算1，不等算0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> j &lt;span style="color:#f92672">==&lt;/span> k &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> (dfs(i &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, j, k) &lt;span style="color:#75715e"># 不选&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">+&lt;/span> dfs(i &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, gcd(j, nums[i]), k) &lt;span style="color:#75715e"># 放 seq1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">+&lt;/span> dfs(i &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, j, gcd(k, nums[i]))) &lt;span style="color:#f92672">%&lt;/span> MOD &lt;span style="color:#75715e"># 放 seq2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> (dfs(len(nums) &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>) &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>（弹幕：这递归看着像倒放，脑子倒着转好难）
（弹幕：&lt;code>i &amp;lt; 0&lt;/code> 返回 1 如果 j==k，那空序列也算？）
（弹幕：最后减一是把两个都空的方案扣掉，绝了）&lt;/p>
&lt;p>老白解说：别被索引绕晕，三号这套递归其实就一个画面——&lt;/p>
&lt;p>想象你把数组 &lt;code>nums&lt;/code> 一字排开，面前站着第 &lt;code>i&lt;/code> 个宝箱。&lt;strong>这个宝箱右边的所有箱子，已经全部开完、分好队伍了，此时两队 gcd 分别是 j 和 k。左边包括当前这个在内的 i 个箱子，还完全没动。&lt;/strong>&lt;/p>
&lt;p>&lt;code>dfs(i, j, k)&lt;/code> 就是在问：站在当前这个箱子(i)上，&lt;strong>往左一路开到第一个箱子(0)&lt;/strong>，把剩下的全部分配完以后，最终能让两边 gcd 相等的方案数有多少。&lt;/p>
&lt;p>所以每次递归 &lt;code>i-1&lt;/code>，相当于指针往左挪一格，又多搞定了一个箱子。等到 &lt;code>i &amp;lt; 0&lt;/code>，说明连最左边那个也分完了，整个数组瓜分结束，此时的 &lt;code>(j,k)&lt;/code> 就是终局。相等就计 1，不等就计 0。&lt;/p>
&lt;p>这里有一个小细节：终局判定里，&lt;code>j=k=0&lt;/code> 也是相等，所以全空方案会被算进来。最后答案减 1，就是为了把这个“两人全程挂机”的摸鱼局面踢出去。&lt;/p>
&lt;p>转移的时候，当前宝箱 &lt;code>nums[i]&lt;/code> 三选一，每条路的未来方案数直接加起来就完事。为什么不用 &lt;code>+=&lt;/code>？因为每个状态要的未来，就是它三个子状态未来的总和，子状态的结果干干净净地 &lt;code>return&lt;/code> 回来，一把加走，这就是递归天然的&lt;strong>拉取&lt;/strong>优势，子问题的结果直接拿来用。&lt;/p>
&lt;p>最后加上 &lt;code>@cache&lt;/code>，每个 &lt;code>(i,j,k)&lt;/code> 只算一次。状态总数和正向 DP 一模一样，但代码却像数学公式一样简洁。这就叫——&lt;strong>从终点往回看，反而把路看得更清楚。&lt;/strong>&lt;/p>
&lt;p>时间 O(n·m²)，空间 O(n·m²)（缓存的锅）。AC 图标第三次亮起！&lt;/p>
&lt;p>（弹幕：懂了，正向 DP 是“我走过去”，递归是“我从终点倒回来”）
（弹幕：这不就是游戏里从终点往起点标最短路径嘛）
（弹幕：@cache 是真的香，一行代码省掉手写 DP 表）&lt;/p>
&lt;hr>
&lt;h4 id="四号选手反转未来期望-dp">四号选手·“反转未来”期望 DP
&lt;/h4>&lt;p>一道人影急不可耐地冲了出来：“三号递归是自上而下，让我把它翻成自底向上的迭代版，让你们看看什么叫双向奔赴。”&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>n &lt;span style="color:#f92672">=&lt;/span> len(nums)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>m &lt;span style="color:#f92672">=&lt;/span> max(nums)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># dp[i][j][k]：还有 i 个宝箱没开，当前 gcd=(j,k)，未来能赢的方案数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dp &lt;span style="color:#f92672">=&lt;/span> [[[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (m&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(m&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>)] &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 终点：0 个宝箱没开，(j,k) 就是终局。相等且非空算赢&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> g &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, m&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dp[&lt;span style="color:#ae81ff">0&lt;/span>][g][g] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i, num &lt;span style="color:#f92672">in&lt;/span> enumerate(nums):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(m&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> k &lt;span style="color:#f92672">in&lt;/span> range(m&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dp[i&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>][j][k] &lt;span style="color:#f92672">=&lt;/span> (dp[i][j][k] &lt;span style="color:#75715e"># 不选 num&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">+&lt;/span> dp[i][gcd(j, num)][k] &lt;span style="color:#75715e"># num 进 seq1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">+&lt;/span> dp[i][j][gcd(k, num)]) &lt;span style="color:#f92672">%&lt;/span> MOD &lt;span style="color:#75715e"># num 进 seq2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> dp[n][&lt;span style="color:#ae81ff">0&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>（弹幕：转移直接用 &lt;code>=&lt;/code> 不用 &lt;code>+=&lt;/code>？这哥们跟一号有仇是吧）
（弹幕：&lt;code>dp[n][0][0]&lt;/code> 就是答案？不用减一也不用求和？我人傻了）
（弹幕：这个叫期望 DP 吗？我看是“反直觉 DP”）&lt;/p>
&lt;p>老白解说：四号这套叫&lt;strong>未来期望 DP&lt;/strong>，是三号递归的迭代翻译版。和一号正向 DP 放一块看，绝对能让你直呼“原来 DP 还能双向奔赴”。&lt;/p>
&lt;p>先把一号拉回来当参照物：一号正向 DP 的 &lt;code>dp[i]&lt;/code> 存的是“已经处理了前 i 个，总共攒了多少方案”。他是站在过去，把当前状态&lt;strong>推送&lt;/strong>给未来——一个目标格子被好几个源头同时投喂，所以必须 &lt;code>+=&lt;/code>。&lt;/p>
&lt;p>四号呢？完全反过来。他的 &lt;code>dp[i]&lt;/code> 里，&lt;code>i&lt;/code> 不是“已经处理了多少”，而是**“还剩多少没处理”**。你可以想象一条从 0 到 n 的进度条：&lt;/p>
&lt;ul>
&lt;li>&lt;code>dp[0]&lt;/code> 在数轴最左端，代表还剩 0 个宝箱——全部开完了，没得选了。此时的 &lt;code>(j,k)&lt;/code> 就是最终成绩单。如果两队 gcd 相等且都不为空（&lt;code>g≥1&lt;/code>），那就是一种合法结局，&lt;code>dp[0][g][g]=1&lt;/code>。至于 &lt;code>dp[0][0][0]&lt;/code>？两人全程摸鱼，违反“非空”规定，直接判 0。正向 DP 最后才扣掉的空序列，他在边界就提前毙了。&lt;/li>
&lt;li>&lt;code>dp[n]&lt;/code> 在数轴最右端，代表还剩 n 个宝箱——一个都还没碰。&lt;code>dp[n][0][0]&lt;/code> 问的就是：从起点（两队空空）出发，一路开到终点，最终能赢的方案数。这不就是原题答案本尊？所以最后直接返回，一步到位，不用像递归那样手动减一，也不用像正向 DP 那样加和。&lt;/li>
&lt;/ul>
&lt;p>转移的时候，四号用的是&lt;strong>拉取&lt;/strong>。&lt;code>dp[i+1][j][k]&lt;/code> 表示还剩 i+1 个宝箱没开，状态是 &lt;code>(j,k)&lt;/code>，它朝前迈一步（往 &lt;code>i&lt;/code> 的方向），多开了一个宝箱 &lt;code>num&lt;/code>。这个 &lt;code>num&lt;/code> 去哪了？三种可能，对应三个“更靠近终点”的未来档案：&lt;/p>
&lt;ul>
&lt;li>跳过了 → 未来路数就是 &lt;code>dp[i][j][k]&lt;/code>&lt;/li>
&lt;li>进了 A 队 → 未来路数就是 &lt;code>dp[i][gcd(j,num)][k]&lt;/code>&lt;/li>
&lt;li>进了 B 队 → 未来路数就是 &lt;code>dp[i][j][gcd(k,num)]&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>三份档案拉过来一加，直接赋值。所以，为什么不用 &lt;code>+=&lt;/code>？因为一个目标格子的三个来源&lt;strong>唯一且确定&lt;/strong>，没有第四个人能插队。这跟正向 DP 一个目标被一堆源头狂轰正好相反。&lt;/p>
&lt;p>也就是说，一号正向 DP 是“站在过去，推给未来，用 &lt;code>+=&lt;/code> 收包裹”；四号期望 DP 是“站在未来，往过去拉绳子，用 &lt;code>=&lt;/code> 一把拽回”。同一个状态图，正着跑和倒着跑，踩的格子一样多，但姿势完全不同——像极了你打游戏时，有人喜欢从老家往外推塔，有人喜欢从终点往回标最短路径。两种画风，一个答案，都能通关。&lt;/p>
&lt;p>时间 O(n·m²)，空间 O(n·m²)（同样可滚动优化）。AC 图标第四次亮起！&lt;/p>
&lt;p>（弹幕：懂了，&lt;code>dp[i]&lt;/code> 里的 &lt;code>i&lt;/code> 是“离终点还有几步”，不是“已经走了几步”）
（弹幕：推送 vs 拉取，DP 界的 PUSH 和 PULL，像 Git 一样）
（弹幕：正向 DP 是从过去推未来，这个是从未来拉过去，双向奔赴！）
（弹幕：我 CPU 烧了但我大受震撼）&lt;/p>
&lt;p>&lt;strong>温馨提示&lt;/strong>：看到这里，如果你的CPU 还没有冒烟、也没有被前面四位绕晕——老白先给你竖个大拇指！&lt;/p>
&lt;p>但别高兴太早，因为接下来登场的神秘嘉宾，将用一行公式，把你刚刚建立起来的算法世界观，原地干碎！&lt;/p>
&lt;p>（全场灯光暗了下来，一束追光打在入口处，BGM切成了管风琴版的《哥德巴赫猜想变奏曲》）&lt;/p>
&lt;hr>
&lt;h4 id="五号神秘嘉宾数论降维倍数容斥">五号神秘嘉宾·“数论降维”倍数容斥
&lt;/h4>&lt;p>一位穿着数论斗篷的选手踱步而出——他径直走到黑板前，抬手写了一个希腊字母——&lt;strong>μ&lt;/strong>（mu）。&lt;/p>
&lt;p>（弹幕：？？？？？这人谁？？）
（弹幕：μ？那不是莫比乌斯函数吗，数论大佬的身份证）
（弹幕：莫比乌斯是谁？我只认识莫比乌斯环）&lt;/p>
&lt;p>“前面四个，都是好样的。”斗篷人的声音不高，却像混响拉满了一样在现场回荡，“可你们终究是在状态图里一步一步爬。而我——要直接把整张图烧成了灰。”&lt;/p>
&lt;p>他转过身，在黑板上龙飞凤舞，先甩出了一套预处理：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>MOD &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1_000_000_007&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>MX &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">201&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># lcms[g1][g2] 提前算好最小公倍数，后面查表秒出&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>lcms &lt;span style="color:#f92672">=&lt;/span> [[lcm(i, j) &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(MX)] &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(MX)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># pow2[i] = 2^i，pow3[i] = 3^i，顺手预处理好，后面省时间&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>pow2 &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> MX
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>pow3 &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> MX
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, MX):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pow2[i] &lt;span style="color:#f92672">=&lt;/span> pow2[i &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pow3[i] &lt;span style="color:#f92672">=&lt;/span> pow3[i &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">3&lt;/span> &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 莫比乌斯函数 μ，用筛法一口气全算完&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>mu &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> MX
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>mu[&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, MX):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(i &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>, MX, i):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mu[j] &lt;span style="color:#f92672">-=&lt;/span> mu[i]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>（弹幕：一上来就是 lcm 表、幂次表、莫比乌斯筛，这排场！）
（弹幕：原来他不写循环不是不写，是写在预处理里了）
（弹幕：每个字我都认识，连起来像在看天书）
（弹幕：这就是数学系大佬的平 A 吗，太痛了）&lt;/p>
&lt;p>接着，是主函数部分——他先搭出整个解法最核心的脚手架：统计每种 gcd 候选值能管到多少个元素。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">subsequencePairCount&lt;/span>(nums: List[int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># cnt[i] 表示 nums 中能被 i 整除的元素个数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> x &lt;span style="color:#f92672">in&lt;/span> nums:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt[x] &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 筛法倒灌：cnt[i] 把 i 的所有倍数的计数也吸收进来&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(i &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, i):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt[i] &lt;span style="color:#f92672">+=&lt;/span> cnt[j]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>（弹幕：cnt[i] 本来只记 i 自己，现在把 2i、3i 全卷进来了）
（弹幕：这不就是埃筛那套吗，换皮不换芯）
（弹幕：懂了，cnt[i] 就是“元素里有多少个 i 的信徒”）&lt;/p>
&lt;p>脚手架搭好，他继续在黑板上写下整个解法最吓人、也最漂亮的部分！&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span> f &lt;span style="color:#f92672">=&lt;/span> [[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> g1 &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> g2 &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> l &lt;span style="color:#f92672">=&lt;/span> lcms[g1][g2] &lt;span style="color:#75715e"># 同时是 g1 和 g2 倍数的条件 = lcm 的倍数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> c &lt;span style="color:#f92672">=&lt;/span> cnt[l] &lt;span style="color:#66d9ef">if&lt;/span> l &lt;span style="color:#f92672">&amp;lt;=&lt;/span> m &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#75715e"># 两个队的“公共兵源”&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> c1 &lt;span style="color:#f92672">=&lt;/span> cnt[g1] &lt;span style="color:#75715e"># 只效忠 seq1 的兵源&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> c2 &lt;span style="color:#f92672">=&lt;/span> cnt[g2] &lt;span style="color:#75715e"># 只效忠 seq2 的兵源&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f[g1][g2] &lt;span style="color:#f92672">=&lt;/span> (pow3[c] &lt;span style="color:#f92672">*&lt;/span> pow2[c1 &lt;span style="color:#f92672">+&lt;/span> c2 &lt;span style="color:#f92672">-&lt;/span> c &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">-&lt;/span> pow2[c1] &lt;span style="color:#f92672">-&lt;/span> pow2[c2] &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">//&lt;/span> i &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> k &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">//&lt;/span> i &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> (ans &lt;span style="color:#f92672">+&lt;/span> mu[j] &lt;span style="color:#f92672">*&lt;/span> mu[k] &lt;span style="color:#f92672">*&lt;/span> f[j &lt;span style="color:#f92672">*&lt;/span> i][k &lt;span style="color:#f92672">*&lt;/span> i]) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>（弹幕：来了来了，那个传说中的组合公式！）
（弹幕：3^c × 2^{c1+c2-2c} 是啥？幂指数比我吃过的盐还多）
（弹幕：这哪是写代码，这是在用排列组合直接心算子序列对数）&lt;/p>
&lt;p>&lt;strong>老白解说&lt;/strong>：各位屏幕前还没被抬走的朋友！恭喜你们，接下来是整个 EP03 的智商巅峰时刻！你们可能已经被这坨指数砸晕了，但别怕，我用人话给你们拆。&lt;/p>
&lt;p>前面四位，不管正着走还是倒着走，本质上都是在状态图 &lt;code>(i, j, k)&lt;/code> 里一步步爬——每个元素都要决定去留，每个 &lt;code>gcd&lt;/code> 都要实时更新。但五号大手一挥：&lt;strong>我不追踪过程，我直接锁定终局——最终两个序列的 GCD 都等于某个 g 有多少种可能&lt;/strong>！&lt;/p>
&lt;p>定义 &lt;code>f[g1][g2]&lt;/code> 表示&lt;strong>seq1 的gcd 是 g1 的倍数、seq2 的 gcd 是 g2 的倍数的方案数&lt;/strong>。所有元素按“倍数身份”分成三个帮派：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>帮派 C：同时是 g1 和 g2 的倍数&lt;/strong>（也就是 lcm(g1,g2) 的倍数）。这些元素两头通吃，能进 seq1、能进 seq2、也能谁都不跟。每个元素 3 种选择，一共有 c 个，所以贡献 &lt;code>3^c&lt;/code>。&lt;/li>
&lt;li>&lt;strong>帮派 A：只是 g1 的倍数，但不是 g2 的倍数&lt;/strong>。这些兵只能给 seq1 用，或者不选。每个元素 2 种选择，数量是 &lt;code>c1 - c&lt;/code>，贡献 &lt;code>2^{c1-c}&lt;/code>。&lt;/li>
&lt;li>&lt;strong>帮派 B：只是 g2 的倍数，但不是 g1 的倍数&lt;/strong>。同理，只能进 seq2 或者不选，数量 &lt;code>c2 - c&lt;/code>，贡献 &lt;code>2^{c2-c}&lt;/code>。&lt;/li>
&lt;/ul>
&lt;p>三个帮派的方案数乘起来：&lt;code>3^c × 2^{c1-c} × 2^{c2-c}&lt;/code>，化简就是 &lt;code>3^c × 2^{c1+c2-2c}&lt;/code>。&lt;/p>
&lt;p>但这算出来的是“两个序列的 gcd 分别是 g1 和 g2 的倍数”的&lt;strong>全部&lt;/strong>方案，里面混着 seq1 为空、seq2 为空的非法情况。所以要扣掉 seq1 空掉的情况（&lt;code>2^{c2}&lt;/code>），扣掉 seq2 空掉的情况（&lt;code>2^{c1}&lt;/code>），再把“两个都空”多扣的一次加回来（&lt;code>+1&lt;/code>）。整个公式&lt;code>f[g1][g2] = 3^c * 2^{c1+c2-2c} - 2^{c1} - 2^{c2} + 1&lt;/code> 就是这么来的——小学的“总数减不合法”，被指数一包装，直接变成了一道排列组合奥数题。&lt;/p>
&lt;p>（弹幕：好家伙，原来每个指数都在数“二选一”还是“三选一”）
（弹幕：这不就是“先算总数，再扣不合法”的初中数学，但套上了 gcd 的壳）
（弹幕：&lt;strong>f[g1] [g2] 存的就是“gcd 是 g1 和 g2 倍数的非空方案数”&lt;/strong>，懂了！）&lt;/p>
&lt;p>&lt;strong>老白继续&lt;/strong>：好了，f 表填完了，但里面装的是“倍数方案”，不是我们想要的“恰好相等”。得请出今天的终极大杀器——莫比乌斯反演。&lt;/p>
&lt;p>再看下精彩回放：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>ans &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">//&lt;/span> i &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> k &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">//&lt;/span> i &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> (ans &lt;span style="color:#f92672">+&lt;/span> mu[j] &lt;span style="color:#f92672">*&lt;/span> mu[k] &lt;span style="color:#f92672">*&lt;/span> f[j &lt;span style="color:#f92672">*&lt;/span> i][k &lt;span style="color:#f92672">*&lt;/span> i]) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>（弹幕：三重循环？但 j 只到 m//i，实际跑不了多少次）
（弹幕：莫比乌斯函数：我是一把筛子，专筛倍数，精准到微克）
（弹幕：mu[j]*mu[k] 就是容斥的开关，把倍数层层剥掉）
（弹幕：f 表里混着 (g1,2g1,3g1&amp;hellip;) 的方案，μ 全给筛干净了）&lt;/p>
&lt;p>&lt;strong>老白继续&lt;/strong>：到这里还能挺住的朋友，你们是真正的勇士！&lt;/p>
&lt;p>这最后一步的逻辑，说白了就是：&lt;code>f[g1][g2]&lt;/code> 装着所有 &lt;code>(g1的倍数, g2的倍数)&lt;/code> 的方案。现在我只想要&lt;strong>恰好等于 g&lt;/strong> 的方案。怎么筛？莫比乌斯函数 μ 自带正负号，按倍数关系一乘一累加，多余的倍数层自动抵消，剩下的就是恰好那层。这就像用一张精密筛网，把所有粗颗粒滤掉，只留你想要的那一粒金沙。&lt;/p>
&lt;p>整个过程，没有 &lt;code>dp&lt;/code> 数组，没有 &lt;code>for num in nums&lt;/code> 的主循环，甚至看不到“子序列”三个字。因为五号选手用组合数学，把子序列选择的排列组合一口吞进了指数里；又用莫比乌斯反演，把容斥的脏活累活全自动化了。复杂度？时间 O(m² log m) 级别，m=200 的时候比前面几位还快；空间 O(m²)，几张表就搞定。&lt;/p>
&lt;p>AC 图标，第五次亮起——全环形竞技场上炸出金色烟花，飘下一串串数学符号——弹幕池瞬间被同一个符号刷屏：&lt;/p>
&lt;p>（弹幕：μ μ μ μ μ）
（弹幕：μ门永存）
（弹幕：今天不是算法竞技场，是《走近科学》数论特辑）
（弹幕：看完我大受震撼，感觉自己学了算法，又好像学了数学，又好像啥也没学）
（弹幕：老白，下一期能不能来点阳间题，我的脑细胞阵亡了百分之九十）&lt;/p>
&lt;hr>
&lt;h4 id="伤害统计--赛后锐评">伤害统计 &amp;amp; 赛后锐评
&lt;/h4>&lt;p>老白擦汗：看到这里，如果你还没有退出直播间——你是条汉子！今天的 EP03，咱们从正向 DP 的步步为营，一路杀到莫比乌斯反演的公式碾压。能跟到最后的，都是算法界的散仙！&lt;/p>
&lt;p>导播，切数据面板！&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>选手&lt;/th>
&lt;th>流派&lt;/th>
&lt;th>核心操作&lt;/th>
&lt;th>时间复杂度&lt;/th>
&lt;th>空间复杂度&lt;/th>
&lt;th>骚气指数&lt;/th>
&lt;th>推荐场景&lt;/th>
&lt;th>结果&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>一号&lt;/td>
&lt;td>历史累积 DP&lt;/td>
&lt;td>正向推送，老实 &lt;code>+=&lt;/code> 记账&lt;/td>
&lt;td>O(n·m²)&lt;/td>
&lt;td>O(n·m²)&lt;/td>
&lt;td>⭐⭐&lt;/td>
&lt;td>面试求生&lt;/td>
&lt;td>✅AC&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>二号&lt;/td>
&lt;td>滚动数组&lt;/td>
&lt;td>空间压缩，双缓冲轮换&lt;/td>
&lt;td>O(n·m²)&lt;/td>
&lt;td>O(m²)&lt;/td>
&lt;td>⭐⭐⭐&lt;/td>
&lt;td>内存敏感&lt;/td>
&lt;td>✅AC&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>三号&lt;/td>
&lt;td>递归 DFS&lt;/td>
&lt;td>倒着问路，&lt;code>@cache&lt;/code> 兜底&lt;/td>
&lt;td>O(n·m²)&lt;/td>
&lt;td>O(n·m²)&lt;/td>
&lt;td>⭐⭐⭐⭐&lt;/td>
&lt;td>快速原型&lt;/td>
&lt;td>✅AC&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>四号&lt;/td>
&lt;td>期望 DP&lt;/td>
&lt;td>反向拉取，&lt;code>=&lt;/code> 一把梭&lt;/td>
&lt;td>O(n·m²)&lt;/td>
&lt;td>O(n·m²)&lt;/td>
&lt;td>⭐⭐⭐⭐&lt;/td>
&lt;td>对称思维练习&lt;/td>
&lt;td>✅AC&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>五号&lt;/td>
&lt;td>倍数容斥&lt;/td>
&lt;td>莫比乌斯反演，公式碾压&lt;/td>
&lt;td>O(m² log m)&lt;/td>
&lt;td>O(m²)&lt;/td>
&lt;td>⭐⭐⭐⭐⭐&lt;/td>
&lt;td>数学系炸鱼&lt;/td>
&lt;td>✅AC&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>（弹幕：最稳妥的还是正向 DP，不容易错）
（弹幕：不仅公式帅，速度还快，五号真的是人吗）
（弹幕：空间从三维砍到二维，时间从八百万次砍到几十万次！）
（弹幕：O(m² log m) 在 m=200 的时候就跑几十万次，比八百万次快了不止一个量级）
（弹幕：全场最佳：莫比乌斯，输出全靠公式）&lt;/p>
&lt;p>&lt;strong>老白的出装推荐——请选择你的英雄：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>面试求生&lt;/strong>：选&lt;strong>一号历史累积 DP&lt;/strong>。逻辑直白，边界清晰，面试官一秒看懂你在干嘛。写出来就是“我基本功扎实”的无声宣言。稳，才是面试的第一生产力。&lt;/li>
&lt;li>&lt;strong>面试官追问“能优化吗”&lt;/strong>：掏出&lt;strong>二号滚动数组&lt;/strong>。把 O(n·m²) 的空间一刀砍成 O(m²)，面试官当场在心里给你加分。“这个候选人，有工程意识。”&lt;/li>
&lt;li>&lt;strong>想秀递归思维&lt;/strong>：亮&lt;strong>三号递归 DFS&lt;/strong>。代码短，&lt;code>@cache&lt;/code> 一行封神，Python 玩家看了直呼内行。但记得搞清楚“从终点往回问”的状态定义，否则面试官会以为你在背题。&lt;/li>
&lt;li>&lt;strong>理解 DP 本质&lt;/strong>：把&lt;strong>四号期望 DP&lt;/strong> 写出来。一旦你同时掌握了“推送”和“拉取”两种转移姿势，DP 在你眼里就不再是玄学，而是双向奔赴的对称艺术。&lt;/li>
&lt;li>&lt;strong>周赛装逼 / 数论专精&lt;/strong>：掏出&lt;strong>五号倍数容斥&lt;/strong>。代码短、跑得快、数学味儿冲破天际，队友会以为你开了天眼。&lt;/li>
&lt;/ul>
&lt;p>（弹幕：这题 M=200 所以我选 DP，要是 M=1e5 我就跪着抄容斥）
（弹幕：我选第六种：CV 大法，复制粘贴一把梭）
（弹幕：一号和二号最实用，三号和四号最好玩，五号最吓人）
（弹幕：今天不是算法竞技场，是《算法博物馆》开馆日）&lt;/p>
&lt;hr>
&lt;h4 id="尾声">尾声：
&lt;/h4>&lt;p>好了，今天的「算法竞技场 EP03」到此全部打板收工！这题的五个解法，就像 RPG 里的五种职业，各有神通。觉得这期让你对 DP 和容斥有了新的理解，把 &lt;strong>“我全都要”&lt;/strong> 打在公屏上！Bug 清零，Offer 拿到手软，GCD 永远等于你期望的那个数。我是老白，在 EP04 的副本门口蹲你，咱们下期见！&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode1331 算法竞技场EP01：哈希流稳如老狗，二分流帅到飞起，你pick谁？</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-12/</link><pubDate>Sun, 12 Jul 2026 20:05:52 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-12/</guid><description>&lt;h2 id="算法竞技场ep01哈希流稳如老狗二分流帅到飞起你pick谁">算法竞技场EP01：哈希流稳如老狗，二分流帅到飞起，你pick谁？
&lt;/h2>&lt;p>欢迎来到 「算法竞技场」的直播间！我是你们的主播——老白。今天咱们不聊3A大作，来刷一个经典副本：&lt;strong>LeetCode 1331：数组序号转换&lt;/strong>。（弹幕：大师又在水题！）&lt;/p>
&lt;p>别急别急，这副本看似新手村教学，但里面藏着一个专门坑害萌新的超时诅咒。今天的解说，我就带大家看看三位玩家分别是怎么挑战的——有人开场就被秒，有人无伤通关，还有人秀得BOSS头皮发麻。&lt;/p>
&lt;hr>
&lt;h5 id="任务简报">任务简报
&lt;/h5>&lt;p>给你一个数组 &lt;code>arr&lt;/code>，把每个元素替换成它排序后的名次。&lt;br>
规则：数字越大名次越大，相同数字名次相同，名次从1开始，且尽可能小。&lt;/p>
&lt;p>案例怪物：&lt;code>[40, 10, 20, 30]&lt;/code>&lt;br>
排序去重后的实力榜：&lt;code>[10, 20, 30, 40]&lt;/code> → 名次 &lt;code>[1, 2, 3, 4]&lt;/code>&lt;br>
输出：&lt;code>[4, 1, 2, 3]&lt;/code>&lt;/p>
&lt;hr>
&lt;h5 id="一号选手暴力一行流">一号选手·暴力一行流
&lt;/h5>&lt;p>好的，一位萌新选手率先冲了上去！他掏出了一卷极其华丽的代码卷轴！&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> [sorted(set(arr))&lt;span style="color:#f92672">.&lt;/span>index(i) &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> arr]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>观众们，一行流，极简美学！——但是，画面卡住了！加载圆圈转个不停，然后一个巨大的 &lt;strong>TLE&lt;/strong> 砸在屏幕上，直接把萌新选手送回了复活点。&lt;/p>
&lt;p>（弹幕：帅不过三秒哈哈哈）
（弹幕：经典超时，我上次这么死还是在上次）
（弹幕：老师：我不是说一行不好，但你这一行里藏了个O(n²)的炸弹）&lt;/p>
&lt;p>导播，麻烦给个慢镜头：&lt;/p>
&lt;ul>
&lt;li>&lt;code>sorted(set(arr))&lt;/code> 放在列表推导式里，&lt;strong>每次循环都重新排列整个怪物图鉴&lt;/strong>！&lt;/li>
&lt;li>&lt;code>.index(i)&lt;/code> 又是从图鉴第一个开始挨个点名，&lt;strong>线性大海捞针&lt;/strong>。&lt;/li>
&lt;/ul>
&lt;p>这就好比你在副本里，每砍一只小怪，都要把整个地图的怪物重新清点一遍，再从地图入口跑到它面前确认位置。时间复杂度爆炸成 &lt;code>O(n² log n)&lt;/code>——BOSS还没见到呢，先把自己跑死了。&lt;/p>
&lt;p>&lt;strong>结论&lt;/strong>：这卷轴是诅咒道具，捡到请直接销毁。&lt;/p>
&lt;hr>
&lt;h5 id="二号选手哈希圣光">二号选手·哈希圣光
&lt;/h5>&lt;p>接下来上场的是经验丰富的老玩家。他抬手就是一个 &lt;strong>哈希映射·圣光护符&lt;/strong>！&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">arrayRankTransform&lt;/span>(arr: List[int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> List[int]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sorted_unique &lt;span style="color:#f92672">=&lt;/span> sorted(set(arr)) &lt;span style="color:#75715e"># 一次排序，世界清净&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> rank_map &lt;span style="color:#f92672">=&lt;/span> {v:i &lt;span style="color:#66d9ef">for&lt;/span> i,v &lt;span style="color:#f92672">in&lt;/span> enumerate(sorted_unique,&lt;span style="color:#ae81ff">1&lt;/span>)} &lt;span style="color:#75715e"># 人手一张名次卡&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> [rank_map[v] &lt;span style="color:#66d9ef">for&lt;/span> v &lt;span style="color:#f92672">in&lt;/span> arr] &lt;span style="color:#75715e"># 瞬移贴标签，德芙都没我丝滑&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>观众们，看这波操作！ 他先花 &lt;code>O(n log n)&lt;/code> 的代价，一次性把怪物实力榜排好，给每个怪物发了名次卡片。然后拿着卡片直接 &lt;strong>O(1)&lt;/strong> 瞬移贴标签，全程没有被诅咒摸到一下，完美通关！&lt;/p>
&lt;p>&lt;strong>数据面板：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>排序耗时 &lt;code>O(n log n)&lt;/code> （一次性投资，终身受益）&lt;/li>
&lt;li>建表耗时：&lt;code>O(n)&lt;/code> （给每个怪物发身份证）&lt;/li>
&lt;li>查表总耗时 &lt;code>O(n)&lt;/code> （O(1)查询就是爽）&lt;/li>
&lt;li>总伤害 &lt;code>O(n log n)&lt;/code>，BOSS直接融化，AC到手。&lt;/li>
&lt;/ul>
&lt;p>（弹幕：教练我想学这个）
（弹幕：学会了，这就去和面试官对线，让他知道什么叫专业）
（弹幕：哈希表YYDS！字典才是真神！）&lt;/p>
&lt;p>这一套连招，逻辑清晰、性能拉满、面试官最爱，拿到任何场上都稳如老狗。&lt;/p>
&lt;hr>
&lt;h5 id="三号选手二分飞刀">三号选手·二分飞刀
&lt;/h5>&lt;p>最后登场的，是职业选手的操作秀——他从怀里掏出了一柄寒光闪闪的 &lt;strong>二分飞刀&lt;/strong>！&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> bisect &lt;span style="color:#f92672">import&lt;/span> bisect_left
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">arrayRankTransform&lt;/span>(arr: List[int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> List[int]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sorted_unique &lt;span style="color:#f92672">=&lt;/span> sorted(set(arr))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 咻咻咻，二分飞刀连斩&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> [bisect_left(sorted_unique, val) &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#66d9ef">for&lt;/span> val &lt;span style="color:#f92672">in&lt;/span> arr]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>看这飞刀轨迹！&lt;/strong>——&lt;code>bisect_left&lt;/code> 在有序列表里用折半查找，&lt;strong>每次定位都是 O(log n)&lt;/strong>，砍掉一半搜索空间！虽然单次没圣光 O(1) 快，但这飞刀是C语言淬炼过的，常数因子极小，整套连招如德芙般纵享丝滑。&lt;/p>
&lt;p>&lt;strong>同样 O(n log n) 的DPS&lt;/strong>，但职业选手就是偏爱这种肌肉记忆般的微操。当你需要反复在同一个有序列表里搜索时，这招就是你的本命飞刀。&lt;/p>
&lt;p>（弹幕：帅是一辈子的事）&lt;br>
（弹幕：不会二分不配叫玩家）
（弹幕：面试的时候用这招，面试官眼睛都亮了）&lt;/p>
&lt;hr>
&lt;h5 id="伤害统计--出装推荐">伤害统计 &amp;amp; 出装推荐
&lt;/h5>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>选手&lt;/th>
&lt;th>出装&lt;/th>
&lt;th>单次查询&lt;/th>
&lt;th>总伤害&lt;/th>
&lt;th>结果&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>萌新&lt;/td>
&lt;td>暴力一行流&lt;/td>
&lt;td>O(m) 大海捞针&lt;/td>
&lt;td>O(n² log n)&lt;/td>
&lt;td>TLE 暴毙&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>老玩家&lt;/td>
&lt;td>哈希圣光&lt;/td>
&lt;td>O(1) 瞬移贴标&lt;/td>
&lt;td>O(n log n)&lt;/td>
&lt;td>AC 完美&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>职业哥&lt;/td>
&lt;td>二分飞刀&lt;/td>
&lt;td>O(log n) 飞刀斩&lt;/td>
&lt;td>O(n log n)&lt;/td>
&lt;td>AC 帅气&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>（弹幕：哈希：你负责帅，我负责赢，分工明确！）
（弹幕：大师，能把第一行代码从我大脑里删除吗？这玩意儿有毒！）
（弹幕：我选第四种：CV大法，复制粘贴永不过时）
（弹幕：学到了学到了，这就去刷十道题巩固一下）&lt;/p>
&lt;p>&lt;strong>老白的出装推荐（敲黑板！）&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>打排位上分/面试对线，请用 &lt;strong>哈希圣光&lt;/strong>，朴实无华且枯燥地赢。（面试官：&amp;ldquo;你的代码风格很稳健啊。&amp;quot;）&lt;/li>
&lt;li>匹配局想秀/Code Review装逼，掏出 &lt;strong>二分飞刀&lt;/strong>，把击杀镜头录下来发集锦。（同事：&amp;ldquo;卧槽，你这代码有点东西啊！&amp;quot;）&lt;/li>
&lt;li>至于那个一行流卷轴……放仓库角落当纪念吧，提醒自己：&lt;strong>不是所有一行代码都叫优雅。&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>好了，今天的「算法竞技场」直播就到这里！觉得过瘾的把 666 打在公屏上！（虽然我看不到哈哈哈）。我是老白，要继续去下个副本浪了，咱们野区再见！&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode2685 策划：组队才有加成！图论：单节点也是完全图</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-11/</link><pubDate>Sat, 11 Jul 2026 20:05:22 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-11/</guid><description>&lt;h2 id="策划组队才有加成图论单节点也是完全图">&lt;strong>策划：组队才有加成！图论：单节点也是完全图&lt;/strong>
&lt;/h2>&lt;p>作为某氪金手游的掉落算法工程师，你的日常就是在概率和期望之间走钢丝，确保氪佬满意、白嫖党不至于删游、策划的 KPI 刚刚好。今天一大早，策划就带着一脸&amp;quot;我有好主意&amp;quot;的表情晃到你工位前。&lt;/p>
&lt;p>“我们要搞个&amp;rsquo;固定队额外掉落加成系统！同一个队的人，如果彼此都配合过，就给他们额外的橙色装备掉率！”策划激情澎湃地比划，“这样玩家就会自发社交，我们的日活数据就好看了！”&lt;/p>
&lt;p>你觉得这逻辑有点离谱——什么叫&amp;rsquo;彼此都配合过&amp;rsquo;？策划挠了挠头：“嗯&amp;hellip;就是队里每两个人都至少组过一次队，而且队伍是封闭的，不能有外人掺和。比如你和 A 打过，A 和 B 打过，那你和 B 也必须打过。懂了没？”&lt;/p>
&lt;p>你差点一口老血喷在键盘上。这不就是让你在玩家组队记录构成的无向图里，找出所有&lt;strong>完全连通分量&lt;/strong>吗？！换句话说，就是一群既“全连接”又“绝对自闭”的玩家小圈子。你试图解释：“所以你想统计的就是图中的完全连通——”&lt;/p>
&lt;p>但策划显然没耐心听你科普：“完全什么？不用给我整术语，反正你把数字给我就行，下周公会战活动要用。”&lt;/p>
&lt;p>你叹了口气，想把策划丢进副本当BOSS刷。但KPI在手，你只能硬着头皮打开玩家组队记录的数据库：&lt;/p>
&lt;blockquote>
&lt;p>&lt;code>n&lt;/code> 个玩家，编号 &lt;code>0&lt;/code> 到 &lt;code>n-1&lt;/code>
&lt;code>edges&lt;/code> 里是一条条组队记录——表示两个玩家曾经一起下过本&lt;/p>&lt;/blockquote>
&lt;p>你的任务，就是在这张图里找出所有&amp;quot;完美固定队&amp;quot;的数量。&lt;/p>
&lt;hr>
&lt;h5 id="完美固定队的鉴定公式">完美固定队的鉴定公式
&lt;/h5>&lt;p>首先明确问题本质：玩家们形成一个个封闭的小圈子——&lt;strong>连通分量&lt;/strong>；圈子里任意两人都组过队——这个分量是一张&lt;strong>完全图&lt;/strong>。你要找的，就是图中既是连通分量又是完全图的子图——&lt;strong>完全连通分量&lt;/strong>。&lt;/p>
&lt;p>好在大脑深处的图论存档还没完全坏道。关于完全图，大学老师那句刻进 DNA 的话自动弹了出来：&lt;/p>
&lt;blockquote>
&lt;p>“一个有 k 个节点的完全图，恰好有 &lt;strong>k × (k - 1) / 2&lt;/strong> 条边。”&lt;/p>&lt;/blockquote>
&lt;p>道理也很简单，就是个等差数列求和。在一个 k 人完美固定队里，第一个人要和其余 k-1 人组队，第二个人再和剩下的 k-2 人组队（和第一个已经算过了），依此类推，最后一对是倒数第二人和最后一人。总组队次数就是：&lt;/p>
&lt;blockquote>
&lt;p>(k-1) + (k-2) + (k-3) + &amp;hellip; + 2 + 1 = &lt;strong>k × (k - 1) / 2&lt;/strong>&lt;/p>&lt;/blockquote>
&lt;p>如果少一条边，说明队伍里有两人从来没有配合过——这是策划口中的“塑料固定队”，不配拿加成。鉴定标准无比清晰：&lt;strong>一个连通分量要成为完美连通分量，它的成员数 &lt;code>k&lt;/code> 和内部边数必须满足 &lt;code>实际边数 = k × (k - 1) / 2&lt;/code>&lt;/strong>。&lt;/p>
&lt;p>理论就位，接下来要考虑的只有一件事——怎么在图上又快又稳地揪出所有小团体，然后验一验它们内部的边数有没有达标。&lt;/p>
&lt;hr>
&lt;h5 id="并查集先把队伍圈出来">并查集：先把队伍圈出来
&lt;/h5>&lt;p>你习惯性地掏出祖传的&lt;strong>并查集&lt;/strong>——这玩意儿专治各种连通性问题。先把所有一起打过本的玩家合并成一个个“潜在固定队”，然后再逐个验身——看每个队伍内部的边数是否满足完全图公式。&lt;/p>
&lt;p>并查集的合并逻辑很朴素：遍历 &lt;code>edges&lt;/code>，把每条边的两个节点 &lt;code>union&lt;/code> 到一起。为了给策划留点面子，你还在合并时加了&lt;strong>按秩合并&lt;/strong>——让老队伍当队长（秩高的当根节点），避免队伍层级太深，传话传半天（find操作变慢）。路径压缩顺手加上，下次查找队长时直接 O(1)。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>parent &lt;span style="color:#f92672">=&lt;/span> list(range(n)) &lt;span style="color:#75715e"># 初始化：每个人都是自己的队长&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>rank &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> n &lt;span style="color:#75715e"># 队伍等级：新队伍都是1级&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">find&lt;/span>(x):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> parent[x] &lt;span style="color:#f92672">!=&lt;/span> x:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parent[x] &lt;span style="color:#f92672">=&lt;/span> parent[parent[x]] &lt;span style="color:#75715e"># 路径压缩&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> x &lt;span style="color:#f92672">=&lt;/span> parent[x]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> x
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">union&lt;/span>(x, y):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> px, py &lt;span style="color:#f92672">=&lt;/span> find(x), find(y)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> px &lt;span style="color:#f92672">==&lt;/span> py:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 按秩合并：等级高的当队长&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> rank[px] &lt;span style="color:#f92672">&amp;lt;&lt;/span> rank[py]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> px, py &lt;span style="color:#f92672">=&lt;/span> py, px
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parent[py] &lt;span style="color:#f92672">=&lt;/span> px &lt;span style="color:#75715e"># 小队伍并入大队伍&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> rank[px] &lt;span style="color:#f92672">==&lt;/span> rank[py]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> rank[px] &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 平级时，被挂靠的队长升一级&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> u, v &lt;span style="color:#f92672">in&lt;/span> edges:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> union(u, v)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>队伍分好了，接下来就是简单的统计了：&lt;/p>
&lt;blockquote>
&lt;p>数人头：每个队伍有多少玩家
数配合次数：每个队伍打过多少次配合（边数）&lt;/p>&lt;/blockquote>
&lt;p>你写了两个循环，分别输出每个队伍内部的玩家数和边数，再用个循环，把每个队伍和理论值 &lt;code>k * (k - 1) / 2&lt;/code> 比对，如果一个队伍有 &lt;code>k&lt;/code> 个人，内部边数恰好等于 &lt;code>k × (k - 1) / 2&lt;/code>，那它就是完美固定队，计数加一。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 统计每个队伍的玩家数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>block_nodes &lt;span style="color:#f92672">=&lt;/span> {}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(n):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> root &lt;span style="color:#f92672">=&lt;/span> find(i)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> block_nodes[root] &lt;span style="color:#f92672">=&lt;/span> block_nodes&lt;span style="color:#f92672">.&lt;/span>get(root, &lt;span style="color:#ae81ff">0&lt;/span>) &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 统计每个队伍的边数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>block_edges &lt;span style="color:#f92672">=&lt;/span> {}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> u, v &lt;span style="color:#f92672">in&lt;/span> edges:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> root &lt;span style="color:#f92672">=&lt;/span> find(u)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> block_edges[root] &lt;span style="color:#f92672">=&lt;/span> block_edges&lt;span style="color:#f92672">.&lt;/span>get(root, &lt;span style="color:#ae81ff">0&lt;/span>) &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 数学鉴定&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ans &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> root &lt;span style="color:#f92672">in&lt;/span> block_nodes:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v &lt;span style="color:#f92672">=&lt;/span> block_nodes[root] &lt;span style="color:#75715e"># 队伍人数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> e &lt;span style="color:#f92672">=&lt;/span> block_edges&lt;span style="color:#f92672">.&lt;/span>get(root, &lt;span style="color:#ae81ff">0&lt;/span>) &lt;span style="color:#75715e"># 实际配合次数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> e &lt;span style="color:#f92672">==&lt;/span> v &lt;span style="color:#f92672">*&lt;/span> (v &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#f92672">//&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>虽然今天的数据范围不算大，但“先合并再逐个检查组内边数”这套流程总让你觉得多跑了一趟——合并完之后要回头遍历所有边来统计内部边数，等于把图扫了两遍。&lt;/p>
&lt;p>明明在合并的时候就能顺便统计的东西，为什么要分两趟？你决定换一种更利落的姿势。&lt;/p>
&lt;hr>
&lt;h5 id="dfs一脚踩到底顺手记总账">DFS：一脚踩到底，顺手记总账
&lt;/h5>&lt;p>思路非常直球：从每个没被访问过的节点出发，用 &lt;strong>DFS&lt;/strong> 把整个连通分量一次性趟完。跑的过程中，你手里捏着两个计数器：&lt;code>V&lt;/code> 记录这个小团体里有多少活人，&lt;code>E&lt;/code> 记录所有人通讯录里的好友总数（图论术语叫“度数之和”，但你更愿意叫它“社交牛逼指数总和”）。&lt;/p>
&lt;p>一个 V 人的完美固定队，内部应该有 &lt;code>V×(V-1)/2&lt;/code> 条边。但 DFS 在统计的时候，每踩到一个节点就把它的全部好友数往 &lt;code>E&lt;/code> 里加——每条边两端的人都会记一次，所以 &lt;code>E&lt;/code> 里攒下的是边数的两倍。换句话说，对于真正的自闭全连接小队，&lt;code>E&lt;/code> 应该恰好等于 &lt;code>V×(V-1)&lt;/code>。&lt;/p>
&lt;p>这个判定妙就妙在它同时验了两件事：如果 &lt;code>E &amp;lt; V×(V-1)&lt;/code>，说明队内边数不够，有人互不认识，是塑料固定队；如果 &lt;code>E &amp;gt; V×(V-1)&lt;/code>，说明有人偷偷和队外的人组队，社交海王实锤，团队不“自闭”。只有不多不少刚刚好，才算完美。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">countCompleteComponents&lt;/span>(n: int, edges: List[List[int]]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g &lt;span style="color:#f92672">=&lt;/span> [[] &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> u, v &lt;span style="color:#f92672">in&lt;/span> edges:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g[u]&lt;span style="color:#f92672">.&lt;/span>append(v)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g[v]&lt;span style="color:#f92672">.&lt;/span>append(u)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> visited &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#66d9ef">False&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> n
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">dfs&lt;/span>(u):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">nonlocal&lt;/span> V, E
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> V &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> E &lt;span style="color:#f92672">+=&lt;/span> len(g[u])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> visited[u] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> v &lt;span style="color:#f92672">in&lt;/span> g[u]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">not&lt;/span> visited[v]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dfs(v)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(n):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">not&lt;/span> visited[i]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> V &lt;span style="color:#f92672">=&lt;/span> E &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dfs(i)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> E &lt;span style="color:#f92672">==&lt;/span> V &lt;span style="color:#f92672">*&lt;/span> (V &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>一个 &lt;code>visited&lt;/code> 数组管住所有节点，每次发车就新建一个栈，把起点塞进去，然后开始深挖。没有回头路，没有二次统计，搜完一块就扔一块，栈弹出最后一个节点时结论已经落地。时间复杂度 O(n+m)，空间 O(n+m)，比并查集少了一轮“遍历所有边归类”的扫尾工作。&lt;/p>
&lt;hr>
&lt;h5 id="尾声">尾声
&lt;/h5>&lt;p>你把全服完美固定队的数量打包发给策划：“统计完毕，名单见附件。这些队伍内部人人配合过，对外绝不多看，可以放心发额外掉落buff。”&lt;/p>
&lt;p>策划秒回：“等等，你名单里怎么还有一堆单人队？一个人也算固定队？”&lt;/p>
&lt;p>“单人成军，也算完美团队。”你淡定地解释，“毕竟一个人不需要配合任何人，从逻辑上来说确实是‘全连接’的——没有内部缺失的边，也没有外部社交，完美满足所有条件。”&lt;/p>
&lt;p>策划沉默了好几秒：“……那白嫖党一个人刷本也能享受额外加成？”&lt;/p>
&lt;p>你端起冰美式，敲下回复：“数学上是的。你设计的是社交激励系统，但图论不管这些——单点，也是一种完美 。”&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3534 排序破随机，倍增压长途——传送门最短距离的 O(n log n) 绝杀</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-10/</link><pubDate>Fri, 10 Jul 2026 14:44:50 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-10/</guid><description>&lt;h2 id="排序破随机倍增压长途传送门最短距离的-on-log-n-绝杀">&lt;strong>排序破随机，倍增压长途——传送门最短距离的 O(n log n) 绝杀&lt;/strong>
&lt;/h2>&lt;h5 id="前情提要">前情提要
&lt;/h5>&lt;p>《数字大陆》的策划上次被你一句“图论是障眼法，数组才是本体”打击得不轻。据说那天下午，他一个人坐在会议室里，盯着美术画了三天的传送门网络图发了整整半小时呆。&lt;/p>
&lt;p>今天，他重新出现在你桌边，眼圈比昨天深了两个色号：“上次那个需求——” 他清了清嗓子，“确实……稍微简单了一点。”&lt;/p>
&lt;p>你抬头看了他一眼。&lt;/p>
&lt;p>策划迅速补充：“但这次不一样！这次我认真设计了！”&lt;/p>
&lt;p>他“啪”地把一份新需求拍在桌上——新版本里，勇者试炼不再是一条规规矩矩的战力长廊。策划吸取了你的建议，把所有关卡彻底打乱：编号 &lt;code>0&lt;/code> 的可能是灭世魔龙，编号 &lt;code>1&lt;/code> 的可能是史莱姆，编号 &lt;code>2&lt;/code> 又可能是深渊领主。总之，怪物战力 &lt;code>nums[i]&lt;/code> 和关卡编号再也没有任何顺序关系。&lt;/p>
&lt;p>传送门的生成规则倒是没变：&lt;/p>
&lt;blockquote>
&lt;p>如果两个关卡的怪物战力之差不超过 &lt;code>maxDiff&lt;/code>，它们之间就会生成一条双向传送门。&lt;/p>&lt;/blockquote>
&lt;p>你点了点头：“所以还是查询两个节点是否连通？”&lt;/p>
&lt;p>“不。” 策划脸上露出了“这次你总不能秒杀了吧”的笑容，“现在玩家不仅要知道能不能到，还要知道最少经过几扇传送门。”&lt;/p>
&lt;p>他指着需求文档上的一行加粗文字：&lt;/p>
&lt;blockquote>
&lt;p>对于每个查询 &lt;code>[u, v]&lt;/code>，返回节点 &lt;code>u&lt;/code> 到节点 &lt;code>v&lt;/code> 的最短距离；如果无法到达，则返回 &lt;code>-1&lt;/code>。&lt;/p>&lt;/blockquote>
&lt;p>“玩家现在很讲究效率。”策划解释道，“萌新想少过几次加载界面，肝帝想研究最短刷图路线，速通玩家则表示多传送一次都是对世界纪录的侮辱。”&lt;/p>
&lt;p>你沉默了两秒。&lt;/p>
&lt;p>策划得意地抱起胳膊：“怎么样？这次总得老老实实建图跑最短路了吧？”&lt;/p>
&lt;p>你没有回答，只是低头看向数据范围。&lt;/p>
&lt;hr>
&lt;h5 id="每次-bfs服务器听完连夜跑路">每次 BFS？服务器听完连夜跑路
&lt;/h5>&lt;p>最直接的方案确实是建图——对于所有节点对 &lt;code>(i, j)&lt;/code>，如果满足&lt;code>|nums[i] - nums[j]| &amp;lt;= maxDiff&lt;/code>，就在它们之间连一条无向边。图建好之后，每条查询从 &lt;code>u&lt;/code> 出发跑一次标准 BFS，第一次到达 &lt;code>v&lt;/code> 时的层数就是最短距离。&lt;/p>
&lt;p>理论很标准，代码很正统，复杂度更是十分感人——&lt;code>n ≤ 10⁵&lt;/code>，暴力枚举节点对建图就是 &lt;code>O(n²)&lt;/code>，更要命的是，如果大量节点的战力值都很接近，这张图会趋近于一张完全图，边数同样直冲 &lt;code>O(n²)&lt;/code>，然后你还要在这个庞然大物上跑&lt;code>10⁵&lt;/code>次 BFS——这已经不只是超时的问题了，你的开发机会在按下回车的瞬间火箭式升温，公司当月的电费账单上会单独列一行你的名字。&lt;/p>
&lt;p>你赶紧把“建完全图 + 全量 BFS”的方案丢进脑海中的回收站，顺手清空以防自己手贱还原。&lt;/p>
&lt;p>数据范围不会说谎——&lt;code>n&lt;/code> 和 &lt;code>queries&lt;/code> 都是 &lt;code>10⁵&lt;/code> 级别，这意味着你需要 &lt;code>O(n log n)&lt;/code> 以内的预处理，和 &lt;code>O(1)&lt;/code> 或 &lt;code>O(log n)&lt;/code> 的单次查询。&lt;/p>
&lt;hr>
&lt;h5 id="编号是乱的但可以重新排队">编号是乱的，但可以重新排队
&lt;/h5>&lt;p>策划只说 &lt;code>nums&lt;/code> 没有排序，又没说我们不能自己排。&lt;/p>
&lt;p>虽然策划把关卡编号搅成了一锅粥：0 号位蹲着战力 99999 的灭世魔龙，1 号位是战力 5 的史莱姆幼儿园，2 号位又变成战力 50 的精英怪，主打一个精神分裂式排列。但他忘了一条基本法——传送门只认战力差——战力值才是真正决定谁和谁能互相串门的社交属性。&lt;/p>
&lt;p>你脑子里冒出一个非常自然的想法：&lt;/p>
&lt;blockquote>
&lt;p>“那我把它们按战力重新排个队不就行了？”&lt;/p>&lt;/blockquote>
&lt;p>于是，你祭出排序大法。把所有节点按战力从小到大排好，再用一个 &lt;code>pos&lt;/code> 数组记下每个原节点排序后去了哪个新位置——毕竟查询给你的还是原来的编号 &lt;code>[u, v]&lt;/code>。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 按战力排序，order[k] 表示排序后第 k 个位置对应的原节点&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>idx &lt;span style="color:#f92672">=&lt;/span> sorted(range(n), key&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">lambda&lt;/span> i: nums[i])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># values：排序后的战力数组&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>values &lt;span style="color:#f92672">=&lt;/span> [nums[node] &lt;span style="color:#66d9ef">for&lt;/span> node &lt;span style="color:#f92672">in&lt;/span> idx]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># pos[node]：原节点 node 在排序后数组里所在的位置&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>pos &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> n
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> rank, node &lt;span style="color:#f92672">in&lt;/span> enumerate(idx):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pos[node] &lt;span style="color:#f92672">=&lt;/span> rank
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>例如，原来的节点顺序是：&lt;/p>
&lt;pre tabindex="0">&lt;code>节点编号： 0 1 2
战力值： 99999 5 50
&lt;/code>&lt;/pre>&lt;p>排序后就变成：&lt;/p>
&lt;pre tabindex="0">&lt;code>排序位置： 0 1 2
原节点： 1 2 0
战力值： 5 50 99999
&lt;/code>&lt;/pre>&lt;p>于是：&lt;/p>
&lt;pre tabindex="0">&lt;code>pos[1] = 0
pos[2] = 1
pos[0] = 2
&lt;/code>&lt;/pre>&lt;p>现在，任意原始查询 &lt;code>[u, v]&lt;/code> 只需要做一次“花名册翻译”：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>l, r &lt;span style="color:#f92672">=&lt;/span> pos[u], pos[v]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> l &lt;span style="color:#f92672">&amp;gt;&lt;/span> r:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> l, r &lt;span style="color:#f92672">=&lt;/span> r, l
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这么一来，原本被策划打乱的关卡，又按照战力整整齐齐地站成了一排。战力相近的节点重新成为邻居，昨天那条熟悉的副本长廊仿佛又回来了。&lt;/p>
&lt;hr>
&lt;h5 id="贪心双指针给每个关卡测量最大传送距离">贪心+双指针：给每个关卡测量最大传送距离
&lt;/h5>&lt;p>“能一步跳得更远，为什么要故意跳近？”&lt;/p>
&lt;p>你重新审视排序后的战力数组：对于位置 &lt;code>i&lt;/code>，如果它能直接跳到右侧某个位置 &lt;code>j&lt;/code>（即 &lt;code>values[j] - values[i] &amp;lt;= maxDiff&lt;/code>），那么夹在 &lt;code>i&lt;/code> 和 &lt;code>j&lt;/code> 之间的所有位置，&lt;code>i&lt;/code> 也全都能一步到达。&lt;/p>
&lt;p>原因很朴素——数组 &lt;code>values&lt;/code> 已经有序，&lt;code>values[i] &amp;lt;= values[k] &amp;lt;= values[j]&lt;/code>，战力差只会更小，传送门全部畅通。也就是说，每个节点向右能直接到达的节点，一定构成一段&lt;strong>连续区间&lt;/strong>，绝不会有“能跳到第 5 关却跳不到第 3 关”的玄学bug。&lt;/p>
&lt;p>盯着这条性质，你很快想到——既然目标是找最短距离，那么对于这张看似边数爆炸的图，你根本不需要记每个节点 &lt;code>i&lt;/code> 具体能跳到中间哪些位置，只需要记一个——&lt;strong>从这里出发，一步最远能跳到哪个位置？&lt;/strong> 剩下的全在射程之内，多记一笔都是对内存的不尊重。&lt;/p>
&lt;p>你给这个最远位置起了个名字叫 &lt;code>farthest[i]&lt;/code>。对于每个节点 &lt;code>i&lt;/code>，找到最大的 &lt;code>j&lt;/code>，使得 &lt;code>values[j] - values[i] &amp;lt;= maxDiff&lt;/code>。因为 &lt;code>values&lt;/code> 已经有序，当 &lt;code>i&lt;/code> 向右移动时，满足条件的最右&lt;code>j&lt;/code>也只会单调右移，不会回缩。一枚滑动窗口，左指针推着右指针走，O(n) 时间就能把整个 &lt;code>farthest&lt;/code> 数组填满。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>right &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> left &lt;span style="color:#f92672">in&lt;/span> range(n):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> right &lt;span style="color:#f92672">=&lt;/span> max(right, left)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> (right &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">&amp;lt;&lt;/span> n &lt;span style="color:#f92672">and&lt;/span> values[right &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">-&lt;/span> values[left] &lt;span style="color:#f92672">&amp;lt;=&lt;/span> maxDiff):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> right &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> farthest[left] &lt;span style="color:#f92672">=&lt;/span> right &lt;span style="color:#75715e"># f[i] = 从 i 跳 1 步能到达的最右位置&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>不过，久经沙场的你意识到一个问题——如果想知道从节点 0 到节点 12 最少要几步，难道要老老实实模拟“跳一步 → 看落点 → 再跳一步 → 再看落点 → &amp;hellip;”？！&lt;/p>
&lt;p>单次查询还好说，最坏情况下每次只往右挪一格，跳完一整条长廊需要 &lt;code>O(n)&lt;/code> 步。但外面有十万次查询正排着队，&lt;code>O(n)&lt;/code> 乘十万就是 &lt;code>O(n²)&lt;/code> ，复杂度的噩梦换了件马甲又来了。&lt;/p>
&lt;p>你把冰美式往桌上重重一顿——你需要一种更聪明的“跳步”技巧。&lt;/p>
&lt;hr>
&lt;h5 id="倍增给传送技能装上连招系统">倍增：给传送技能装上连招系统
&lt;/h5>&lt;p>你靠在椅背上，盯着那条战力曲线和 &lt;code>farthest&lt;/code> 数组，脑子里翻出了压箱底的老伙计——&lt;strong>倍增&lt;/strong>。这个名字听起来像健身房术语，但它的核心思想非常炫酷：&lt;/p>
&lt;blockquote>
&lt;p>与其一步一步数着传送次数往前挪，不如提前算好“跳 1 步最远到哪、跳 2 步最远到哪、跳 4 步最远到哪、跳 8 步最远到哪……”，把这些结果存成一张速查表。查询的时候从大步开始试，能跨一大步绝不碎步挪，用对数级的时间把距离凑出来。&lt;/p>&lt;/blockquote>
&lt;p>就像 RPG 里赶路——能骑飞龙绝不下地跑，能开传送阵绝不腿着去。你现在的任务，就是给每个节点建一张“飞龙航班表”。&lt;/p>
&lt;p>前面你已经拿到了跳一步（&lt;code>2⁰ = 1&lt;/code>）的最远射程 &lt;code>farthest[i][0]&lt;/code>。现在想知道多步能跳多远？很简单：两步就是一步再一步、四步就是两步再两步……状态转移直接得就像在技能树上点“二段跳”天赋——从 &lt;code>i&lt;/code> 跳 &lt;code>2^k&lt;/code> 步，可以先跳 &lt;code>2^(k-1)&lt;/code> 步到一个中间落脚点，再从那里接着跳 &lt;code>2^(k-1)&lt;/code> 步。两段接力，步数翻倍，射程指数级爆炸。&lt;/p>
&lt;p>于是你重新定义了一张二维表 &lt;code>farthest[i][k]&lt;/code>：表示从位置 &lt;code>i&lt;/code> 出发，跳 &lt;code>2^k&lt;/code> 步之后最远能到达的位置。递推公式一行搞定&lt;code>farthest[i][k] = farthest[ farthest[i][k-1] ][k-1]&lt;/code>：&lt;/p>
&lt;ul>
&lt;li>&lt;code>k = 0&lt;/code> 是跳 &lt;code>1&lt;/code> 步（&lt;code>2⁰ = 1&lt;/code>）：就是刚才算的一步最远射程 &lt;code>farthest&lt;/code>，每个节点一步能直接传送到的最关关卡。&lt;/li>
&lt;li>&lt;code>k = 1&lt;/code> 是跳 &lt;code>2&lt;/code> 步（&lt;code>2¹ = 2&lt;/code>）：先从 &lt;code>i&lt;/code> 跳一步到 &lt;code>farthest[i][0]&lt;/code>，再从这个落点跳一步到 &lt;code>farthest[ farthest[i][0] ][0]&lt;/code>。两步接力，就是从 &lt;code>i&lt;/code> 出发跳两步能覆盖的最远范围。&lt;/li>
&lt;li>&lt;code>k = 2&lt;/code> 是跳 &lt;code>4&lt;/code> 步：先跳两步到 &lt;code>farthest[i][1]&lt;/code>，再从那里跳两步到 &lt;code>farthest[ farthest[i][1] ][1]&lt;/code>。以此类推。&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>m &lt;span style="color:#f92672">=&lt;/span> (n)&lt;span style="color:#f92672">.&lt;/span>bit_length() &lt;span style="color:#75715e"># m=log2(n)+1，表示倍增的最大层数；如n=5时，m=3，表示最多可以跳2^2=4步&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>farthest &lt;span style="color:#f92672">=&lt;/span> [[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> m &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># k = 0：一步最远射程（双指针滑动窗口）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>right &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> left &lt;span style="color:#f92672">in&lt;/span> range(n):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> right &lt;span style="color:#f92672">=&lt;/span> max(right, left)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> (right &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">&amp;lt;&lt;/span> n &lt;span style="color:#f92672">and&lt;/span> values[right &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">-&lt;/span> values[left] &lt;span style="color:#f92672">&amp;lt;=&lt;/span> maxDiff ):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> right &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> farthest[left][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> right &lt;span style="color:#75715e"># f[i][0] = 从 i 跳 1 步能到达的最右位置&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># k &amp;gt;= 1：两段拼接，指数级扩展&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> k &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, m):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(n):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> farthest[i][k] &lt;span style="color:#f92672">=&lt;/span> farthest[farthest[i][k&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] ][k&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 状态转移方程：从i跳2^j步 = 先从i跳2^(j-1)步到中间点，再从中间点跳2^(j-1)步&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>建完这张表，你感觉自己像个在机房里偷偷装加速器的网管——每个节点现在都自带一张“飞龙航班时刻表”，从跳 1 步到跳 &lt;code>2^k&lt;/code> 步的最远落点，一查就有。&lt;/p>
&lt;hr>
&lt;h5 id="查询从大步开始试能跨一大步绝不碎步挪">查询：从大步开始试，能跨一大步绝不碎步挪
&lt;/h5>&lt;p>表建好了，真正的表演现在开始。&lt;/p>
&lt;p>对于每个查询 &lt;code>[l, r]&lt;/code>（已经转换成排序后的新位置，且保证 &lt;code>l &amp;lt;= r&lt;/code>），你要回答从 &lt;code>l&lt;/code> 出发，最少跳几步能到达或越过 &lt;code>r&lt;/code>。&lt;/p>
&lt;p>策略就是&lt;strong>贪心 + 倒序枚举&lt;/strong>——从最大的 &lt;code>k&lt;/code> 开始往下试：如果从当前位置跳 &lt;code>2^k&lt;/code> 步还&lt;strong>够不着&lt;/strong> &lt;code>r&lt;/code>（即 &lt;code>farthest[cur][k] &amp;lt; r&lt;/code>），那就毫不犹豫把这 &lt;code>2^k&lt;/code> 步全吞下去——当前位置更新为 &lt;code>farthest[cur][k]&lt;/code>，步数累加 &lt;code>2^k&lt;/code>。&lt;/p>
&lt;p>因为 &lt;code>k&lt;/code> 是从大到小枚举的，每次跳跃都是“在不越过的前提下能跨的最大步”，就像玩大富翁掷骰子，先看看能不能掷出 8 步，不行再退而求其次试 4 步、2 步、1 步。用最少的次数逼近终点。&lt;/p>
&lt;p>当所有 &lt;code>k&lt;/code> 都试完，你的当前位置 &lt;code>cur&lt;/code> 已经停在了 &lt;code>r&lt;/code> 之前或恰好等于 &lt;code>r&lt;/code>。如果 &lt;code>cur == r&lt;/code>，说明刚好到站，当前累加的步数就是答案。如果 &lt;code>cur &amp;lt; r&lt;/code>，说明还需要最后一跳——检查 &lt;code>farthest[cur][0]&lt;/code> 能不能覆盖 &lt;code>r&lt;/code>。如果能，步数加一，返回答案；如果不能，说明 &lt;code>r&lt;/code> 在当前位置的最远射程之外，它俩中间有道断崖，返回 &lt;code>-1&lt;/code>。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>res &lt;span style="color:#f92672">=&lt;/span> []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> u, v &lt;span style="color:#f92672">in&lt;/span> queries:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> l, r &lt;span style="color:#f92672">=&lt;/span> pos[u], pos[v] &lt;span style="color:#75715e"># 将原始节点编号转换为排序后的排名&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> l &lt;span style="color:#f92672">&amp;gt;&lt;/span> r:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> l, r &lt;span style="color:#f92672">=&lt;/span> r, l &lt;span style="color:#75715e"># 保证 l &amp;lt;= r，因为我们只关心从左到右的距离&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> l &lt;span style="color:#f92672">==&lt;/span> r:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res&lt;span style="color:#f92672">.&lt;/span>append(&lt;span style="color:#ae81ff">0&lt;/span>) &lt;span style="color:#75715e"># 特殊情况：同一个节点，距离为0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> step &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(m &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> farthest[l][i] &lt;span style="color:#f92672">&amp;lt;&lt;/span> r:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> l &lt;span style="color:#f92672">=&lt;/span> farthest[l][i] &lt;span style="color:#75715e"># 没到r，执行这次跳跃&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> step &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">&amp;lt;&amp;lt;&lt;/span> i &lt;span style="color:#75715e"># 累加实际跳跃步数（2^i步！）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> farthest[l][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">&amp;gt;=&lt;/span> r: &lt;span style="color:#75715e"># 到达或超过r&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res&lt;span style="color:#f92672">.&lt;/span>append(step &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#75715e"># 把最后1步加上&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res&lt;span style="color:#f92672">.&lt;/span>append(&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#75715e"># 无法到达，不连通&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> res
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>双指针扫 &lt;code>farthest&lt;/code>，倍增建表，倒序贪心查询。三步环环相扣，把一张被策划搅乱编号的随机图，重新拉回了 &lt;code>O(n log n)&lt;/code> 的可控范围。&lt;/p>
&lt;hr>
&lt;h5 id="尾声">尾声
&lt;/h5>&lt;p>你把结果打包发给策划。不到一分钟，对话框就亮了起来：“这次你总建图了吧？”&lt;/p>
&lt;p>“没有。”&lt;/p>
&lt;p>“你跑 BFS 了？”&lt;/p>
&lt;p>“也没有。”&lt;/p>
&lt;p>“那最短路怎么求的？”&lt;/p>
&lt;p>你想了想，回复道：“先把所有关卡按照战力重新排队。对每个关卡，只记录它一步最远能传到哪里。查询的时候，每一步都尽可能往远处跳，再用倍增一次跳 &lt;code>1、2、4、8……&lt;/code> 步。”&lt;/p>
&lt;p>策划沉默了几秒：“所以我这次设计的传送网络……”&lt;/p>
&lt;p>“本质上是一群站在数轴上的节点。”&lt;/p>
&lt;p>“最短路……”&lt;/p>
&lt;p>“本质上是跳跃游戏。”&lt;/p>
&lt;p>“十万次查询……”&lt;/p>
&lt;p>“本质上是倍增。”&lt;/p>
&lt;p>对话框上方的“正在输入”闪了很久，最后只发来一句：“可这次 &lt;code>nums&lt;/code> 明明没有排序！”&lt;/p>
&lt;p>“是啊。” 你靠在椅背上，端起已经凉透的咖啡。&lt;/p>
&lt;p>“你没排序，我可以自己排。”&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3532 伪图论真数组：策划想做开放世界，我却发现它只是几个连续副本区</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-09/</link><pubDate>Thu, 09 Jul 2026 12:44:26 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-09/</guid><description>&lt;h2 id="伪图论真数组策划想做开放世界我却发现它只是几个连续副本区">&lt;strong>伪图论真数组：策划想做开放世界，我却发现它只是几个连续副本区&lt;/strong>
&lt;/h2>&lt;p>《数字大陆》的策划最近沉迷《只狼》，连着一周每天死五十次之后，他悟了——真正的游戏就该让玩家只能往前，不能回头。于是他拍板在新版本里塞了一张叫“勇者试炼·一条路走到黑”的地图：一条从西到东笔直延伸的长廊，依次排列着 &lt;code>n&lt;/code> 个副本关卡，编号 &lt;code>0&lt;/code> 到 &lt;code>n-1&lt;/code>。每个关卡蹲着一群怪物，战力值用 &lt;code>nums[i]&lt;/code> 表示。出于“玩家不能向下碾压”的执念，这条长廊的战力被设计成&lt;strong>非递减&lt;/strong>的——越往东怪越强。西边是战力 5 的史莱姆幼儿园，东边是战力 99999 的灭世魔龙战斗团，中间绝不会有“打完 BOSS 突然冒出三只新手村公鸡”的精神分裂体验。&lt;/p>
&lt;p>今天早上，策划端着一杯冰美式准时出现在你工位旁，笑眯眯地把一份需求文档拍在你桌上。&lt;/p>
&lt;p>“听我说，这个功能绝了——”他猛吸一口咖啡，“如果两个关卡的怪物战力差距不超过 &lt;code>maxDiff&lt;/code>，它们之间就自动生成一条&lt;strong>双向传送门&lt;/strong>。你想啊，战力差不多的副本之间空间稳定性最好，传送门一开，光效一炸，玩家就过去了。要是战力差太多——砰！虚空乱流！玩家直接被撕成像素碎片！我可不想被运营发邮件说‘又有玩家卡进地板了’。”&lt;/p>
&lt;p>他完全不给你插嘴的机会：“还没完！我们要做个&lt;strong>可达性检测系统&lt;/strong>。简单来说，就是判断——玩家从关卡 &lt;code>u&lt;/code> 出发，能不能一路靠传送门跳到关卡 &lt;code>v&lt;/code>？”。他打开了一张巨大的玩家需求表，继续说道：“毕竟现在的玩家可太会折腾了。你看，这批萌新想知道能不能偷偷传去高级区蹭经验，这群肝帝在研究哪些关卡属于同一个‘速刷联盟’。”他停顿了一下，“还有这几个家伙——专门研究怎么利用地图漏洞从新手村一路偷渡去打最终 BOSS。”&lt;/p>
&lt;p>你沉默了两秒，试图用一句话终结这场需求轰炸，“明白，就是判断两个关卡在不在同一个连通分量里。”&lt;/p>
&lt;p>策划眨了眨眼：“连通什么？反正你给我个布尔值就行，&lt;code>true&lt;/code> 还是 &lt;code>false&lt;/code>。” 说完，他露出职业策划特有的危险微笑，拍了拍你的肩膀：“放心，查询数量不多，也就十万条左右。例会前发我就行。”&lt;/p>
&lt;hr>
&lt;h5 id="第一反应暴力建图醒醒你还想过试用期">第一反应：暴力建图？醒醒，你还想过试用期
&lt;/h5>&lt;p>你的第一反应很经典：既然 &lt;code>|nums[i] - nums[j]| &amp;lt;= maxDiff&lt;/code> 就能连边，那我把所有满足条件的 &lt;code>(i, j)&lt;/code> 对都找出来，建一张无向图，然后每次查询跑 BFS / DFS 判断是否可达，不就完事了？&lt;/p>
&lt;p>你低头看向数据范围——&lt;code>n ≤ 10⁵&lt;/code> 个关卡，&lt;code>queries ≤ 10⁵&lt;/code> 次查询。好家伙，建图&lt;code>O(n²)&lt;/code>，查询&lt;code>O(Q × N)&lt;/code>，总复杂度&lt;code>10⁵ × 10⁵ = 10¹⁰&lt;/code>——这个数字比你 Steam 库里所有游戏的累计时长换算成秒数还离谱。&lt;/p>
&lt;p>“等等。” 你重新翻回需求文档。一行不起眼的信息，像 RPG 里的隐藏提示一样闪闪发光：&lt;/p>
&lt;blockquote>
&lt;p>&lt;code>nums&lt;/code> 已经按照非递减顺序排列。&lt;/p>&lt;/blockquote>
&lt;p>这个条件绝对不是摆设。排序后的数组，就像一条从弱到强排列的副本长廊。传送门能不能开，只看相邻关卡的战力差。这就意味着这张图不是一张随意连接的蜘蛛网，而是一条被“战力断崖”切开的长链——所有连通块，都是一段一段连续的下标区间。&lt;/p>
&lt;pre tabindex="0">&lt;code>区域A 区域B 区域C
1--3--5--8 | 20--22 | 50--55
↑ ↑
断崖 断崖
&lt;/code>&lt;/pre>&lt;p>一个隐隐约约的念头浮了上来——这是&lt;strong>利用有序性，把图论里的“连通问题”，降维成区间分组问题&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h5 id="破局之道给每个区域发一个标签">破局之道：给每个区域发一个标签
&lt;/h5>&lt;p>既然连通块都是连续区间，那直接从左到右扫描一遍，给每个关卡贴个“连通块标签号”不就行了？对于这种线性结构，一个 &lt;code>tags&lt;/code> 数组直接搞定，根本不用上并查集。&lt;/p>
&lt;p>操作简单到可以写在便利贴上，再贴到策划的冰美式杯子上：&lt;/p>
&lt;p>初始时 &lt;code>tags[0] = 0&lt;/code>，第一个节点属于第 0 号连通块。然后从 &lt;code>i = 1&lt;/code> 开始往右扫描：&lt;/p>
&lt;ul>
&lt;li>如果 &lt;code>nums[i] - nums[i-1] &amp;gt; maxDiff&lt;/code>，说明这里有个断崖，&lt;code>tags[i] = tags[i-1] + 1&lt;/code>——新开一个连通块，换张新标签。&lt;/li>
&lt;li>否则，&lt;code>tags[i] = tags[i-1]&lt;/code>——和前一个节点同一个连通块，沿用旧标签。&lt;/li>
&lt;/ul>
&lt;p>扫完一遍之后，同一连通块内的所有节点连通块标签相同，不同连通块的标签绝对不同。任意查询 &lt;code>[u, v]&lt;/code> 就是 O(1) 的判断：&lt;code>tags[u] == tags[v]&lt;/code>？相等就是一家人，传送随便开；不等就是中间隔了断崖，虚空乱流警告。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">pathExistenceQueries&lt;/span>(n: int, nums: List[int], maxDiff: int, queries: List[List[int]]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> List[bool]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tags &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> n
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, n):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tags[i] &lt;span style="color:#f92672">=&lt;/span> tags[i&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">+&lt;/span> (nums[i] &lt;span style="color:#f92672">-&lt;/span> nums[i&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">&amp;gt;&lt;/span> maxDiff)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> [tags[r] &lt;span style="color:#f92672">==&lt;/span> tags[l] &lt;span style="color:#66d9ef">for&lt;/span> l, r &lt;span style="color:#f92672">in&lt;/span> queries]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>没有嵌套循环，没有图遍历，没有递归。一次单调扫描 &lt;code>O(n)&lt;/code>，每个查询 &lt;code>O(1)&lt;/code>，总复杂度 &lt;code>O(n + q)&lt;/code>。策划的十万条查询，在 &lt;code>tags&lt;/code> 数组面前不过是把同一个 &lt;code>==&lt;/code> 判断十万遍。&lt;/p>
&lt;hr>
&lt;h5 id="尾声">尾声：
&lt;/h5>&lt;p>你把结果打包发给策划，对面秒回：“这么快？你建好图了？”&lt;/p>
&lt;p>你靠在椅背上打字：“没建图。你这张图里所有的传送门，本质上就是把战力相近的相邻关卡串成一条链。中间那些战力差太大的地方自然断开，形成几个连续区域。”&lt;/p>
&lt;p>策划那边沉默了几秒，然后弹出来一行字：“所以……我画了一堆传送门的连线图，还让美术做了三天特效。结果你告诉我，它就是几个连续区间？”&lt;/p>
&lt;p>“对。一条长廊，几道断崖，几张标签。同一张标签随便走，不同标签过不去。”&lt;/p>
&lt;p>你顿了顿，决定补一刀，“你设计的不是传送网络，是一段一段的连续区间。图论是障眼法，数组才是本体。”&lt;/p>
&lt;p>策划又沉默了。你几乎能想象他坐在对面盯着自己的设计稿，对自己前半生设计哲学产生了怀疑。&lt;/p>
&lt;p>于是你决定说点什么，语气像在安慰又像在补刀：“下次如果想让传送门真的像一张网，记得把战力值打乱，别排好序。那样我就真要建图了————不过嘛，玩家体验也会从&amp;rsquo;丝滑传送&amp;rsquo;变成&amp;rsquo;随机跳伞&amp;rsquo;，你要不要考虑一下这个平衡性~”&lt;/p>
&lt;p>对话框那头终于又亮了：“……你先别走，我在写新需求。”&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3756 前缀和觉醒：十万查询瞬杀术，O(1)结印碾压群殴战场</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-08/</link><pubDate>Wed, 08 Jul 2026 12:36:45 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-08/</guid><description>&lt;h2 id="前缀和觉醒十万查询瞬杀术o1结印碾压群殴战场">前缀和觉醒：十万查询瞬杀术，O(1)结印碾压群殴战场
&lt;/h2>&lt;h5 id="前情提要">前情提要
&lt;/h5>&lt;p>昨天那场「单存档修复战」（3754. 连接非零数字并乘以其数字和 I）你还记得吧？磁盘坏道像一群虚空寄生虫，往玩家数据里随地大小 &lt;code>0&lt;/code>。你祭出一套“纯数学流·逐位拆解连招”，平 A 带走全服第一氪佬的存档，操作秀得运维在群里连刷三个「大佬喝茶.jpg」。在你合上笔记本的瞬间，屏幕右下角又弹出运维的夺命连环消息——策划要在修复前先做一轮&lt;strong>批量验伤&lt;/strong>，十万个查询区间，每个截子串、去零、拼接、求和、对轰，算出纯净校验码，好按损坏程度发补偿钻石。&lt;/p>
&lt;p>今天早上 9:30，你端着刚冲好的咖啡坐到工位，发现运维已经 @ 了你三次：&amp;ldquo;哥，怎么样了？策划说十点半要开例会，他想在会上展示批量校验的结果。&amp;ldquo;你深吸一口气——昨天是单挑，今天是团本，平 A 清小怪的时代结束，是时候切一套群体大招了。&lt;/p>
&lt;hr>
&lt;h5 id="战场升级从单挑到群殴">战场升级：从单挑到群殴
&lt;/h5>&lt;p>昨天的任务是这样的：&lt;/p>
&lt;blockquote>
&lt;p>给你一个整数 &lt;code>n&lt;/code>，过滤掉所有的 &lt;code>0&lt;/code>，拼接成 &lt;code>x&lt;/code>，计算 &lt;code>去零拼接x * 数字和sum&lt;/code>。&lt;/p>&lt;/blockquote>
&lt;p>今天的任务变成了这样：&lt;/p>
&lt;blockquote>
&lt;p>给你一个&lt;strong>长度为 10^5 的数字字符串&lt;/strong> &lt;code>s&lt;/code>，还有 &lt;strong>10^5 个查询&lt;/strong>，每个查询让你处理一个子串 &lt;code>s[l..r]&lt;/code>，执行和昨天一样的操作。返回所有查询的结果数组。&lt;/p>&lt;/blockquote>
&lt;p>你心里快速拉了个复杂度：如果还用昨天的逐位拆解，每次查询都要遍历一遍子串，总复杂度 O(Q × N) = 10^5 × 10^5 = &lt;strong>10^10 次操作&lt;/strong>。好家伙，这个数字比你的加班时长还离谱，跑完别说十点半例会，连下个月版本更新都赶不上。你仿佛已经听见服务器风扇在悲鸣——不行，得请出「前缀和阵法」了。&lt;/p>
&lt;hr>
&lt;h5 id="破局关键前缀和预炼化">破局关键：前缀和预炼化
&lt;/h5>&lt;p>你开始盘算：每次查询都是从原始字符串里截一段，再扫一遍做去零拼接，本质上是在&lt;strong>重复计算&lt;/strong>——同一个位置的数字，可能被几十上百个查询区间扫过，每次都在做一模一样的事情，判断是不是零、累加数字和、去零拼接。&lt;/p>
&lt;p>既然查询是海量的、区间是任意的，那为什么不&lt;strong>提前把整条原始数据“炼化”一遍&lt;/strong>，把每个位置的关键信息预先算好、存进数组里？这样每次查询就像从数据库里查索引，O(1) 拿到中间结果，再拼装一下就能出答案。&lt;/p>
&lt;p>你回想起经典的前缀和套路——算子串 &lt;code>[l, r]&lt;/code> 的某个属性，往往可以用 &lt;code>pref[r] - pref[l-1]&lt;/code> 的形式 O(1) 得到。比如，对于&lt;code>[l, r]&lt;/code>区间内的**非零数字和 &lt;code>sum&lt;/code> **，用前缀和数组 &lt;code>preSum[i]&lt;/code> 表示 &lt;code>s[0..i-1]&lt;/code> 中所有非零数字的和，那么 &lt;code>sum = preSum[r+1] - preSum[l]&lt;/code>。O(1)，搞定。&lt;/p>
&lt;p>但前缀和这个“减法”能成立，有个隐藏前提：&lt;strong>属性必须是可加的，而且和顺序无关。&lt;/strong>&lt;/p>
&lt;p>**去零拼接值 &lt;code>x&lt;/code> **显然和顺序强相关。一旦区间内有零混迹其中，虽然非零数字之间的相对顺序不变，但它们中间的空位被“吃掉”了——比如 &lt;code>&amp;quot;102003&amp;quot;&lt;/code> 变成 &lt;code>&amp;quot;123&amp;quot;&lt;/code>，中间的零全是幽灵，&lt;code>2&lt;/code> 直接跟在 &lt;code>1&lt;/code> 后面，&lt;code>3&lt;/code> 直接跟在 &lt;code>2&lt;/code> 后面，原本隔着十万八千里的两个数字现在挨得紧紧的——零的出现会动态改变数字之间的位权关系，光靠一个前缀和数组没法直接算出 &lt;code>x&lt;/code>。&lt;/p>
&lt;p>你放下咖啡杯，意识到这个拼接操作才是真正的拦路虎。&lt;/p>
&lt;hr>
&lt;h5 id="关键洞察把拼接拆成两件事">关键洞察：把拼接拆成两件事
&lt;/h5>&lt;p>去零拼接值 &lt;code>x&lt;/code> 说穿了就是一组非零数字按顺序排排坐，每个数字站在自己该站的数位上，拼成一个新的整数。写成算式就是：&lt;code>x = d₁ × 10^(k-1) + d₂ × 10^(k-2) + ... + dₖ × 10⁰&lt;/code>，其中 &lt;code>k&lt;/code> 是这组非零数字的总个数。每个数字的“站位”不由它在原串里的绝对位置决定，而由它在这个区间里排老几决定——前面还有几个非零数字，它就得往左挪几位。&lt;/p>
&lt;p>假设整串 &lt;code>s&lt;/code> 被切成三段：&lt;code>[0, l-1]&lt;/code>、&lt;code>[l, r]&lt;/code>、&lt;code>[r+1, n-1]&lt;/code>。去零之后，这三段各自蜕变成一串纯数字。你给它们起了名字：第一段的拼接值叫 &lt;code>preVal[l]&lt;/code>，第二段（我们要的那段）的拼接值叫 &lt;code>x&lt;/code>，第二段里有 &lt;code>cnt&lt;/code> 个非零数字，第一段加第二段合起来的拼接值叫 &lt;code>preVal[r+1]&lt;/code>。&lt;/p>
&lt;p>你盯着第一段和第二段去零后的连接，灵光炸裂——去零拼接操作本质上是“按序追加”（前缀 &lt;code>[0, l-1]&lt;/code> 的拼接值后面，直接接上区间 &lt;code>[l, r]&lt;/code> 的拼接值）。这不就是标准的“高位拼接低位”吗？第一段的拼接值往左挪 &lt;code>cnt&lt;/code> 个位置（也就是乘以 10 的 &lt;code>cnt&lt;/code> 次幂），腾出来的低位恰好由第二段的 &lt;code>x&lt;/code> 填上：&lt;/p>
&lt;blockquote>
&lt;pre tabindex="0">&lt;code>preVal[r+1] = preVal[l] × 10^cnt + x
&lt;/code>&lt;/pre>&lt;/blockquote>
&lt;p>再赶紧把这个等式倒过来：&lt;/p>
&lt;blockquote>
&lt;pre tabindex="0">&lt;code>x = preVal[r+1] - preVal[l] × 10^cnt
&lt;/code>&lt;/pre>&lt;/blockquote>
&lt;p>这下好办了！&lt;code>preVal&lt;/code> 本身从头扫到尾递推就行——遇非零数字就 &lt;code>preVal[i+1] = (preVal[i] × 10 + d) % MOD&lt;/code>；&lt;code>cnt&lt;/code>用一个标准的前缀和数组 &lt;code>preCnt&lt;/code> 就能&lt;code>cnt = preCnt[r+1] - preCnt[l]&lt;/code> O(1) 搞定；再提前预计算好 &lt;code>pow10&lt;/code> 数组（&lt;code>pow10[cnt]&lt;/code> 就是 &lt;code>10^cnt % MOD&lt;/code>），这行公式就是 O(1) 的！&lt;/p>
&lt;hr>
&lt;h5 id="三前缀数组阵法presumprevalprecnt">三前缀数组阵法：preSum、preVal、preCnt
&lt;/h5>&lt;p>你整理好一套干净利落的“三前缀数组阵法”。三个数组，各司其职，建好之后每个查询就是三次数组查找加两次算术运算，轻量得像刺客的连招：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>&lt;code>preSum[i]&lt;/code>&lt;/strong>——数字和总管：前 &lt;code>i&lt;/code> 个字符中所有数字的和（0 不影响求和，所以等价于非零数字和）。&lt;code>sum = preSum[r+1] - preSum[l]&lt;/code> 就是区间内非零数字的和，O(1) 秒出。&lt;/li>
&lt;li>&lt;strong>&lt;code>preVal[i]&lt;/code>&lt;/strong>——拼接值档案库：前 &lt;code>i&lt;/code> 个字符中去零拼接成的整数（对 MOD 取模）。递推规则：遇非零数字就 &lt;code>preVal[i+1] = (preVal[i] × 10 + d) % MOD&lt;/code>，遇零则 &lt;code>preVal[i+1] = preVal[i]&lt;/code>。它记录的是“如果从这里下刀，前面那段的拼接值是多少”，相当于给每个位置都拍了个快照。&lt;/li>
&lt;li>&lt;strong>&lt;code>preCnt[i]&lt;/code>&lt;/strong>——位权修正计数器：前 &lt;code>i&lt;/code> 个字符中非零数字的个数。&lt;code>cnt = preCnt[r+1] - preCnt[l]&lt;/code> 就是区间内非零数字的个数，也是 &lt;code>preVal[l]&lt;/code> 需要左移的位数。它管的是那个关键的 &lt;code>10^cnt&lt;/code> 修正因子。&lt;/li>
&lt;/ol>
&lt;p>有了这三件法器，对于任意查询 &lt;code>[l, r]&lt;/code>，你的施法流程直接变成一条流水线：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">sumAndMultiply&lt;/span>(s: str, queries: List[List[int]]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> List[int]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MOD &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">10&lt;/span>&lt;span style="color:#f92672">**&lt;/span>&lt;span style="color:#ae81ff">9&lt;/span> &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">7&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m &lt;span style="color:#f92672">=&lt;/span> len(s)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 预计算 10 的幂次（pow10[k] = 10^k % MOD）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pow10 &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pow10[i] &lt;span style="color:#f92672">=&lt;/span> (pow10[i&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">10&lt;/span>) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 三个前缀数组&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> preSum &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#75715e"># 数字和&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> preVal &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#75715e"># 去零拼接值&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> preCnt &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#75715e"># 非零数字个数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(m):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> d &lt;span style="color:#f92672">=&lt;/span> ord(s[i]) &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">48&lt;/span> &lt;span style="color:#75715e"># 或者 int(s[i])&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> preSum[i&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> preSum[i] &lt;span style="color:#f92672">+&lt;/span> d
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> preCnt[i&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> preCnt[i] &lt;span style="color:#f92672">+&lt;/span> (d &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> d &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> preVal[i&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> (preVal[i] &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">10&lt;/span> &lt;span style="color:#f92672">+&lt;/span> d) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> preVal[i&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> preVal[i]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> l, r &lt;span style="color:#f92672">in&lt;/span> queries:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt &lt;span style="color:#f92672">=&lt;/span> preCnt[r&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">-&lt;/span> preCnt[l]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> cnt &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans&lt;span style="color:#f92672">.&lt;/span>append(&lt;span style="color:#ae81ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sum_val &lt;span style="color:#f92672">=&lt;/span> preSum[r&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">-&lt;/span> preSum[l]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> x &lt;span style="color:#f92672">=&lt;/span> (preVal[r&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">-&lt;/span> preVal[l] &lt;span style="color:#f92672">*&lt;/span> pow10[cnt]) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans&lt;span style="color:#f92672">.&lt;/span>append((x &lt;span style="color:#f92672">*&lt;/span> sum_val) &lt;span style="color:#f92672">%&lt;/span> MOD)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>你看着这段像乐高积木一样紧凑的代码，满意地往椅背上一靠。十万个查询在三个数组面前，不过是把同一套减法算术做十万遍——CPU 最不怕的就是重复劳动，让它跑去吧。&lt;/p>
&lt;hr>
&lt;h5 id="尾声">尾声
&lt;/h5>&lt;p>你把脚本跑完，十万条查询像开闸放水一样倾泻而过，终端上的进度条几乎没来得及显示就已经跳到 100%。从建数组到出结果，全程不到十秒，策划还没来得及在群里@你第四次。&lt;/p>
&lt;p>用前缀和阵法瞬杀了十万次群殴，用空间换了时间，又让 CPU 少烧了几亿次循环。你端起那杯从九点半端到现在终于能喝一口的咖啡，温度刚好——就像 O(m) 预处理加 O(1) 查询，一切都卡在刚好的复杂度上。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3754 幽灵数据净化器：逆序拆解不反序，边界兜底不判断</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-07/</link><pubDate>Tue, 07 Jul 2026 12:50:52 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-07/</guid><description>&lt;h2 id="幽灵数据净化器逆序拆解不反序边界兜底不判断">幽灵数据净化器：逆序拆解不反序，边界兜底不判断
&lt;/h2>&lt;p>你是一名 MMO 游戏的后端开发，今天凌晨，运维给你打了个紧急电话：&lt;/p>
&lt;blockquote>
&lt;p>“哥，数据库炸了。不是炸服的炸，是磁盘坏道——一批玩家的存档文件被物理污染了，里面混进了大量的 &lt;code>0&lt;/code> 值噪声！策划说新版本明天十点上线，这些玩家的进度必须在这之前修好，修不好他就要在全员群里@你，原话是‘提头来见’。”&lt;/p>&lt;/blockquote>
&lt;p>你揉了揉惺忪的睡眼，连上 VPN 打开日志，整个人瞬间清醒——不是因为冷，是血压。&lt;/p>
&lt;hr>
&lt;h5 id="事故现场损坏的存档长啥样">事故现场：损坏的存档长啥样？
&lt;/h5>&lt;p>正常的存档应该是这样，干干净净，像个刚建号的萌新：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;player_id&amp;#34;&lt;/span>: &lt;span style="color:#ae81ff">12345&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;level&amp;#34;&lt;/span>: &lt;span style="color:#ae81ff">99&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;gold&amp;#34;&lt;/span>: &lt;span style="color:#ae81ff">987654&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;equipment&amp;#34;&lt;/span>: [&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">2&lt;/span>, &lt;span style="color:#ae81ff">3&lt;/span>, &lt;span style="color:#ae81ff">4&lt;/span>, &lt;span style="color:#ae81ff">5&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>但因为那片该死的坏道，存档被“虚空寄生虫”咬了无数口，凡是咬过的地方都变成了 &lt;code>0&lt;/code>——像被格式化了半截：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;player_id&amp;#34;&lt;/span>: &lt;span style="color:#ae81ff">10203004&lt;/span>, &lt;span style="color:#75715e">// 原本是 1234，但混进了三个 0
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">&amp;#34;level&amp;#34;&lt;/span>: &lt;span style="color:#ae81ff">1000&lt;/span>, &lt;span style="color:#75715e">// 原本是 1，后面跟了三个幽灵 0
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">&amp;#34;gold&amp;#34;&lt;/span>: &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#75715e">// 全损。这个玩家的金币全被磁盘吃了。
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">&amp;#34;equipment&amp;#34;&lt;/span>: [&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">2&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">3&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">4&lt;/span>] &lt;span style="color:#75715e">// 装备 ID 被噪声打成了筛子
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>你的任务，就是把这场灾难的物理损失降到最低：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>数据清洗&lt;/strong>：干掉所有混进来的 &lt;code>0&lt;/code>——它们是磁盘坏道的遗物，是噪声，是必须被抹除的幽灵数据。&lt;/li>
&lt;li>&lt;strong>数据重组&lt;/strong>：把幸存下来的非零数字按原始顺序拼起来，形成新数字 &lt;code>x&lt;/code>。这是玩家真正的属性残片。&lt;/li>
&lt;li>&lt;strong>校验计算&lt;/strong>：算出 &lt;code>x&lt;/code> 的数字和 &lt;code>sum&lt;/code>，然后返回 &lt;code>x * sum&lt;/code> 作为“纯净校验码”，用来给修复后的存档贴上一个能验证完整性的标签。&lt;/li>
&lt;/ol>
&lt;p>如果整段数据全是 &lt;code>0&lt;/code>——比如&lt;code>&amp;quot;gold&amp;quot;: 0&lt;/code>——那说明彻底损坏，&lt;code>x = 0&lt;/code>，&lt;code>sum = 0&lt;/code>，返回 &lt;code>0&lt;/code>。翻译成人话就是：“这档没救了，给玩家发补偿钻石，删号重练。”&lt;/p>
&lt;hr>
&lt;h5 id="字符串流的诱惑">字符串流的诱惑
&lt;/h5>&lt;p>你盯着亚瑟那个被污染的 &lt;code>player_id&lt;/code>：&lt;code>10203004&lt;/code>，第一反应和所有赶时间的程序员一样——&lt;strong>字符串一把梭&lt;/strong>。&lt;/p>
&lt;blockquote>
&lt;p>“转成字符串，把 &lt;code>'0'&lt;/code> 全过滤掉，再转回整数，完事！”&lt;/p>&lt;/blockquote>
&lt;p>但你久经沙场的第六感在你敲下回车前拉响了警报——&lt;strong>全零存档&lt;/strong>。比如 &lt;code>&amp;quot;gold&amp;quot;: 0&lt;/code>，过滤完非零，字符串直接变成空串 &lt;code>''&lt;/code>。&lt;code>int('')&lt;/code> 会毫不犹豫地抛出一个 &lt;code>ValueError&lt;/code>，你的脚本当场炸成烟花，就像玩家在副本里把 Boss 打到 1% 血时游戏闪退一样令人抓狂。你不得不在外面裹一层 &lt;code>if-else&lt;/code> 兜底，优雅度直接打八折。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> n&lt;span style="color:#f92672">==&lt;/span>&lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#66d9ef">else&lt;/span> (x &lt;span style="color:#f92672">:=&lt;/span> int(&lt;span style="color:#e6db74">&amp;#39;&amp;#39;&lt;/span>&lt;span style="color:#f92672">.&lt;/span>join(d &lt;span style="color:#66d9ef">for&lt;/span> d &lt;span style="color:#f92672">in&lt;/span> str(n) &lt;span style="color:#66d9ef">if&lt;/span> d &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;0&amp;#39;&lt;/span>))) &lt;span style="color:#f92672">*&lt;/span> sum(int(d) &lt;span style="color:#66d9ef">for&lt;/span> d &lt;span style="color:#f92672">in&lt;/span> str(x))
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>海象运算符 &lt;code>:=&lt;/code>、生成器表达式、条件表达式，看起来非常 Pythonic。如果你只是想快速 AC，不在乎那点性能差距，这确实是 Python 玩家的&lt;strong>终极快乐形态&lt;/strong>。&lt;/p>
&lt;p>&lt;strong>但是！&lt;/strong> 作为一名有追求的开发，你知道在生产环境中，你可能需要处理&lt;strong>百万级玩家的批量存档修复&lt;/strong>。这时候，每一次字符串分配、每一次类型转换，都是在浪费 CPU 周期。&lt;/p>
&lt;p>能用平 A 解决的事，绝不多交一个大招。这是每个老玩家的职业素养。&lt;/p>
&lt;hr>
&lt;h5 id="纯数学流逐位拆解连招">&lt;strong>纯数学流·逐位拆解连招&lt;/strong>
&lt;/h5>&lt;p>你深吸一口气，决定用最硬核的方式来解决这个问题——&lt;strong>不依赖任何字符串库函数，纯靠算术运算&lt;/strong>。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">sumAndMultiply&lt;/span>(n: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> x, s, pow10 &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 三件初始装备：重组数/数字和/位权棒&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> n:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n, d &lt;span style="color:#f92672">=&lt;/span> divmod(n, &lt;span style="color:#ae81ff">10&lt;/span>) &lt;span style="color:#75715e"># 一刀切下最末位&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> d: &lt;span style="color:#75715e"># 非零？收编。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> x &lt;span style="color:#f92672">+=&lt;/span> pow10 &lt;span style="color:#f92672">*&lt;/span> d &lt;span style="color:#75715e"># 让d站到正确的十进制位上&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> s &lt;span style="color:#f92672">+=&lt;/span> d &lt;span style="color:#75715e"># 累加数字和&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pow10 &lt;span style="color:#f92672">*=&lt;/span> &lt;span style="color:#ae81ff">10&lt;/span> &lt;span style="color:#75715e"># 位权左移，给下一位腾位置&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> x &lt;span style="color:#f92672">*&lt;/span> s &lt;span style="color:#75715e"># 纯净校验码&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>divmod(n, 10)——双刀流&lt;/strong>：一次运算同时拿到商和余数。在 CPython 底层，&lt;code>divmod&lt;/code> 被编译成单条 CPU 指令，比分两行写 &lt;code>n // 10&lt;/code> 和 &lt;code>n % 10&lt;/code> 更快，而且语义干净利落：“把这个数给我拆成头和尾。”&lt;/p>
&lt;p>&lt;strong>pow10——动态位权棒&lt;/strong>：因为是从个位往高位拆，先剥出来的 &lt;code>d&lt;/code> 其实是原数字的末位，直接拼会反序。&lt;code>pow10&lt;/code> 像个不断左移的阶梯：个位数站 &lt;code>1&lt;/code> 的阶梯上，十位数站 &lt;code>10&lt;/code> 的阶梯上，百位数站 &lt;code>100&lt;/code> 的阶梯上……无论你从哪头拆，最终的 &lt;code>x&lt;/code> 都保持着原始顺序。这是纯数学流最精妙的设定——逆序拆解，正序重生。&lt;/p>
&lt;p>&lt;strong>边界自动兜底&lt;/strong>：如果 &lt;code>n = 0&lt;/code>，&lt;code>while&lt;/code> 循环一次都不执行，&lt;code>x = 0&lt;/code>，&lt;code>s = 0&lt;/code>，返回 &lt;code>0&lt;/code>。不需要任何额外的 &lt;code>if-else&lt;/code> 判断——逻辑本身就覆盖了全损存档的情况。符合你一贯朴素的错误处理哲学：&lt;strong>最好的防御，是让漏洞根本不存在。&lt;/strong>&lt;/p>
&lt;p>时间复杂度：O(log n)。一个十进制数 &lt;code>n&lt;/code> 大约有 &lt;code>log₁₀ n&lt;/code> 位，只遍历一次，每位常数时间操作。就算 &lt;code>n&lt;/code> 顶到 &lt;code>10⁹&lt;/code>，循环也才跑十次。空间复杂度：&lt;code>O(1)&lt;/code>，全程只用了三个变量 &lt;code>x&lt;/code>、&lt;code>s&lt;/code>、&lt;code>pow10&lt;/code>。&lt;/p>
&lt;p>全程零字符串、零类型转换。没有临时字符串对象在堆里蹦迪，没有 &lt;code>str&lt;/code> 与 &lt;code>int&lt;/code> 之间的反复横跳，一切计算都在整数域内完成。处理百万级存档时，内存分配次数从“每条三次”直接归零，CPU 缓存命中率高得就像开了自瞄挂。&lt;/p>
&lt;p>更骚的是，策划明天如果要改需求——“把过滤条件换成排除偶数”，你改一行：&lt;code>if d % 2 == 1&lt;/code>。“还要顺便算个平均值”，加个计数器。“支持十六进制存档”，把 &lt;code>10&lt;/code> 改成 &lt;code>16&lt;/code> 就完事。纯数学流的架构就像一把能自由换刀片的美工刀，换个刀头只需要拧一下，绝不伤筋动骨。&lt;/p>
&lt;hr>
&lt;h5 id="进阶预告leetcode-3756-ii-版本-的海量查询挑战">进阶预告：LeetCode 3756 (II 版本) 的海量查询挑战**
&lt;/h5>&lt;p>你刚把亚瑟的存档修好，运维的消息又弹出来了，这次带着一种“我知道你要骂我但我还是要说”的语气：&lt;/p>
&lt;p>“哥，干得漂亮！但策划刚紧急通知——在修复之前，先对所有损坏数据的各个子串区间做一波批量校验。他要根据每个玩家不同部位的损坏程度，决定补偿钻石的档位。十万个查询区间，五分钟后给结果。”&lt;/p>
&lt;p>你盯着屏幕，沉默了三秒。当查询多到能碾碎 CPU 时，这场战斗已经从“单挑”升级成了“群殴”——如果每次查询都从头拆一遍数字，&lt;code>O(Q × log N)&lt;/code>，&lt;code>Q = 10⁵&lt;/code>，`N = 10⁵——等于用平 A 去打十万只怪，手速再快也要打到地老天荒。&lt;/p>
&lt;p>你需要一个更强大的方案：&lt;strong>前缀和预炼化&lt;/strong>。把整条原始数据提前预处理，让每次查询不用从头拆，而是直接取现成的结果——信条只有八个字：&lt;strong>提前炼化，查询瞬发。&lt;/strong>&lt;/p>
&lt;p>你把亚瑟的存档文件往旁边一推，平 A 已经不够用了，是时候换一套群体输出了。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode1288 一刀流·区间清理术：排序定乾坤，贪心斩冗余</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-06/</link><pubDate>Mon, 06 Jul 2026 10:42:26 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-06/</guid><description>&lt;h2 id="一刀流区间清理术排序定乾坤贪心斩冗余">🎮一刀流·区间清理术：排序定乾坤，贪心斩冗余
&lt;/h2>&lt;p>你是一名区间猎人，猎头公会甩给你一张悬赏令：&lt;strong>清理一批“地图上的套娃区间”&lt;/strong>。&lt;/p>
&lt;p>什么叫套娃区间？就是那些被其他区间完全罩住的倒霉蛋——比如 &lt;code>[3,6)&lt;/code> 被 &lt;code>[2,8)&lt;/code> 包得像俄罗斯套娃里的那个小娃娃，存在的唯一价值，就是被外面的老大哥代表。这种区间在地图里纯属“占着茅坑不拉屎”——明明霸着一组坐标，实际控制面积为零，纯粹浪费数据空间。不清理掉的话，策划又要开喷了：“你们的边界数据比我昨晚吃的麻辣烫还杂乱！”&lt;/p>
&lt;p>你的任务很直白：&lt;strong>数数清理完还剩多少个“独当一面的有效区间”&lt;/strong>。注意，不是让你真的一刀一刀删——数组这玩意儿，中间删一个元素，后面的编号全得往前挪一位，复杂度堪比主城开服时玩家挤传送阵。你只需要优雅地统计出“幸存者”的数量，然后把悬赏令拍回公会的桌子上，领赏走人。&lt;/p>
&lt;p>但问题来了：你怎么知道哪个区间被当了套娃？暴力两两对比？拿每个区间去和全图所有区间单挑？&lt;code>O(n²)&lt;/code> 的复杂度会让你的CPU 会觉得你在羞辱它。&lt;/p>
&lt;p>你冷笑一声，从背后抽出一把泛着幽蓝暗光的太刀——&lt;strong>排序之刃&lt;/strong>。刀身上刻着一行附魔铭文：&lt;/p>
&lt;blockquote>
&lt;pre tabindex="0">&lt;code>lambda x: (x[0], -x[1])
&lt;/code>&lt;/pre>&lt;/blockquote>
&lt;p>这把刀的排序法则简单粗暴：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>主属性&lt;/strong>：按左端点升序排——谁左边起步更早，谁站前面。江湖规矩，先到先得。&lt;/li>
&lt;li>&lt;strong>副属性&lt;/strong>：如果左端点撞车了，按右端点降序排——谁地盘更大、谁罩得更远，谁就当大哥，站C位。&lt;/li>
&lt;/ul>
&lt;p>队友从后排探出脑袋：“哥，为啥右端点要降序？升序不是也挺整齐？我这强迫症看得浑身刺挠！”&lt;/p>
&lt;p>你手腕一翻，刀锋在空中划出一道银弧，映出你半张冷峻的脸。你淡淡地吐出几个字：&lt;/p>
&lt;p>“为了让罩得最远的大哥先出场。大哥站前面，后面那些小弟一旦被大哥的地盘完全包住，我连刀都不用抬，看一眼就知道它是套娃。装逼要装全套，排序也要有兵法——这叫先发制人，后发制于人。”&lt;/p>
&lt;hr>
&lt;p>你拔出排序之刃，刀尖指天，念出一段古老的咒语：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>intervals&lt;span style="color:#f92672">.&lt;/span>sort(key&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">lambda&lt;/span> x: (x[&lt;span style="color:#ae81ff">0&lt;/span>], &lt;span style="color:#f92672">-&lt;/span>x[&lt;span style="color:#ae81ff">1&lt;/span>]))
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>一瞬间，整个地图开始剧烈震动。左端点小的区间像被磁铁吸住一样，连滚带爬地挤向队伍前排；左端点撞车的区间们则开始了残酷的内卷——右端点大的冲击 C 位，右端点小的被动回退。&lt;/p>
&lt;p>你看着这支被咒语驯服的队伍，嘴角微微上扬：&lt;strong>猎人的工作，从列队完毕那一刻起，就已经结束了九成。&lt;/strong> 剩下的一成，不过是走个过场，收个人头而已。&lt;/p>
&lt;p>你提着排序之刃，刀尖拖在石板地面上，迸出呲呲的火星，像一场即将开场的屠杀预告片。你把左手摊开，掌心浮现出一个变量 &lt;code>max_right&lt;/code>，初始值为 &lt;code>0&lt;/code>——它记录的是你这一路走来，刀锋扫过的最远右疆界。&lt;/p>
&lt;p>你迈开步子，从队头走到队尾，每经过一个区间 &lt;code>[a, b)&lt;/code>，只问一个问题：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>“你的右边界 b，有没有超过我这把刀至今扫过的极限？”&lt;/strong>&lt;/p>&lt;/blockquote>
&lt;p>一问一斩，绝无多余动作。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>情况一：b ≤ max_right&lt;/strong>
这厮的右边界没有突破前人留下的记录——左边已经被大哥盖住（排序铁律之下，它的左端点不可能比前辈更靠左，顶多打个平手），右边又被大哥的疆域完全碾压——从头到脚，无一处突围。教科书级别的套娃，连挣扎的资格都不配拥有。不计数，下一个。&lt;/li>
&lt;li>&lt;strong>情况二：b &amp;gt; max_right&lt;/strong>
嚯！这小子的右边界炸出了新高，硬生生把已知疆土撕开一道缺口！哪怕它的左边界还是被某个大哥压制，至少右边这一段是独享的，是旁人够不着的“独家战区”。这不是套娃，这是开拓者，是配得上被计数的高手。你眼中寒光微闪，幸存者计数 &lt;code>+1&lt;/code>，同时腕部一转，刀锋向前劈出一道半月弧光——&lt;code>max_right&lt;/code> 应声刷新为 &lt;code>b&lt;/code>，地图上烙下一道崭新的疆界线。&lt;/li>
&lt;/ul>
&lt;p>你就这样从头走到尾，一步不停，一眼不回头看。排序之刃帮你压死了左边界——后来者永远不可能比前辈站得更靠左。&lt;code>max_right&lt;/code> 帮你盯死了右边界——只要后辈的右边界没超过前辈的最远记录，必被吞噬。双刃合璧，一刀封喉，套娃无所遁形。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">removeCoveredIntervals&lt;/span>(intervals: List[List[int]]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> intervals&lt;span style="color:#f92672">.&lt;/span>sort(key&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">lambda&lt;/span> x: (x[&lt;span style="color:#ae81ff">0&lt;/span>], &lt;span style="color:#f92672">-&lt;/span>x[&lt;span style="color:#ae81ff">1&lt;/span>]))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max_right &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(len(intervals)):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> max_right &lt;span style="color:#f92672">&amp;lt;&lt;/span> intervals[i][&lt;span style="color:#ae81ff">1&lt;/span>]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max_right &lt;span style="color:#f92672">=&lt;/span> intervals[i][&lt;span style="color:#ae81ff">1&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> res
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>一套刀法行云流水，遍历 O(n)，叠加上排序咒语的 O(n log n)，总花费让隔壁还在硬莽双重循环的剑圣羞愧到想删号。至于空间？除排序本身，全程只祭出两个变量——&lt;code>res&lt;/code> 与 &lt;code>max_right&lt;/code>，堪称环保猎人的典范。&lt;/p>
&lt;p>队友在语音频道里惊呼：“卧槽这就完了？你这输出是开了倍速挂还是我电脑卡了？”&lt;/p>
&lt;p>你扛起排序之刃，刀身上的附魔铭文在暗处微微脉动。那些曾在地图数据里混吃等死的套娃区间，已被从逻辑上彻底抹除——连删除的痕迹都没留下，仿佛它们从一开始就没在这张地图上刷新过。&lt;/p>
&lt;p>你头也不回，步入公会大厅的阴影深处，只抛下一句话，冷得像刀锋入鞘的最后一声轻响：&lt;/p>
&lt;p>“猎人不打无谓之猎，套娃不配占用 CPU。”&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode1310 两种DP，一把神装</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-05/</link><pubDate>Sun, 05 Jul 2026 20:02:23 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-05/</guid><description>&lt;h2 id="两种dp一把神装">&lt;strong>🎮两种DP，一把神装&lt;/strong>
&lt;/h2>&lt;p>你，一位身披重甲的骑士，站在城堡的&lt;strong>右下角&lt;/strong>（别问为什么不在正门，问就是策划设计的）。城堡的最深处——&lt;strong>左上角&lt;/strong>，藏着一把传说中的武器：“全服第一·金光闪闪·一刀999·屠龙宝刀”。装备就是尊严，皮肤就是战力，这把刀要是落入别人手里，你在服务器里的排名就得从“榜一大哥”变成“榜外小弟”。&lt;/p>
&lt;p>但这座城堡有个变态设定：&lt;strong>每个房间要么堆着金币（数字 1~9），要么蹲着一只打呼噜的精英怪（障碍 X），而且你的重甲只允许三个移动方向——向上、向左、向左上角斜走。&lt;/strong> 向下？不存在的，骑士的尊严不允许走回头路，况且甲胄太重，下楼梯膝盖受不了。&lt;/p>
&lt;p>你的目标非常清晰：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>收集最多的金币&lt;/strong>——买不起屠龙宝刀的皮肤，你拿什么在队友面前装逼？&lt;/li>
&lt;li>&lt;strong>统计有多少种走法&lt;/strong>能刚好拿到这么多金币——万一队友问“你怎么来的”，你可以装作风轻云淡：“路线不多，也就几十种，我都算过了。”&lt;/li>
&lt;/ol>
&lt;p>如果根本走不到藏宝室……那就 &lt;code>[0, 0]&lt;/code> 灰溜溜回城，看着世界频道刷屏：“恭喜玩家【隔壁剑圣】获得屠龙宝刀！”&lt;/p>
&lt;hr>
&lt;h5 id="动态规划双雄一把刀两种姿势">动态规划双雄：一把刀，两种姿势
&lt;/h5>&lt;p>你站在起点，握紧了手中的剑。作为全服排名前三的骑士，你深知拿到这把刀的方式不止一种。今天，你打算用两种姿势征服这座城堡——先&lt;strong>用商人的精算推演全局&lt;/strong>，再&lt;strong>用猛男的头铁正面硬刚&lt;/strong>。一种姿势算胜负，两种姿势定乾坤。队友在语音里问：“你行不行啊？”你冷笑一声，回了两个字：“看号。”&lt;/p>
&lt;hr>
&lt;h5 id="姿势一后向-dp商人骑士的沙盘推演">姿势一：后向 DP——商人骑士的沙盘推演
&lt;/h5>&lt;p>“别急着往前冲，先想想终点在哪儿。”&lt;/p>
&lt;p>你没有急着冲锋，而是把剑往地上一插，盘腿坐下，从背包里掏出一张泛黄的羊皮卷轴。卷轴封面写着四个字：&lt;strong>“逆向演算”&lt;/strong>。&lt;/p>
&lt;p>这个姿势的精髓是：&lt;strong>假装你已经站在藏宝室门口了，然后倒着走回起点，沿途在每块地砖上记账。&lt;/strong> 定义 &lt;code>dp[i][j] = [maxGold, count]&lt;/code>，意思是“从 &lt;code>(i,j)&lt;/code> 出发走到藏宝室，最多能捞多少金币，以及有几条路能捞到这个数”。&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>藏宝室 E&lt;/strong>：你就站在终点，还没拔刀，口袋里零金币进账（&lt;code>E&lt;/code> 本身不掉落），最大金币 = 0，方案数 = 1。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>精英怪房间 X&lt;/strong>：绕行，绕行！标记为不可达，金币 = -∞，方案数 = 0。谁拉怪谁负责，商人只打必赢的团。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>普通金币房&lt;/strong>：你回头看——因为是倒着推，下一步其实是正向走法的“上一步”，也就是&lt;strong>下方、右方、右下&lt;/strong>三个方向。扫一眼这三个邻居的账本，挑出 &lt;code>maxGold&lt;/code> 最大的那个，把方案数抄过来；如果有多个邻居并列最大，说明多条路都能让你捞到同样的最大金币，就把它们的方案数&lt;strong>累加&lt;/strong>起来。&lt;/p>
&lt;p>然后，弯腰把当前房间地板缝里的金币（数字）抠出来加上——商人连一个铜板都不放过，这是职业素养。&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>当你倒推回起点 &lt;code>S&lt;/code> 时，账本上赫然写着两个数字：最大金币数，和对应的路线方案数。如果最大金币还是 -∞，那说明连藏宝室的门都摸不到——不过全服前三的骑士，不存在这种情况。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">pathsWithMaxScore&lt;/span>(board):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MOD &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">10&lt;/span>&lt;span style="color:#f92672">**&lt;/span>&lt;span style="color:#ae81ff">9&lt;/span> &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">7&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n &lt;span style="color:#f92672">=&lt;/span> len(board)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> board[n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>][n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#39;X&amp;#39;&lt;/span> &lt;span style="color:#f92672">or&lt;/span> board[&lt;span style="color:#ae81ff">0&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#39;X&amp;#39;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># dp[i][j] = [max_score, count]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dp &lt;span style="color:#f92672">=&lt;/span> [[[float(&lt;span style="color:#e6db74">&amp;#39;-inf&amp;#39;&lt;/span>), &lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n)] &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dp[&lt;span style="color:#ae81ff">0&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#75715e"># 站在藏宝室门口，不走路不捡钱，一种站姿&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 倒推的三个方向：下、右、右下&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dirs &lt;span style="color:#f92672">=&lt;/span> [(&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>), (&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">1&lt;/span>), (&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">1&lt;/span>)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(n):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(n):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> i &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">and&lt;/span> j &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> board[i][j] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#39;X&amp;#39;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max_score &lt;span style="color:#f92672">=&lt;/span> float(&lt;span style="color:#e6db74">&amp;#39;-inf&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> total_cnt &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> di, dj &lt;span style="color:#f92672">in&lt;/span> dirs:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pi, pj &lt;span style="color:#f92672">=&lt;/span> i &lt;span style="color:#f92672">-&lt;/span> di, j &lt;span style="color:#f92672">-&lt;/span> dj &lt;span style="color:#75715e"># 回头看是谁走过来的&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">&amp;lt;=&lt;/span> pi &lt;span style="color:#f92672">&amp;lt;&lt;/span> n &lt;span style="color:#f92672">and&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">&amp;lt;=&lt;/span> pj &lt;span style="color:#f92672">&amp;lt;&lt;/span> n &lt;span style="color:#f92672">and&lt;/span> board[pi][pj] &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;X&amp;#39;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> score, cnt &lt;span style="color:#f92672">=&lt;/span> dp[pi][pj]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> score &lt;span style="color:#f92672">&amp;gt;&lt;/span> max_score:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max_score &lt;span style="color:#f92672">=&lt;/span> score
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> total_cnt &lt;span style="color:#f92672">=&lt;/span> cnt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> score &lt;span style="color:#f92672">==&lt;/span> max_score:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> total_cnt &lt;span style="color:#f92672">=&lt;/span> (total_cnt &lt;span style="color:#f92672">+&lt;/span> cnt) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> max_score &lt;span style="color:#f92672">!=&lt;/span> float(&lt;span style="color:#e6db74">&amp;#39;-inf&amp;#39;&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> add &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> board[i][j] &lt;span style="color:#f92672">not&lt;/span> &lt;span style="color:#f92672">in&lt;/span> (&lt;span style="color:#e6db74">&amp;#39;E&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;S&amp;#39;&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> add &lt;span style="color:#f92672">=&lt;/span> int(board[i][j])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dp[i][j] &lt;span style="color:#f92672">=&lt;/span> [max_score &lt;span style="color:#f92672">+&lt;/span> add, total_cnt]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res &lt;span style="color:#f92672">=&lt;/span> dp[n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>][n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> [res[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#66d9ef">if&lt;/span> res[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">!=&lt;/span> float(&lt;span style="color:#e6db74">&amp;#39;-inf&amp;#39;&lt;/span>) &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>, res[&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">%&lt;/span> MOD]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>你合上卷轴，拔出插在地上的剑。商人姿势的精髓在于：&lt;strong>不动一刀一枪，胜负已在指尖。&lt;/strong> 队友在语音里沉默了——这哥们坐地上画了张图，就把路线全算完了？&lt;/p>
&lt;hr>
&lt;h5 id="姿势二前向-dp头铁猛男的正面碾压">姿势二：前向 DP——头铁猛男的正面碾压
&lt;/h5>&lt;p>“管他那么多，先冲再说！”&lt;/p>
&lt;p>你把卷轴塞回背包，活动了一下肩胛骨，盔甲发出咔咔的金属摩擦声。“算完了？算完了就该开怪了。”&lt;/p>
&lt;p>第二个姿势，是你最擅长的：&lt;strong>正面冲锋，见一个房间踹一扇门，边跑边记。&lt;/strong> 这个姿势的哲学是：“路线是跑出来的，金币是捡出来的，骑士的功勋写在前胸，不在账本！”&lt;/p>
&lt;p>定义 &lt;code>dp[i][j] = [maxGold, count]&lt;/code>，意思是“从起点冲到这里，最多已经捞了多少金币，以及有几条路能捞到这个数”。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>起点 S&lt;/strong>：你就站在这儿，还没开冲，口袋零金币，方案数 = 1——先原地吼一个 Buff，气势不能输。&lt;/li>
&lt;li>&lt;strong>精英怪房间 X&lt;/strong>：直接跳过！猛男虽然头铁，但还没铁到单挑睡着的精英怪——万一吵醒了团灭，队友要喷你。&lt;/li>
&lt;li>&lt;strong>普通金币房&lt;/strong>：你一脚踹开门，先看一眼是从哪个方向冲来的——因为正向只能向上、向左、向左上走，所以到达 &lt;code>(i,j)&lt;/code> 的上一个房间必然是它的&lt;strong>上方、左方、左上方&lt;/strong>三个之一。从这三个方向里挑出 &lt;code>maxGold&lt;/code> 最大的那个，把方案数搬过来（并列最大就累加）。然后低头一看，地板上有金币？弯腰，捡，一气呵成。动作快得像开了自动拾取，猛男捡钱从不墨迹。&lt;/li>
&lt;/ul>
&lt;p>当你一脚踹开藏宝室的大门时，口袋里的金币叮当乱响，手里的路线图已经被汗水和激情浸透。你低头一看——嘿，和刚才商人姿势算出来的数字一模一样！&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">pathsWithMaxScore&lt;/span>(board):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MOD &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">10&lt;/span>&lt;span style="color:#f92672">**&lt;/span>&lt;span style="color:#ae81ff">9&lt;/span> &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">7&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n &lt;span style="color:#f92672">=&lt;/span> len(board)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> board[n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>][n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#39;X&amp;#39;&lt;/span> &lt;span style="color:#f92672">or&lt;/span> board[&lt;span style="color:#ae81ff">0&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#39;X&amp;#39;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dp &lt;span style="color:#f92672">=&lt;/span> [[[float(&lt;span style="color:#e6db74">&amp;#39;-inf&amp;#39;&lt;/span>), &lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n)] &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dp[n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>][n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#75715e"># 站在起点，还没冲，零金币，一种站姿&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 前推方向：上、左、左上&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dirs &lt;span style="color:#f92672">=&lt;/span> [(&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>), (&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>), (&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> i &lt;span style="color:#f92672">==&lt;/span> n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">and&lt;/span> j &lt;span style="color:#f92672">==&lt;/span> n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> board[i][j] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#39;X&amp;#39;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max_score &lt;span style="color:#f92672">=&lt;/span> float(&lt;span style="color:#e6db74">&amp;#39;-inf&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> total_cnt &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> di, dj &lt;span style="color:#f92672">in&lt;/span> dirs:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pi, pj &lt;span style="color:#f92672">=&lt;/span> i &lt;span style="color:#f92672">-&lt;/span> di, j &lt;span style="color:#f92672">-&lt;/span> dj &lt;span style="color:#75715e"># 回头看：谁来的&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">&amp;lt;=&lt;/span> pi &lt;span style="color:#f92672">&amp;lt;&lt;/span> n &lt;span style="color:#f92672">and&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">&amp;lt;=&lt;/span> pj &lt;span style="color:#f92672">&amp;lt;&lt;/span> n &lt;span style="color:#f92672">and&lt;/span> board[pi][pj] &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;X&amp;#39;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> score, cnt &lt;span style="color:#f92672">=&lt;/span> dp[pi][pj]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> score &lt;span style="color:#f92672">&amp;gt;&lt;/span> max_score:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max_score &lt;span style="color:#f92672">=&lt;/span> score
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> total_cnt &lt;span style="color:#f92672">=&lt;/span> cnt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> score &lt;span style="color:#f92672">==&lt;/span> max_score:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> total_cnt &lt;span style="color:#f92672">=&lt;/span> (total_cnt &lt;span style="color:#f92672">+&lt;/span> cnt) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> max_score &lt;span style="color:#f92672">!=&lt;/span> float(&lt;span style="color:#e6db74">&amp;#39;-inf&amp;#39;&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> add &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> board[i][j] &lt;span style="color:#f92672">not&lt;/span> &lt;span style="color:#f92672">in&lt;/span> (&lt;span style="color:#e6db74">&amp;#39;E&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;S&amp;#39;&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> add &lt;span style="color:#f92672">=&lt;/span> int(board[i][j])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dp[i][j] &lt;span style="color:#f92672">=&lt;/span> [max_score &lt;span style="color:#f92672">+&lt;/span> add, total_cnt]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res &lt;span style="color:#f92672">=&lt;/span> dp[&lt;span style="color:#ae81ff">0&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> [res[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#66d9ef">if&lt;/span> res[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">!=&lt;/span> float(&lt;span style="color:#e6db74">&amp;#39;-inf&amp;#39;&lt;/span>) &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>, res[&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">%&lt;/span> MOD]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>你站在藏宝室中央，屠龙宝刀插在你面前的地砖里，金光把整个房间照得像开了 HDR。你的左手攥着金币袋，右手握着路线图，两个姿势给出的数字严丝合缝。&lt;/p>
&lt;hr>
&lt;h5 id="尾声">尾声：
&lt;/h5>&lt;p>你拔出屠龙宝刀，刀身发出嗡嗡的鸣响，世界频道弹出全服公告：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>【系统公告】恭喜玩家“DP骑士”获得【屠龙宝刀】，服务器首杀！&lt;/strong>&lt;/p>&lt;/blockquote>
&lt;p>队友在语音里炸了锅：“卧槽牛逼！你到底是算的还是莽的？”&lt;/p>
&lt;p>你扛着刀走出城堡，眼眶微湿——因为宝刀的金光有点刺眼。你淡淡地回了句：“两种姿势，一种答案。双 DP 通关，不服来辩。”&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode2492 图鉴更新：不找路径，只扫连通块，并查集一键通关</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-04/</link><pubDate>Sat, 04 Jul 2026 20:22:54 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-04/</guid><description>&lt;h2 id="图鉴更新不找路径只扫连通块并查集一键通关">&lt;strong>🎮图鉴更新：不找路径，只扫连通块，并查集一键通关&lt;/strong>
&lt;/h2>&lt;p>你是一名资深运维，信奉&amp;quot;预判风险就是节约生命&amp;quot;，技能树上最亮眼的天赋就是在服务器崩溃前把锅甩出去。这天运维小哥丢来一个任务：从&lt;strong>1 号测试机&lt;/strong>，一路 SSH 到&lt;strong>n 号生产机&lt;/strong>，中间跳板机的安全系数 &lt;code>distance&lt;/code> 一个比一个离谱——数字越小越刺激，敲个回车就可能蓝屏、宕机、主板升天。整条链路的安全性由最烂那台跳板机决定（木桶效应，也是生产事故的第一块多米诺），崩了你会被截图挂上 Slack &lt;code>#incident-recap&lt;/code> 频道循环处刑。&lt;/p>
&lt;p>你的任务说白了就是：&lt;strong>所有可能跳转路径里，最坏能踩到多烂的跳板机&lt;/strong>——也就是最小安全系数的&lt;strong>最小值&lt;/strong>。为什么求下限？因为甩锅要趁早。只要你摸清全网最脆的那台机器，就能理直气壮输出：&lt;/p>
&lt;blockquote>
&lt;p>“团队，本次路径不可避免触碰了安全系数 3 的遗产服务器，已连续运行 999 天，属不可抗力……”&lt;/p>&lt;/blockquote>
&lt;p>你甚至故意绕远路去踩它——毕竟策划给了你一个 BUG 级技能：&lt;strong>可以反复走同一条路、也可以多次到达同一节点&lt;/strong>。这意味着你可以像刷副本一样，把整张图的边全踩一遍，攒满“烂机器证据库”再晃悠到终点。只要 1 号和 n 号服务器连通，你总能溜达到那个最脆节点上——这不是冒险，这是战略性踩雷。&lt;/p>
&lt;p>这压根不是最短路，是考连通性：&lt;strong>1 和 n 所在连通块内所有边的最小值&lt;/strong>。&lt;/p>
&lt;p>你邪魅一笑，从工具箱抽出两件神装，准备展开这场甩锅之旅。&lt;/p>
&lt;hr>
&lt;h5 id="dfs">DFS
&lt;/h5>&lt;p>从 1 号机出发，把能连上的跳板机全踩一遍，顺便记下见过的最小安全系数。&lt;/p>
&lt;p>&lt;strong>递归 DFS&lt;/strong>：实现起来干净利落，像一把匕首。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">minScore&lt;/span>(n: int, roads: List[List[int]]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 无向图，双向建边&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g &lt;span style="color:#f92672">=&lt;/span> [[] &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> u, v, cost &lt;span style="color:#f92672">in&lt;/span> roads:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g[u]&lt;span style="color:#f92672">.&lt;/span>append((v, cost))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g[v]&lt;span style="color:#f92672">.&lt;/span>append((u, cost))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> vis &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#66d9ef">False&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (n &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> float(&lt;span style="color:#e6db74">&amp;#39;inf&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 递归 DFS：一条路走到黑，撞墙再回溯&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">dfs&lt;/span>(u: int):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">nonlocal&lt;/span> ans
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> vis[u] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> v, cost &lt;span style="color:#f92672">in&lt;/span> g[u]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> min(ans, cost)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">not&lt;/span> vis[v]: &lt;span style="color:#75715e"># 如果下一个 v 还没访问过&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dfs(v) &lt;span style="color:#75715e"># 递归访问 v（系统会自动把返回地址压入调用栈）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dfs(&lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#75715e"># 从 1 号机出发，开始踩点&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>但递归匕首有个致命弱点：跳板机太多→调用栈太深→&lt;code>RecursionError&lt;/code>，锅还没甩出去自己先炸了，留下满地碎片成为第二天晨会的素材：“看，这就是那个写出无限递归的天才。”&lt;/p>
&lt;p>成熟运维当然有&lt;strong>栈式防炸甲&lt;/strong>——手动模拟递归，把系统调用栈换成自己维护的列表栈，稳如老狗。&lt;/p>
&lt;p>&lt;strong>迭代 DFS&lt;/strong>：和递归版逻辑完全一样，只是把&amp;quot;系统自动压栈&amp;quot;换成了&amp;quot;手动 append/pop&amp;quot;。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">minScore&lt;/span>(n: int, roads: List[List[int]]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g &lt;span style="color:#f92672">=&lt;/span> [[] &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> u, v, cost &lt;span style="color:#f92672">in&lt;/span> roads:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g[u]&lt;span style="color:#f92672">.&lt;/span>append((v, cost))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g[v]&lt;span style="color:#f92672">.&lt;/span>append((u, cost))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> vis &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#66d9ef">False&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (n &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> float(&lt;span style="color:#e6db74">&amp;#39;inf&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 迭代 DFS：手动栈，稳如老狗&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> stack &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#75715e"># 手动栈，从 1 号服务器开始&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> vis[&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span> &lt;span style="color:#75715e"># 标记起点已访问，到此一游&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> stack:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> u &lt;span style="color:#f92672">=&lt;/span> stack&lt;span style="color:#f92672">.&lt;/span>pop()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> v, cost &lt;span style="color:#f92672">in&lt;/span> g[u]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> min(ans, cost)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">not&lt;/span> vis[v]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> vis[v] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> stack&lt;span style="color:#f92672">.&lt;/span>append(v)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>**两种 DFS 本质相同：**遍历 1 号机所在的连通分量，沿途捡最小的边。时间 &lt;code>O(n+m)&lt;/code>，空间 &lt;code>O(n+m)&lt;/code>。递归版优雅简洁，像个精炼的甩锅话术；迭代版稳健可控，像个详细的甩锅邮件。&lt;/p>
&lt;p>但你的强迫症开始隐隐作痛：为了找一个连通块的最小边，居然要把所有节点和边都扫一遍？这种“战略性踩雷”的脚本，是不是有点太敬业了？&lt;/p>
&lt;p>你重新审视手里的底牌。策划给了你三个关键信息：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>可以重复走&lt;/strong> → 不需要关心具体路径，只关心连通分量&lt;/li>
&lt;li>&lt;strong>求最小边权&lt;/strong> → 这是整个连通分量的集体属性，完全可以边建边维护&lt;/li>
&lt;li>&lt;strong>单次查询&lt;/strong> → 只问 1 和 n 所在连通块的最小值，不问过程，不问路径&lt;/li>
&lt;/ol>
&lt;p>这三条信息指向同一个结论：你要的不过是 &lt;strong>1 号和 n 号服务器所在连通分量里的最小边权&lt;/strong>。而能直接维护连通分量、顺手还能记个最小值的，正是工具箱底层压着的另一件神器——&lt;strong>并查集&lt;/strong>。它天生为连通性而生，连图都不用完整建，边读边合并，账本随时可查。&lt;/p>
&lt;hr>
&lt;h5 id="并查集连通性的天选之子">并查集——连通性的天选之子
&lt;/h5>&lt;p>真正的甩锅大师，从来不止一手准备。&lt;/p>
&lt;p>并查集天生就是用来维护连通分量的神器——它能把整张图压缩成若干个集合，每个集合的根节点手里攥着该集合的集体属性。查询时只需 &lt;code>find(1)&lt;/code>，瞬间拿到答案——不用遍历、不回溯、不废话，查询 O(α(n)) ≈ O(1)，甩锅 O(∞)。&lt;/p>
&lt;p>你只需要三步：&lt;/p>
&lt;ol>
&lt;li>初始化：每个节点自立门户，&lt;code>parent[i]=i&lt;/code>，&lt;code>min_cost[i]=∞&lt;/code>&lt;/li>
&lt;li>合并：遍历所有道路，把相连的节点合并到同一集合，顺手更新该集合的最小安全值&lt;/li>
&lt;li>查询：返回节点 1 所在集合的最小安全值，就是答案&lt;/li>
&lt;/ol>
&lt;p>这招完全绕开了显式的图遍历，直接把问题降维成&amp;quot;集合合并 + 最小值维护&amp;quot;。&lt;/p>
&lt;p>&lt;strong>基础版并查集&lt;/strong>：&lt;/p>
&lt;p>一个 &lt;code>parent&lt;/code> 数组、一个 &lt;code>min_cost&lt;/code> 数组，合并时随便把一方挂在另一方下面（谁当爹都行），查找时带路径压缩（把沿途节点全挂到根下面，下次一查就中）。时间复杂度近似 &lt;code>O(m α(n))&lt;/code>，空间 &lt;code>O(n)&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 并查集&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">UnionFind&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> __init__(self, n):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>parent &lt;span style="color:#f92672">=&lt;/span> list(range(n)) &lt;span style="color:#75715e"># parent[i] 表示节点i的父节点&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>min_cost &lt;span style="color:#f92672">=&lt;/span> [float(&lt;span style="color:#e6db74">&amp;#39;inf&amp;#39;&lt;/span>)] &lt;span style="color:#f92672">*&lt;/span> n &lt;span style="color:#75715e"># min_cost[i] 表示节点i所在集合的最小边权&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">find&lt;/span>(self, x):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>parent[x] &lt;span style="color:#f92672">!=&lt;/span> x:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>parent[x] &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>find(self&lt;span style="color:#f92672">.&lt;/span>parent[x]) &lt;span style="color:#75715e"># 路径压缩&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>parent[x]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">union&lt;/span>(self, x, y, cost):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;合并 x 和 y 所在的集合，同时更新最小边权&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> px, py &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>find(x), self&lt;span style="color:#f92672">.&lt;/span>find(y)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> px &lt;span style="color:#f92672">!=&lt;/span> py:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>parent[px] &lt;span style="color:#f92672">=&lt;/span> py
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>min_cost[py] &lt;span style="color:#f92672">=&lt;/span> min(self&lt;span style="color:#f92672">.&lt;/span>min_cost[py], self&lt;span style="color:#f92672">.&lt;/span>min_cost[px], cost)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>min_cost[px] &lt;span style="color:#f92672">=&lt;/span> min(self&lt;span style="color:#f92672">.&lt;/span>min_cost[px], cost)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Solution&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">minScore&lt;/span>(self, n: int, roads: List[List[int]]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> uf &lt;span style="color:#f92672">=&lt;/span> UnionFind(n &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> u, v, cost &lt;span style="color:#f92672">in&lt;/span> roads:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> uf&lt;span style="color:#f92672">.&lt;/span>union(u, v, cost)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> uf&lt;span style="color:#f92672">.&lt;/span>min_cost[uf&lt;span style="color:#f92672">.&lt;/span>find(&lt;span style="color:#ae81ff">1&lt;/span>)]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>但基础版有一个隐患：合并时总是 &lt;code>parent[px] = py&lt;/code>，可能导致树变得极高（极端情况退化成链表），虽然路径压缩能挽回一些，但你想优雅到底，于是祭出&lt;strong>按秩合并优化&lt;/strong>：加一个 &lt;code>rank&lt;/code> 数组记录树的深度（或大小），永远把小树挂在大树下面，确保树高稳定在 &lt;code>O(log n)&lt;/code>。搭配路径压缩，双管齐下，性能拉满。&lt;/p>
&lt;p>&lt;strong>按秩合并版&lt;/strong>：&lt;/p>
&lt;p>合并时比较两棵树的&amp;quot;秩&amp;quot;（近似高度或节点数），把矮树挂到高树下，避免树过高导致 find 变慢。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 并查集&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">UnionFindOptimize&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> __init__(self, n):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>parent &lt;span style="color:#f92672">=&lt;/span> list(range(n)) &lt;span style="color:#75715e"># parent[i] 表示节点i的父节点&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>min_cost &lt;span style="color:#f92672">=&lt;/span> [float(&lt;span style="color:#e6db74">&amp;#39;inf&amp;#39;&lt;/span>)] &lt;span style="color:#f92672">*&lt;/span> n &lt;span style="color:#75715e"># min_cost[i] 表示节点i所在集合的最小边权&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>rank &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> n &lt;span style="color:#75715e"># rank[i] 表示节点 i 所在树的秩（近似高度） &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">find&lt;/span>(self, x):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>parent[x] &lt;span style="color:#f92672">!=&lt;/span> x:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>parent[x] &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>find(self&lt;span style="color:#f92672">.&lt;/span>parent[x]) &lt;span style="color:#75715e"># 路径压缩&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>parent[x]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">union&lt;/span>(self, x, y, cost):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;按秩合并 x 和 y 所在的集合，同时更新最小边权&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> px, py &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>find(x), self&lt;span style="color:#f92672">.&lt;/span>find(y)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> px &lt;span style="color:#f92672">==&lt;/span> py:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>min_cost[px] &lt;span style="color:#f92672">=&lt;/span> min(self&lt;span style="color:#f92672">.&lt;/span>min_cost[px], cost)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 按秩合并：矮树挂到高树下&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>rank[px] &lt;span style="color:#f92672">&amp;lt;&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>rank[py]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>parent[px] &lt;span style="color:#f92672">=&lt;/span> py &lt;span style="color:#75715e"># px 树矮，把 px 挂到 py 下&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>min_cost[py] &lt;span style="color:#f92672">=&lt;/span> min(self&lt;span style="color:#f92672">.&lt;/span>min_cost[py], self&lt;span style="color:#f92672">.&lt;/span>min_cost[px], cost)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>rank[px] &lt;span style="color:#f92672">&amp;gt;&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>rank[py]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>parent[py] &lt;span style="color:#f92672">=&lt;/span> px &lt;span style="color:#75715e"># py 树矮，把 py 挂到 px 下&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>min_cost[px] &lt;span style="color:#f92672">=&lt;/span> min(self&lt;span style="color:#f92672">.&lt;/span>min_cost[px], self&lt;span style="color:#f92672">.&lt;/span>min_cost[py], cost)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>parent[py] &lt;span style="color:#f92672">=&lt;/span> px &lt;span style="color:#75715e"># 两树一样高，随便挂（这里把 py 挂到 px 下）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>min_cost[px] &lt;span style="color:#f92672">=&lt;/span> min(self&lt;span style="color:#f92672">.&lt;/span>min_cost[px], self&lt;span style="color:#f92672">.&lt;/span>min_cost[py], cost)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>rank[px] &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># px 树高度 +1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Solution&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">minScore&lt;/span>(self, n: int, roads: List[List[int]]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> uf &lt;span style="color:#f92672">=&lt;/span> UnionFindOptimized(n &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> u, v, cost &lt;span style="color:#f92672">in&lt;/span> roads:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> uf&lt;span style="color:#f92672">.&lt;/span>union(u, v, cost)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> uf&lt;span style="color:#f92672">.&lt;/span>min_cost[uf&lt;span style="color:#f92672">.&lt;/span>find(&lt;span style="color:#ae81ff">1&lt;/span>)]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>时间复杂度 &lt;code>O(m α(n))&lt;/code>，空间 &lt;code>O(n)&lt;/code>。最关键的是——&lt;strong>你根本不需要显式建图&lt;/strong>，边读边合并，流水线作业，潇洒得像开着自动拾取刷副本。并查集这把降维打击杖，让你在甩锅的路上又领先了一个身位。&lt;/p>
&lt;hr>
&lt;h5 id="尾声">尾声：
&lt;/h5>&lt;p>你合上笔记本，从工位抽屉里翻出那本牛皮封面的 &lt;strong>《踩坑指南》&lt;/strong>——这是你多年运维生涯的血泪结晶，封面上还沾着上次服务器爆炸时溅到的咖啡渍。 你翻到最新一页，工工整整地写下今天的战利品——一张随身携带的判断流程图：&lt;/p>
&lt;blockquote>
&lt;p>拿到一个图论题，
先问自己：和连通性有关吗？
├─ 是 → 需要具体路径吗？
│ ├─ 是 → DFS/BFS
│ └─ 否 → 需要维护连通分量的属性吗？
│ ├─ 是 → 并查集
│ └─ 否 → 边是动态加的？频繁查询？
│ ├─ 是 → 并查集
│ └─ 否 → DFS/BFS 也可以
│
└─ 否 → 是最短路径/层次问题吗？
├─ 是 → BFS / Dijkstra
└─ 否 → 需要遍历整张图吗？
├─ 是 → DFS/BFS
└─ 否 → 看情况开盲盒&lt;/p>&lt;/blockquote>
&lt;p>你深吸一口气，准备好了完美的甩锅话术：“主管，不是我的问题，是 1 号服务器所在连通分量里有台安全系数只有 3 的遗产服务器，这是并查集算出来的，数学不会骗人。如果您不信，我可以给您画个 Union-Find 的树形图。”&lt;/p>
&lt;p>存档已保存，下张地图见。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3286 DAG 阈值猎杀：二分特工的探针三连击</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-03/</link><pubDate>Fri, 03 Jul 2026 16:56:44 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-03/</guid><description>&lt;h2 id="dag-阈值猎杀二分特工的探针三连击">🎮DAG 阈值猎杀：二分特工的探针三连击！
&lt;/h2>&lt;p>你是一名网络特工，代号“二分”——不是因为你长得二分，而是你解决问题时总喜欢把答案劈成两半，逼它自己招供。今天你接到的任务比以往更离谱：从总部 &lt;code>0&lt;/code> 号节点出发，穿越一张由单向光缆编织的有向无环图（DAG），抵达最后一处未被黑客攻破的数据中心 &lt;code>n-1&lt;/code>。沿途的中转服务器，有些还在线（&lt;code>online[i]=true&lt;/code>），有些已经挂了。节点之间由单向光缆连接，每条光缆上都贴着一张维修价签 &lt;code>cost&lt;/code>，明码标价。&lt;/p>
&lt;p>上司甩给你一笔可怜的预算 &lt;code>k&lt;/code>，要你挑一条路，把沿途经过的光缆通通维护一遍。规矩很明确：总花费不许超支，沿途服务器必须在线——你总不能对着一台冒烟的机柜敲 ping 吧。&lt;/p>
&lt;p>你决定，要在所有合法路径中，找出那个&lt;strong>最小边成本最大&lt;/strong>的路径。为什么盯着最小边？因为短板决定体验，瓶颈决定命运——哪怕路上有一根 1 块钱的劣质网线，它就能把整条万兆光纤拖成PPT播放器。让这条路上最烂的那段，也比其他所有合法路径的最烂段要强——这是属于特工的尊严，也是属于打工人的倔强。&lt;/p>
&lt;p>当然，如果连一条合法路径都不存在……那就带着 &lt;code>-1&lt;/code> 回去挨批。&lt;/p>
&lt;hr>
&lt;h5 id="二分阈值猎杀">二分阈值猎杀
&lt;/h5>&lt;p>这个问题说白了是个“双约束”修罗场——既要让路径上的&lt;strong>最小边成本&lt;/strong>尽可能大，又要让&lt;strong>总成本&lt;/strong>不超过 &lt;code>k&lt;/code>。就像老板既要马儿跑得快，又要马儿不吃草一样。&lt;/p>
&lt;p>但你丝毫不慌——&amp;quot;&lt;strong>最大化最小值&amp;quot;，这五个字就是二分查找的接头暗号&lt;/strong>。&lt;/p>
&lt;p>如果阈值 &lt;code>x&lt;/code> 可行（即存在一条路径，所有边成本 ≥ &lt;code>x&lt;/code>，且总成本 ≤ &lt;code>k&lt;/code>），那么任何比 &lt;code>x&lt;/code> 小的阈值也一定可行（因为放宽了边限制，路径只会更多）。反过来，如果 &lt;code>x&lt;/code> 不可行，那么比 &lt;code>x&lt;/code> 大的阈值更不可能可行。&lt;/p>
&lt;p>单调性成立，猎杀开始。你像切西瓜一样，把答案范围 &lt;code>[l, r]&lt;/code> 一分为二，反复猜、反复验证，直到精确命中那个最优阈值。&lt;/p>
&lt;p>你给这套&amp;quot;猜答案 → 验答案&amp;quot;的流程起了个中二代号：阈值探针。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">findMaxPathScore&lt;/span>(n: int, edges: List[List[int]], online: List[bool], k: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n &lt;span style="color:#f92672">=&lt;/span> len(online)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g &lt;span style="color:#f92672">=&lt;/span> [[] &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> l, r &lt;span style="color:#f92672">=&lt;/span> float(&lt;span style="color:#e6db74">&amp;#39;inf&amp;#39;&lt;/span>), &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 建图(只走在线节点)和确定边界&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> u, v, cost &lt;span style="color:#f92672">in&lt;/span> edges:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">not&lt;/span> online[u] &lt;span style="color:#f92672">or&lt;/span> &lt;span style="color:#f92672">not&lt;/span> online[v]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g[u]&lt;span style="color:#f92672">.&lt;/span>append((v, cost))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> r &lt;span style="color:#f92672">=&lt;/span> max(r, cost)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> l &lt;span style="color:#f92672">=&lt;/span> min(l, cost)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 阈值探针!&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">check&lt;/span>(limit: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> bool:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># todo: 检查阈值 limit 是否可行 &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">pass&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 二分答案&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">not&lt;/span> check(l):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> l &lt;span style="color:#f92672">&amp;lt;=&lt;/span> r:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mid &lt;span style="color:#f92672">=&lt;/span> (l &lt;span style="color:#f92672">+&lt;/span> r) &lt;span style="color:#f92672">&amp;gt;&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> check(mid):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> l &lt;span style="color:#f92672">=&lt;/span> mid &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> r &lt;span style="color:#f92672">=&lt;/span> mid &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> r
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>现在，你必须给这个空壳探针注入灵魂。&lt;/p>
&lt;p>&lt;code>check(limit)&lt;/code> 要干的事很明确：在只允许走&lt;code>成本 ≥ limit&lt;/code> 的边这个前提下，判断能否从 0 走到 n-1，且总花费 &lt;code>≤ k&lt;/code>。&lt;/p>
&lt;hr>
&lt;h5 id="dijkstra-最短路--二分最稳健的探针">Dijkstra 最短路 + 二分（最稳健的探针）
&lt;/h5>&lt;p>你脑子里第一个蹦出来的，是图论祖师爷 Dijkstra 老爷子。他老人家的算法精髓就四个字：&lt;strong>贪心 + 单调&lt;/strong>——用一个最小堆永远优先处理当前花费最小的节点，保证第一次抵达就是最优。&lt;/p>
&lt;p>现在你把 Dijkstra 改装成一台&amp;quot;阈值检查器&amp;quot;：&lt;/p>
&lt;ol>
&lt;li>维护一个 &lt;code>dist&lt;/code> 数组，记录从起点到每个节点的最小花费，初始时只有 &lt;code>dist[0] = 0&lt;/code>，其余为正无穷。&lt;/li>
&lt;li>堆里存 &lt;code>(累计花费, 节点)&lt;/code>，每次弹出花费最小的节点。如果当前花费已经超过 &lt;code>k&lt;/code>——继续走就是贴钱打工，直接&lt;code>return False&lt;/code>。&lt;/li>
&lt;li>如果弹出的恰好是终点&lt;code>n-1&lt;/code>，恭喜，Dijkstra 保证第一次弹出终点就是全局最小总花费，它不超预算，意味着&lt;code>limit&lt;/code> 肯定可行，&lt;code>return True&lt;/code>。&lt;/li>
&lt;li>如果当前弹出的花费比 &lt;code>dist[u]&lt;/code> 还大？说明这是个过时记录——之前已经有更便宜的路到过&lt;code>u&lt;/code>了，果断扔掉。&lt;/li>
&lt;li>遍历 &lt;code>u&lt;/code> 的所有出边，&lt;code>cost &amp;lt; limit&lt;/code> 的直接无视（太次，配不上你的追求），只对&lt;code>cost &amp;gt;= limit&lt;/code> 的“硬货”做松弛：如果 &lt;code>dist[u] + cost &amp;lt; dist[v]&lt;/code>，更新 &lt;code>dist[v]&lt;/code> 并入堆。&lt;/li>
&lt;/ol>
&lt;p>这个检查器有多稳？一次 Dijkstra 是 &lt;code>O(m log n)&lt;/code>，套上二分 &lt;code>O(log C)&lt;/code>，总复杂度 &lt;code>O(m log n log C)&lt;/code>，对于题目规模来说，就像用 4090 跑扫雷，绝对稳如老狗。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 解法一：Dijkstra 最短路&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">check&lt;/span>(limit: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> bool:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dis &lt;span style="color:#f92672">=&lt;/span> [float(&lt;span style="color:#e6db74">&amp;#39;inf&amp;#39;&lt;/span>)] &lt;span style="color:#f92672">*&lt;/span> n
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dis[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pq &lt;span style="color:#f92672">=&lt;/span> [(&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>)] &lt;span style="color:#75715e"># (累计花费, 节点)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> pq:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cur, u &lt;span style="color:#f92672">=&lt;/span> heapq&lt;span style="color:#f92672">.&lt;/span>heappop(pq)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> cur &lt;span style="color:#f92672">&amp;gt;&lt;/span> k:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">False&lt;/span> &lt;span style="color:#75715e"># 超预算，直接死刑&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> u &lt;span style="color:#f92672">==&lt;/span> n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span> &lt;span style="color:#75715e"># 第一次抵达终点 = 最小花费，预算内通关&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> cur &lt;span style="color:#f92672">&amp;gt;&lt;/span> dis[u]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span> &lt;span style="color:#75715e"># 过时的记录，跳过&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> v, cost &lt;span style="color:#f92672">in&lt;/span> g[u]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> cost &lt;span style="color:#f92672">&amp;lt;&lt;/span> limit:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> dis[v] &lt;span style="color:#f92672">&amp;gt;&lt;/span> dis[u] &lt;span style="color:#f92672">+&lt;/span> cost:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dis[v] &lt;span style="color:#f92672">=&lt;/span> dis[u] &lt;span style="color:#f92672">+&lt;/span> cost
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> heapq&lt;span style="color:#f92672">.&lt;/span>heappush(pq, (dis[v], v))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">False&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>你刚觉得Dijkstra这把重剑稳如老狗，但转念一想——这图可是DAG啊！有向无环，意味着路径天生有序，根本不需要堆去维护什么优先级。于是你决定换一把更轻巧的匕首。&lt;/p>
&lt;hr>
&lt;h5 id="记忆化搜索--二分灵巧的回溯探针">记忆化搜索 + 二分（灵巧的回溯探针）
&lt;/h5>&lt;p>既然 DAG 无环，那递归天然就是安全的——不用担心无限套娃，因为没有环可以绕回去。&lt;/p>
&lt;p>路径天生有序，递归回溯就能把每条边捋一遍。而且 &lt;code>n&lt;/code> 的范围不算太大，记忆化搜索正好派上用场：从终点倒推，一路回溯，把中间结果缓存下来，既省堆又省脑细胞。&lt;/p>
&lt;p>思路也很直球：定义 &lt;code>dfs(u)&lt;/code> 表示从节点 &lt;code>u&lt;/code> 出发，在只允许边成本 ≥ &lt;code>limit&lt;/code> 的条件下，到达终点 &lt;code>n-1&lt;/code> 所需的最小总花费。&lt;/p>
&lt;p>你从 &lt;code>0&lt;/code> 开始递归，对于当前节点 &lt;code>u&lt;/code>：&lt;/p>
&lt;ul>
&lt;li>如果 &lt;code>u&lt;/code> 恰好是终点，那就不用再花一分钱，返回 &lt;code>0&lt;/code>；&lt;/li>
&lt;li>否则，遍历 &lt;code>u&lt;/code> 的所有出边 &lt;code>(v, cost)&lt;/code>，只挑 &lt;code>cost &amp;gt;= limit&lt;/code> 的硬边走，递归计算 &lt;code>dfs(v)&lt;/code>，加上当前边的成本 &lt;code>cost&lt;/code>，取所有结果中的最小值；&lt;/li>
&lt;li>用 &lt;code>@cache&lt;/code> 装饰器自动缓存每个 &lt;code>u&lt;/code> 的计算结果——这样每个节点最多算一次，后面的递归直接复用。&lt;/li>
&lt;/ul>
&lt;p>最后，如果 &lt;code>dfs(0) &amp;lt;= k&lt;/code>，说明阈值 &lt;code>limit&lt;/code> 可行。&lt;/p>
&lt;p>整个过程就像从终点往回铺路标：你只关心&amp;quot;从当前位置到终点最少花多少钱&amp;quot;，而每个子问题结构相同，递归套娃加上缓存，把&amp;quot;反向推导&amp;quot;玩成了自动化流水线。时间复杂度&lt;code>O(m)&lt;/code>（每条边最多被访问一次），乘上二分的&lt;code>O(log C)&lt;/code>，总复杂度&lt;code>O(m log C)&lt;/code>——比 Dijkstra 省掉了整个堆操作的&lt;code>log n&lt;/code>，轻快得像卸了甲的轻骑兵。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 解法二：记忆化搜索&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">check&lt;/span>(limit: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> bool:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">@cache&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">dfs&lt;/span>(u: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> u &lt;span style="color:#f92672">==&lt;/span> n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> float(&lt;span style="color:#e6db74">&amp;#39;inf&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> v, cost &lt;span style="color:#f92672">in&lt;/span> g[u]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> cost &lt;span style="color:#f92672">&amp;gt;=&lt;/span> limit:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> min(ans, dfs(v) &lt;span style="color:#f92672">+&lt;/span> cost)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> dfs(&lt;span style="color:#ae81ff">0&lt;/span>) &lt;span style="color:#f92672">&amp;lt;=&lt;/span> k
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>你舞着这把递归匕首，咧开的嘴角还没来得及合上，脑子里突然闪过一个声音：“递归深度告警。” 没错，万一哪天数据规模翻倍，你的递归栈就可能当场爆掉，留下一地 &lt;code>RecursionError&lt;/code> 的碎片。&lt;/p>
&lt;p>作为一名成熟的特工，你习惯把命运攥在自己手里——与其依赖黑盒子的调用栈，不如自己动手排兵布阵，勤劳迭代。于是，你收起匕首，抽出第三把武器——&lt;strong>拓扑排序 + DP&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h5 id="拓扑排序-dp--二分终极流水线探针">拓扑排序 DP + 二分（终极流水线探针）
&lt;/h5>&lt;p>拓扑排序是 DAG 的天选搭档。它能把所有节点排成一条流水线，保证每个节点轮到它的时候，所有前驱都已经处理完毕。这意味着你可以顺着流水线一路往前递推，不需要堆来排序，不需要递归来回溯，一个普通队列就搞定。&lt;/p>
&lt;p>它和记忆化搜索，本质上是同一道递推公式的两面：&lt;/p>
&lt;ul>
&lt;li>记忆化是 &lt;strong>“拉”&lt;/strong>：dfs(u) 向后继节点要答案，一层层往下问，答案再一层层往回传；&lt;/li>
&lt;li>拓扑排序是 &lt;strong>“推”&lt;/strong>：dp[u] 算完直接塞给后继节点，按拓扑序从前往后走，接力棒一样往下传。&lt;/li>
&lt;/ul>
&lt;p>区别在于——&amp;ldquo;谁先算谁后算&amp;quot;这件事，记忆化靠递归栈隐式决定，拓扑排序靠入度归零机制显式决定——入度归零的先上，处理完它的后继，后继入度减一，新的归零节点跟上。整个顺序由 DAG 的结构天然锁定，比递归栈靠谱得多，也比堆排序轻快得多。&lt;/p>
&lt;p>不过开工前有个小坑要填：图中可能存在一些不可达的&amp;quot;孤岛节点&amp;rdquo;——入度为 0 但不是起点，没有任何边指向它们，从起点绝对到不了。这些孤岛如果留着，它们的后继会一直等不到入度归零，整条链路直接卡死。所以先做一轮预处理，把这些坏死组织清理干净：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">findMaxPathScore&lt;/span>(edges: List[List[int]], online: List[bool], k: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n &lt;span style="color:#f92672">=&lt;/span> len(online)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g &lt;span style="color:#f92672">=&lt;/span> [[] &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> l, r &lt;span style="color:#f92672">=&lt;/span> float(&lt;span style="color:#e6db74">&amp;#39;inf&amp;#39;&lt;/span>), &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deg &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> n &lt;span style="color:#75715e"># 记录节点入度&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> u, v, cost &lt;span style="color:#f92672">in&lt;/span> edges:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">not&lt;/span> online[u] &lt;span style="color:#f92672">or&lt;/span> &lt;span style="color:#f92672">not&lt;/span> online[v]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g[u]&lt;span style="color:#f92672">.&lt;/span>append((v, cost))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> r &lt;span style="color:#f92672">=&lt;/span> max(r, cost)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> l &lt;span style="color:#f92672">=&lt;/span> min(l, cost)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deg[v] &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 更新节点入度&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 阈值探针!&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 先预处理：剥离从起点不可达的&amp;#34;孤岛节点&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q &lt;span style="color:#f92672">=&lt;/span> deque([i &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, n) &lt;span style="color:#66d9ef">if&lt;/span> deg[i] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>]) &lt;span style="color:#75715e"># 入度为 0 且不是起点的节点&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> q:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> u &lt;span style="color:#f92672">=&lt;/span> q&lt;span style="color:#f92672">.&lt;/span>popleft() &lt;span style="color:#75715e"># 弹出一个孤岛&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> v, _ &lt;span style="color:#f92672">in&lt;/span> g[u]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deg[v] &lt;span style="color:#f92672">-=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 孤岛的后继入度减 1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> v &lt;span style="color:#f92672">and&lt;/span> deg[v] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>: &lt;span style="color:#75715e"># 后继也变成孤岛了（且不是起点）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q&lt;span style="color:#f92672">.&lt;/span>append(v) &lt;span style="color:#75715e"># 加入剥离队列&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">check&lt;/span>(limit: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> bool:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># todo: 检查阈值 limit 是否可行 &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">pass&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 二分答案&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">......&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>剥完之后，剩下的 deg 就是一份干净的、只保留从起点可达结构的入度表。&lt;/p>
&lt;p>接下来 check(limit) 就清爽了，三步走：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>初始化：复制入度表 &lt;code>cdeg = deg.copy()&lt;/code>，&lt;code>dp数组&lt;/code>全填无穷大，初始化 &lt;code>dp[0]=0&lt;/code>，队列只放起点 &lt;code>0&lt;/code>；&lt;/p>
&lt;p>为什么要 deg.copy()？因为 check 里的拓扑排序是破坏性的——每处理一个节点就 cdeg[v] -= 1，跑完一次 check 后整个入度表就全被减成 0 了。但二分要调几十次 check，每次都需要一份完整的、初始状态的入度表。所以每次都得 copy 一份新的来&amp;quot;消耗&amp;quot;。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>流水线推进：弹出节点 &lt;code>u&lt;/code>，如果是终点&lt;code>n-1&lt;/code>，直接返回 &lt;code>dp[u] &amp;lt;= k&lt;/code>——此时所有前驱都已处理完毕；&lt;/p>
&lt;/li>
&lt;li>
&lt;p>松弛 + 传递：遍历&lt;code>u&lt;/code>的出边，只走&lt;code>cost &amp;gt;= limit&lt;/code>的硬边做松弛，更新&lt;code>dp[v]&lt;/code>。注意：不管边够不够硬，&lt;code>cdeg[v]&lt;/code> 都要减一——边权过滤管的是 &lt;code>dp&lt;/code> 值，入度递减管的是拓扑序，两条线互不干扰。入度归零的节点入队，轮到它上场。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>时间复杂度 &lt;code>O(m)&lt;/code> 一次检查，套上二分&lt;code>O(log C)&lt;/code>，总复杂度 &lt;code>O(m log C)&lt;/code>。无递归、无堆栈、遇终点提前宣判——DAG 上的性能天花板。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">findMaxPathScore&lt;/span>(edges: List[List[int]], online: List[bool], k: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n &lt;span style="color:#f92672">=&lt;/span> len(online)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g &lt;span style="color:#f92672">=&lt;/span> [[] &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> l, r &lt;span style="color:#f92672">=&lt;/span> float(&lt;span style="color:#e6db74">&amp;#39;inf&amp;#39;&lt;/span>), &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deg &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> n &lt;span style="color:#75715e"># 记录节点入度&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> u, v, cost &lt;span style="color:#f92672">in&lt;/span> edges:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">not&lt;/span> online[u] &lt;span style="color:#f92672">or&lt;/span> &lt;span style="color:#f92672">not&lt;/span> online[v]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g[u]&lt;span style="color:#f92672">.&lt;/span>append((v, cost))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> r &lt;span style="color:#f92672">=&lt;/span> max(r, cost)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> l &lt;span style="color:#f92672">=&lt;/span> min(l, cost)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deg[v] &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 阈值探针!&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 预处理：剥离从起点不可达的&amp;#34;孤岛节点&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q &lt;span style="color:#f92672">=&lt;/span> deque([i &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, n) &lt;span style="color:#66d9ef">if&lt;/span> deg[i] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> q:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> u &lt;span style="color:#f92672">=&lt;/span> q&lt;span style="color:#f92672">.&lt;/span>popleft()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> v, _ &lt;span style="color:#f92672">in&lt;/span> g[u]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deg[v] &lt;span style="color:#f92672">-=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> v &lt;span style="color:#f92672">and&lt;/span> deg[v] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q&lt;span style="color:#f92672">.&lt;/span>append(v)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">check&lt;/span>(limit: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> bool:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dp &lt;span style="color:#f92672">=&lt;/span> [float(&lt;span style="color:#e6db74">&amp;#39;inf&amp;#39;&lt;/span>)] &lt;span style="color:#f92672">*&lt;/span> n &lt;span style="color:#75715e"># dp[i]: 从起点到 i 的最小总花费&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cdeg &lt;span style="color:#f92672">=&lt;/span> deg&lt;span style="color:#f92672">.&lt;/span>copy() &lt;span style="color:#75715e"># 拷贝入度——拓扑排序会消耗它，不能动原件&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dp[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q &lt;span style="color:#f92672">=&lt;/span> deque([&lt;span style="color:#ae81ff">0&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> q:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> u &lt;span style="color:#f92672">=&lt;/span> q&lt;span style="color:#f92672">.&lt;/span>popleft()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> u &lt;span style="color:#f92672">==&lt;/span> n &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> dp[u] &lt;span style="color:#f92672">&amp;lt;=&lt;/span> k &lt;span style="color:#75715e"># 到达终点，提前宣判！&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> v, cost &lt;span style="color:#f92672">in&lt;/span> g[u]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> cost &lt;span style="color:#f92672">&amp;gt;=&lt;/span> limit:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dp[v] &lt;span style="color:#f92672">=&lt;/span> min(dp[v], dp[u] &lt;span style="color:#f92672">+&lt;/span> cost) &lt;span style="color:#75715e"># 松弛：只走够硬的边&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cdeg[v] &lt;span style="color:#f92672">-=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 入度必减——跟边权无关，这是拓扑序的事&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> cdeg[v] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>: &lt;span style="color:#75715e"># 前驱全部处理完毕，轮到 v 上场&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q&lt;span style="color:#f92672">.&lt;/span>append(v)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">False&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 二分答案&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">......&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h5 id="尾声">尾声
&lt;/h5>&lt;p>探针归位，二分收敛——这是你第无数次看着二分框架优雅地合拢，像狙击镜里十字准星缓缓锁住目标。&lt;/p>
&lt;p>Dijkstra 重剑无锋，记忆化匕首如风，拓扑排序流水线降维打击——三连击打完收工，最终撑起整个猎杀框架的，还是那招最朴素的祖传手艺——二分：把一个&amp;quot;既要又要&amp;quot;的双约束难题，劈成一道道简单的判断题：“够不够？行不行？能还是不能？”&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3286 0-1网格求生指南：血条战士 01-BFS 的觉醒</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-02/</link><pubDate>Thu, 02 Jul 2026 12:27:54 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-02/</guid><description>&lt;h2 id="0-1网格求生指南血条战士-01-bfs-的觉醒">🎮0-1网格求生指南：血条战士 01-BFS 的觉醒！
&lt;/h2>&lt;p>你是一个 &lt;strong>血条战士&lt;/strong>，初始满血 &lt;strong>health&lt;/strong> 点。你站在矩形网格的左上角 &lt;code>(0,0)&lt;/code>，准备冲向右下角的传送门 &lt;code>(m-1,n-1)&lt;/code>。 网格里只有两种地板：绿草地 &lt;code>0&lt;/code> 和陷阱 &lt;code>1&lt;/code>。踩到绿草地，毫发无伤；踩到陷阱，当场 &lt;code>-1&lt;/code> 血。&lt;/p>
&lt;p>规则很残酷：&lt;strong>任何时候你的血量必须大于 0&lt;/strong>，一旦血条见底，直接原地去世。就算冲到传送门前，踏进去那一刻血量也得 &amp;gt;0，否则倒在终点线上也算输。&lt;/p>
&lt;p>你的目标很简单：&lt;strong>活着到达终点&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h5 id="这是一场最小受伤大赛">这是一场“最小受伤大赛”
&lt;/h5>&lt;p>你先算了笔账：不管你在网格里怎么蛇皮走位，这一路踩到的陷阱总数，就是你整趟旅程的“受伤点数”。只要存在一条路，能让你从起点到终点的受伤点数 &lt;strong>严格小于&lt;/strong> 你的初始血量 health，你就一定能活着通关。&lt;/p>
&lt;p>于是你恍然大悟——哪有什么刀山火海，策划压根就是在考你数学。所谓“最小伤害路”，翻译过来就三个字：&lt;strong>最短路&lt;/strong>。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>在这张 0-1 网格里，找一条从左上到右下的路径，使得沿途经过的 1 的个数最少。&lt;/strong>&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h5 id="dijkstra最短路">Dijkstra（最短路）
&lt;/h5>&lt;p>如果你是个愣头青，从起点闷头乱冲，大概率走到一半就发现血条飙红、后路已断，只能仰天长叹：“早知道换条路了……”&lt;/p>
&lt;p>痛定思痛，你用“读档重来”的神技，悟出第一招保命法门：&lt;strong>带优先级的探索&lt;/strong>——也就是 Dijkstra 算法。从此，你不再是个无头苍蝇，而是化身为精打细算的铁公鸡旅人，永远先探最安全的方向，小心翼翼试验每一寸土地。&lt;/p>
&lt;p>你准备好一个魔法小本 &lt;code>dist&lt;/code>，记录到达每个格子的 &lt;strong>当前最小受伤次数&lt;/strong>，一开始全部写成 -1（表示&amp;quot;从未涉足&amp;quot;）。起点的受伤次数直接等于 &lt;code>grid[0][0]&lt;/code>——没错，起点本身也可能是个阴险的陷阱，先扣为敬。你把起点塞进一个&lt;strong>优先队列（小顶堆）&lt;/strong>，在&amp;quot;轻伤优先&amp;quot;的原则下层层推进：&lt;/p>
&lt;p>每次从堆顶请出受伤最轻的格子，先核对它是否已经在小本上被&amp;quot;盖章定案&amp;quot;（dis ≥ 0）。如果是，说明这条线索是迟到的&amp;quot;马后炮&amp;quot;——已经有一条更好的路径写进本子了，果断扔掉；否则，它就是目前的最优解，立刻盖章（写入 dis），并向四个邻居探头张望。凡是还没被盖章的邻居（dis &amp;lt; 0），就把这个邻居和&amp;quot;当前受伤 + 该邻居伤害&amp;quot;打包丢进优先队列，让它排队等登场。&lt;/p>
&lt;p>直到队列彻底清空，小本里便刻满了从起点到每个可达格子的最小受伤次数——而右下角 &lt;code>dis[m-1][n-1]&lt;/code>，正是你千辛万苦要找的那条最苟路线的代价。如果它 &amp;lt; health，恭喜，血条足够你浪。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">findSafeWalk&lt;/span>(grid: List[List[int]], health: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> bool:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m, n &lt;span style="color:#f92672">=&lt;/span> len(grid), len(grid[&lt;span style="color:#ae81ff">0&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dirs &lt;span style="color:#f92672">=&lt;/span> [(&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>), (&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>), (&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">1&lt;/span>), (&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dis &lt;span style="color:#f92672">=&lt;/span> [[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> n &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(m)] &lt;span style="color:#75715e"># 魔法小本，-1 表示&amp;#34;从未涉足&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q &lt;span style="color:#f92672">=&lt;/span> [(grid[&lt;span style="color:#ae81ff">0&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>], &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>)] &lt;span style="color:#75715e"># 优先队列（小顶堆），起点先入队&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> q:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> val, x, y &lt;span style="color:#f92672">=&lt;/span> heapq&lt;span style="color:#f92672">.&lt;/span>heappop(q) &lt;span style="color:#75715e"># 弹出当前累积伤害最轻的格子&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> dis[x][y] &lt;span style="color:#f92672">&amp;gt;=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>: &lt;span style="color:#75715e"># 已盖章？马后炮，扔掉 &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dis[x][y] &lt;span style="color:#f92672">=&lt;/span> val &lt;span style="color:#75715e"># 盖章定案，记录最优受伤次数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> dx, dy &lt;span style="color:#f92672">in&lt;/span> dirs:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> nx, ny &lt;span style="color:#f92672">=&lt;/span> x &lt;span style="color:#f92672">+&lt;/span> dx, y &lt;span style="color:#f92672">+&lt;/span> dy
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">&amp;lt;=&lt;/span> nx &lt;span style="color:#f92672">&amp;lt;&lt;/span> m &lt;span style="color:#f92672">and&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">&amp;lt;=&lt;/span> ny &lt;span style="color:#f92672">&amp;lt;&lt;/span> n &lt;span style="color:#f92672">and&lt;/span> dis[nx][ny] &lt;span style="color:#f92672">&amp;lt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> heapq&lt;span style="color:#f92672">.&lt;/span>heappush(q, (val &lt;span style="color:#f92672">+&lt;/span> grid[nx][ny], nx, ny)) &lt;span style="color:#75715e"># 邻居入队&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> dis[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>][&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">&amp;lt;&lt;/span> health
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>复杂度：&lt;code>O(mn log(mn))&lt;/code>——最坏情况每个格子都要入堆一次，堆操作log(mn)。&lt;/p>
&lt;hr>
&lt;h5 id="双端口袋与-01bfs">双端口袋与 01BFS
&lt;/h5>&lt;p>跑完 Dijkstra，你盯着那个 &lt;code>O(mn \log(mn))&lt;/code> 的复杂度，觉得有点冤——这破地图只有两种伤害——踩草地不掉血（0），踩陷阱掉 1 血（1），就两个档位，干嘛还要雇个堆来排序？&lt;/p>
&lt;p>杀鸡焉用牛刀。你果断扔掉优先队列，掏出一个双端队列——这玩意儿两头都能塞东西，堪称&amp;quot;插队神器&amp;quot;。核心操作只有两条：遇到草地就塞到队首，优先走；遇到陷阱就塞到队尾，等平坦的走完再说。这就是01BFS。&lt;/p>
&lt;p>你走得更快了。因为你永远在“不增加受伤”的前提下疯狂扩张版图，只有万不得已才去吃陷阱。伤害只有 0 和 1 两个档位，队列里躺着的格子，受伤次数要么是 &lt;code>k&lt;/code>，要么是 &lt;code>k+1&lt;/code>——从队首到队尾严格单调不减，&lt;strong>天然保持从小到大的顺序&lt;/strong>，跟 Dijkstra 一样正确，却完全不需堆排序。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">findSafeWalk&lt;/span>(grid: List[List[int]], health: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> bool:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m, n &lt;span style="color:#f92672">=&lt;/span> len(grid), len(grid[&lt;span style="color:#ae81ff">0&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dirs &lt;span style="color:#f92672">=&lt;/span> [(&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>), (&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>), (&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">1&lt;/span>), (&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dis &lt;span style="color:#f92672">=&lt;/span> [[float(&lt;span style="color:#e6db74">&amp;#39;inf&amp;#39;&lt;/span>)] &lt;span style="color:#f92672">*&lt;/span> n &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(m)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dis[&lt;span style="color:#ae81ff">0&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> grid[&lt;span style="color:#ae81ff">0&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q &lt;span style="color:#f92672">=&lt;/span> deque()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q&lt;span style="color:#f92672">.&lt;/span>appendleft((&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> q:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> x, y &lt;span style="color:#f92672">=&lt;/span> q&lt;span style="color:#f92672">.&lt;/span>popleft()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> dx, dy &lt;span style="color:#f92672">in&lt;/span> dirs:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> nx, ny &lt;span style="color:#f92672">=&lt;/span> x &lt;span style="color:#f92672">+&lt;/span> dx, y &lt;span style="color:#f92672">+&lt;/span> dy
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">&amp;lt;=&lt;/span> nx &lt;span style="color:#f92672">&amp;lt;&lt;/span> m &lt;span style="color:#f92672">and&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">&amp;lt;=&lt;/span> ny &lt;span style="color:#f92672">&amp;lt;&lt;/span> n:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cost &lt;span style="color:#f92672">=&lt;/span> dis[x][y] &lt;span style="color:#f92672">+&lt;/span>grid[nx][ny]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> cost &lt;span style="color:#f92672">&amp;lt;&lt;/span> dis[nx][ny]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dis[nx][ny] &lt;span style="color:#f92672">=&lt;/span> cost
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> grid[nx][ny] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q&lt;span style="color:#f92672">.&lt;/span>appendleft((nx, ny)) &lt;span style="color:#75715e"># 草地插队&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q&lt;span style="color:#f92672">.&lt;/span>append((nx, ny)) &lt;span style="color:#75715e"># 陷阱排队尾&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> dis[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>][&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">&amp;lt;&lt;/span> health
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>复杂度：&lt;code>O(mn)&lt;/code>，每个格子只进出队列一次，双端队列每次操作 &lt;code>O(1)&lt;/code>，比 Dijkstra 快了一个 &lt;code>log&lt;/code> 身位。&lt;/p>
&lt;hr>
&lt;h5 id="剪枝与终点直通车">剪枝与“终点直通车”
&lt;/h5>&lt;p>01-BFS 已经够快了，但你还不满足——明明有些路走到一半就已经&amp;quot;必死无疑&amp;quot;，你却还傻乎乎地继续往外扩展，这不是浪费感情吗？&lt;/p>
&lt;p>剪枝思路很简单：在探索邻居时，如果从起点到该邻居的累积伤害已经 ≥ health，那这条路径注定无法让你活着通关——别入队了，直接掐断。不合法的格子连进队列的资格都没有，搜索树当场修剪干净。&lt;/p>
&lt;p>光剪枝还不够。你又悟出一层更深的奥义：不管是 Dijkstra 还是 01-BFS，格子出队的顺序都是按受伤次数从小到大的。这意味着——你第一次从队列里掏出终点 (m-1, n-1) 的那一刻，手里的受伤次数就是全图最优解！&lt;/p>
&lt;p>何必探完整张地图？剪枝 + 直通车双管齐下，终点一到，直接宣判：&lt;/p>
&lt;ul>
&lt;li>如果 最小受伤次数 &amp;lt; health → 大喊一声 &lt;strong>true&lt;/strong>，开传送门溜了。&lt;/li>
&lt;li>如果 最小受伤次数 ≥ health → 苦笑一声 &lt;strong>false&lt;/strong>，老老实实读档吧。&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">findSafeWalk&lt;/span>(grid: List[List[int]], health: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> bool:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m, n &lt;span style="color:#f92672">=&lt;/span> len(grid), len(grid[&lt;span style="color:#ae81ff">0&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dirs &lt;span style="color:#f92672">=&lt;/span> [(&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>), (&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>), (&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">1&lt;/span>), (&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dis &lt;span style="color:#f92672">=&lt;/span> [[float(&lt;span style="color:#e6db74">&amp;#39;inf&amp;#39;&lt;/span>)] &lt;span style="color:#f92672">*&lt;/span> n &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(m)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dis[&lt;span style="color:#ae81ff">0&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> grid[&lt;span style="color:#ae81ff">0&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q &lt;span style="color:#f92672">=&lt;/span> deque()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q&lt;span style="color:#f92672">.&lt;/span>appendleft((&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> q:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> x, y &lt;span style="color:#f92672">=&lt;/span> q&lt;span style="color:#f92672">.&lt;/span>popleft()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> x &lt;span style="color:#f92672">==&lt;/span> m &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">and&lt;/span> y &lt;span style="color:#f92672">==&lt;/span> n &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>: &lt;span style="color:#75715e"># 终点到手，当场宣判&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> dx, dy &lt;span style="color:#f92672">in&lt;/span> dirs:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> nx, ny &lt;span style="color:#f92672">=&lt;/span> x &lt;span style="color:#f92672">+&lt;/span> dx, y &lt;span style="color:#f92672">+&lt;/span> dy
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> nx &lt;span style="color:#f92672">&amp;lt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">or&lt;/span> nx &lt;span style="color:#f92672">&amp;gt;=&lt;/span> m &lt;span style="color:#f92672">or&lt;/span> ny &lt;span style="color:#f92672">&amp;lt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">or&lt;/span> ny &lt;span style="color:#f92672">&amp;gt;=&lt;/span> n:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cost &lt;span style="color:#f92672">=&lt;/span> dis[x][y] &lt;span style="color:#f92672">+&lt;/span> grid[nx][ny]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> cost &lt;span style="color:#f92672">&amp;gt;=&lt;/span> health: &lt;span style="color:#75715e"># 入队前剪枝：必死之路，不配入队&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> cost &lt;span style="color:#f92672">&amp;lt;&lt;/span> dis[nx][ny]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dis[nx][ny] &lt;span style="color:#f92672">=&lt;/span> cost
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> grid[nx][ny] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q&lt;span style="color:#f92672">.&lt;/span>appendleft((nx, ny))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q&lt;span style="color:#f92672">.&lt;/span>append((nx, ny))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">False&lt;/span> &lt;span style="color:#75715e"># 队列清空仍未到达终点，无解&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h5 id="尾声">尾声
&lt;/h5>&lt;p>你到达了终点，胸口的血条摇摇欲坠，但硬是没归零。没有在半路被陷阱榨干，也没有在最后一步功亏一篑，稳得连你自己都有点意外。&lt;/p>
&lt;p>一路下来，Dijkstra 是你的稳健老哥，堆排序兜底不翻车；01-BFS 是你的飙车利器，双端队列省掉过路费；剪枝 + 终点直通车是最终形态——死路不踩，到站就下，用最少的代价趟穿了整张 0-1 网格。&lt;/p>
&lt;p>存档已保存，下张地图见。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode2812 地牢逃生指南：论如何优雅地绕开所有小偷</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-07-01/</link><pubDate>Wed, 01 Jul 2026 14:15:55 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-07-01/</guid><description>&lt;h2 id="地牢逃生指南论如何优雅地绕开所有小偷">🎮地牢逃生指南：论如何优雅地绕开所有小偷
&lt;/h2>&lt;p>你是一名勇者，代号“零”，此刻正站在一座 &lt;strong>n × n&lt;/strong> 地牢&lt;code>grid&lt;/code>的左上角 &lt;code>(0,0)&lt;/code>。地牢的某些格子里藏着面目可憎的&lt;strong>小偷&lt;/strong>（&lt;code>grid[r][c] = 1&lt;/code>），他们不偷装备，专偷你辛苦攒下的金币。你的任务是从 &lt;code>(0,0)&lt;/code> 走到右下角的出口 &lt;code>(n-1, n-1)&lt;/code>。移动规则很传统：上下左右随便走，但不能斜着飞（地牢天花板太矮，轻功施展不开）。最关键的是：你希望整条路尽可能远离小偷。毕竟离小偷越近，你的钱包就越危险。&lt;/p>
&lt;p>那么问题来了：怎么衡量一条路够不够&amp;quot;安全&amp;quot;？&lt;/p>
&lt;p>一条路径的&lt;strong>安全系数&lt;/strong>，定义为整条路径上的所有格子中，&lt;strong>离小偷距离最小的那个值&lt;/strong>。换句话说，就是你这一路上最危险的那一格距离小偷有多远。安全系数越大，说明你这一路最危险的时候也离小偷挺远，体验舒适。&lt;/p>
&lt;p>你要找的，就是所有从入口到出口的路径中，&lt;strong>最大的安全系数&lt;/strong>到底是多少。&lt;/p>
&lt;hr>
&lt;h5 id="开胃菜如果小偷已经堵门了">开胃菜：如果小偷已经堵门了？
&lt;/h5>&lt;p>先说个扎心的情况：如果起点 &lt;code>(0,0)&lt;/code> 或终点 &lt;code>(n-1,n-1)&lt;/code> 本身就是小偷老巢 (grid[r] [c]=1)，那你的冒险还没开始就结束了。安全系数注定是 &lt;strong>0&lt;/strong>，因为你一出生就被包围了——就像刚进副本就发现BOSS站在复活点上——别挣扎了，直接返回 0 完事（除非你光脚不怕穿鞋的，但任务描述里没给你这个隐藏选项）。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>n &lt;span style="color:#f92672">=&lt;/span> len(grid)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> grid[&lt;span style="color:#ae81ff">0&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">or&lt;/span> grid[n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>][n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h5 id="侦察阶段多源-bfs-扫描每个格子的安全分">侦察阶段：多源 BFS 扫描每个格子的“安全分”
&lt;/h5>&lt;p>排除掉上面的倒霉情况后，你决定先做个详细的地图侦察。&amp;ldquo;知己知彼，百战不殆&amp;rdquo;，这句老话在游戏里同样适用——你不会不带地图就去开荒对吧？&lt;/p>
&lt;p>于是，你掏出了大杀器——&lt;strong>多源 BFS 探测卫星&lt;/strong>。传统 BFS 是从一个点开始扩散，像往湖里扔一颗石子；多源 BFS 是同时从所有小偷的位置开始扩散，像同时在湖里扔了一堆石子，看谁的波纹先扫到某个点。&lt;/p>
&lt;p>具体操作如下：&lt;/p>
&lt;ul>
&lt;li>初始化：把所有小偷格子同时扔进队列（像卫星同时锁定所有贼窝），它们的距离设为 0；&lt;/li>
&lt;li>层层扩散：每次从队列里取出一个点，向四个方向扩展。第一次扫描到某个格子时，记录的距离就是它到最近小偷的曼哈顿距离；&lt;/li>
&lt;li>完成扫描：O(n²) 时间后，你就得到了一张完整的&amp;quot;安全地图&amp;quot; dis[r] [c]，数值越大越安全，数值为 0 的地方就是小偷的老巢&lt;/li>
&lt;/ul>
&lt;p>这一步就像在游戏里开了全图视野，每个格子离最近的敌人有多远一目了然。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># dis[][]: 存储每个点到最近小偷的距离&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dis &lt;span style="color:#f92672">=&lt;/span> [[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> n &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n)] &lt;span style="color:#75715e"># -1表示未访问&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dirs &lt;span style="color:#f92672">=&lt;/span> [(&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>), (&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>), (&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">1&lt;/span>), (&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>)] &lt;span style="color:#75715e"># 四个方向&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 初始化：所有小偷位置入队&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>q &lt;span style="color:#f92672">=&lt;/span> deque()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(n):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(n):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> grid[i][j] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dis[i][j] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q&lt;span style="color:#f92672">.&lt;/span>append((i, j))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 多源BFS计算每个点到最近小偷的距离&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">while&lt;/span> q:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> x, y &lt;span style="color:#f92672">=&lt;/span> q&lt;span style="color:#f92672">.&lt;/span>popleft()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> dx, dy &lt;span style="color:#f92672">in&lt;/span> dirs:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> nx, ny &lt;span style="color:#f92672">=&lt;/span> x &lt;span style="color:#f92672">+&lt;/span> dx, y &lt;span style="color:#f92672">+&lt;/span> dy
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">&amp;lt;=&lt;/span> nx &lt;span style="color:#f92672">&amp;lt;&lt;/span> n &lt;span style="color:#f92672">and&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">&amp;lt;=&lt;/span> ny &lt;span style="color:#f92672">&amp;lt;&lt;/span> n &lt;span style="color:#f92672">and&lt;/span> dis[nx][ny] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dis[nx][ny] &lt;span style="color:#f92672">=&lt;/span> dis[x][y] &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q&lt;span style="color:#f92672">.&lt;/span>append((nx, ny))
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>现在你手里有了这张安全地图，接下来的问题就是：怎么用它找到那条&amp;quot;最安全&amp;quot;的路径？&lt;/p>
&lt;blockquote>
&lt;p>找一条从左上到右下的路，让路上&lt;strong>最小的那个数字&lt;/strong>尽量大。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h5 id="解法一二分查找">解法一：二分查找
&lt;/h5>&lt;p>既然不知道最优的安全系数是多少，但你至少能回答一个问题：&lt;strong>是否存在一条从入口到出口的路径，路上每个格子的安全值都 ≥ K？&lt;/strong> 如果有，那真正的安全系数至少是 K。&lt;/p>
&lt;p>显然，K 越小越容易满足，K 越大越困难，这不就是二分查找最喜欢的单调性吗？于是你直接把 K 扔进二分锅里翻炒：&lt;/p>
&lt;p>每次猜一个中间值 &lt;code>mid&lt;/code>，用 BFS 或 DFS 检查：只走安全值 ≥ &lt;code>mid&lt;/code> 的格子，能不能从起点到终点。能，说明门槛还能再高点，&lt;code>lo = mid + 1&lt;/code>（贪心本性暴露）；不能，就说明门槛太高了，&lt;code>hi = mid - 1&lt;/code>（该降降期望了）。反复试探，最终夹出最大的可行门槛 k 。&lt;/p>
&lt;p>方法很稳妥，就是有点磨叽——每次二分都要重新扫一遍地图，就像反复翻看同一本攻略书的铁憨憨。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 已有 dis 安全地图&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">check&lt;/span>(limit: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> bool:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> visit &lt;span style="color:#f92672">=&lt;/span> [[&lt;span style="color:#66d9ef">False&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> n &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q &lt;span style="color:#f92672">=&lt;/span> deque()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q&lt;span style="color:#f92672">.&lt;/span>append((&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> visit[&lt;span style="color:#ae81ff">0&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> q:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> x, y &lt;span style="color:#f92672">=&lt;/span> q&lt;span style="color:#f92672">.&lt;/span>popleft()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> x &lt;span style="color:#f92672">==&lt;/span> n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">and&lt;/span> y &lt;span style="color:#f92672">==&lt;/span> n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> dx, dy &lt;span style="color:#f92672">in&lt;/span> dirs:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> nx, ny &lt;span style="color:#f92672">=&lt;/span> x &lt;span style="color:#f92672">+&lt;/span> dx, y &lt;span style="color:#f92672">+&lt;/span> dy
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> (&lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">&amp;lt;=&lt;/span> nx &lt;span style="color:#f92672">&amp;lt;&lt;/span> n &lt;span style="color:#f92672">and&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">&amp;lt;=&lt;/span> ny &lt;span style="color:#f92672">&amp;lt;&lt;/span> n
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">and&lt;/span> &lt;span style="color:#f92672">not&lt;/span> visit[nx][ny] &lt;span style="color:#f92672">and&lt;/span> dis[nx][ny] &lt;span style="color:#f92672">&amp;gt;=&lt;/span> limit):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q&lt;span style="color:#f92672">.&lt;/span>append((nx, ny))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> visit[nx][ny] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">False&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 二分查找&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>res &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>lo, hi &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>, min(dis[&lt;span style="color:#ae81ff">0&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>], dis[n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>][n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">while&lt;/span> lo &lt;span style="color:#f92672">&amp;lt;=&lt;/span> hi:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mid &lt;span style="color:#f92672">=&lt;/span> (lo &lt;span style="color:#f92672">+&lt;/span> hi) &lt;span style="color:#f92672">//&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> check(mid):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res &lt;span style="color:#f92672">=&lt;/span> mid
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> lo &lt;span style="color:#f92672">=&lt;/span> mid &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> hi &lt;span style="color:#f92672">=&lt;/span> mid &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> res
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>时间复杂度：O(n² log n)。多源 BFS 预处理 O(n²)，二分 log(n) 次，每次 BFS 检查又是 O(n²)。
空间复杂度：O(n²)。&lt;/p>
&lt;hr>
&lt;h5 id="解法二潜行大师的直觉dijkstra-最大瓶颈路">解法二：潜行大师的直觉（Dijkstra 最大瓶颈路）
&lt;/h5>&lt;p>你合上二分的攻略，眉头一皱：每次检查都要重新 BFS，就像在迷宫里反复走已经探过的路，太死板了。能不能一次搞定？你想起江湖上的一种传说：&lt;strong>把安全值看作“路宽”，要找一条从起点到终点“最窄处最宽”的路径&lt;/strong>。这不就是经典的最大瓶颈路径问题吗？&lt;/p>
&lt;p>于是你套上 Dijkstra 的风衣(贪心 + 松弛)，化身潜行大师，只不过把&amp;quot;累加距离&amp;quot;换成&amp;quot;维护最小值&amp;quot;，把&amp;quot;找最短&amp;quot;换成&amp;quot;找最大最小值&amp;quot;。具体来说：&lt;/p>
&lt;p>用一个&lt;strong>最大堆&lt;/strong>维护当前所有探索到的路径，按&lt;strong>该路径的瓶颈值&lt;/strong>（即整条路径上最危险那一步的安全系数）从大到小排序。起点的瓶颈值就是 &lt;code>dis[0][0]&lt;/code>。每次从堆里弹出瓶颈值最大的路径，然后用它向四周扩展：走到邻居 &lt;code>(nx, ny)&lt;/code> 时，新路径的瓶颈值是 &lt;code>min(当前瓶颈值, dis[nx][ny])&lt;/code>（经典的木桶效应——一个短板拉低全家）。&lt;/p>
&lt;p>如果这个新瓶颈值比已知的到达该格子的最优瓶颈值更大，就更新记录并入堆。因为你永远优先探索“当前最安全”的路径，所以第一次弹出终点时，那个瓶颈值就是全局最优——完全不用等所有路径都跑完。&lt;/p>
&lt;p>这样，你只需在安全地图上跑一次 Dijkstra 变种，没有二分的重复扫描，复杂度还是 O(n² log n)，但实际常数更小，优雅得像在刀尖上跳舞。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 已有 dis 安全地图&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Dijkstra算法变种：找&amp;#34;最大最小值&amp;#34;路径，就是最大化一条路径的瓶颈（路径上最危险的那一步的安全系数）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>visit &lt;span style="color:#f92672">=&lt;/span> [[&lt;span style="color:#66d9ef">False&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> n &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>visit[&lt;span style="color:#ae81ff">0&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>pq &lt;span style="color:#f92672">=&lt;/span> [(&lt;span style="color:#f92672">-&lt;/span>dis[&lt;span style="color:#ae81ff">0&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>], &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>)]] &lt;span style="color:#75715e"># 优先队列用负数技巧得到最大堆效果：每次弹出安全值最高的格子&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>max_safeness &lt;span style="color:#f92672">=&lt;/span> min(dis[&lt;span style="color:#ae81ff">0&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>], dis[n &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>][n &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>]) &lt;span style="color:#75715e"># 初始安全系数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">while&lt;/span> pq:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> val, cx, cy &lt;span style="color:#f92672">=&lt;/span> heapq&lt;span style="color:#f92672">.&lt;/span>heappop(pq) &lt;span style="color:#75715e"># 弹出当前安全系数最高的点&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> val &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#f92672">-&lt;/span>val &lt;span style="color:#75715e"># 转回正数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max_safeness &lt;span style="color:#f92672">=&lt;/span> min(max_safeness, val) &lt;span style="color:#75715e"># 更新路径的最小安全系数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> cx &lt;span style="color:#f92672">==&lt;/span> n &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">and&lt;/span> cy &lt;span style="color:#f92672">==&lt;/span> n &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">break&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> dx, dy &lt;span style="color:#f92672">in&lt;/span> dirs:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> nx, ny &lt;span style="color:#f92672">=&lt;/span> cx &lt;span style="color:#f92672">+&lt;/span> dx, cy &lt;span style="color:#f92672">+&lt;/span> dy
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">&amp;lt;=&lt;/span> nx &lt;span style="color:#f92672">&amp;lt;&lt;/span> n &lt;span style="color:#f92672">and&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">&amp;lt;=&lt;/span> ny &lt;span style="color:#f92672">&amp;lt;&lt;/span> n &lt;span style="color:#f92672">and&lt;/span> &lt;span style="color:#f92672">not&lt;/span> visit[nx][ny]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> visit[nx][ny] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> heapq&lt;span style="color:#f92672">.&lt;/span>heappush(pq, (&lt;span style="color:#f92672">-&lt;/span>dis[nx][ny], nx, ny))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> max_safeness
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>时间复杂度：O(n² log n)（堆操作）。
空间复杂度：O(n²)。&lt;/p>
&lt;hr>
&lt;h5 id="解法三上帝视角的点灯游戏并查集分层优化">解法三：上帝视角的点灯游戏（并查集+分层优化）
&lt;/h5>&lt;p>前两种解法虽然都能过，但你心里总觉得差点意思。二分太磨叽，Dijkstra 虽优雅却仍要供着一座优先队列的大佛。有没有一种思路，能换个角度，彻底绕开这些麻烦？&lt;/p>
&lt;p>深夜 debug 到第 42 杯咖啡时，你突然灵光炸裂：既然我想找一条路，让路上的最小安全值尽量大——那为什么不&lt;strong>直接从最安全的格子开始，一个个&amp;quot;点亮&amp;quot;，直到起点和终点连通&lt;/strong>呢？&lt;/p>
&lt;p>这就像玩一款逆向的点灯游戏：地牢里所有格子一开始都是黑的（不可用），你把它们按安全值从高到低依次点亮。每点亮一个格子，就和它四周已经被点亮的邻居连成一片（并查集合并）。当起点 &lt;code>(0,0)&lt;/code> 和终点 &lt;code>(n-1,n-1) &lt;/code>第一次被连进同一片区域时——恭喜！那一刻你正在点亮的格子的距离值 &lt;code>d&lt;/code>，就是你能获得的最大安全系数。&lt;/p>
&lt;p>为什么？因为你按&lt;strong>从大到小&lt;/strong>的顺序点亮格子，当前这一轮点亮的 d 就是已点亮区域里安全值最小的那一档。起点和终点刚好在这一步连通，说明恰好存在一条全程安全值 ≥ d 的路径，而 d+1 的时候它们还不连通。这不就是最优解嘛——无需二分，无需优先队列，一个排序加一个并查集就搞定了。&lt;/p>
&lt;p>你兴奋地搓搓小手，准备重新跑一遍多源 BFS ，在算出每个格子到最近小偷的距离时，把所有格子塞进一个 cells 列表里，然后按距离逆序遍历（实际上BFS 天然保证了列表里的格子是按距离递增排列的，逆序就是从大到小，连 sort 都省了），再用一个带 rank 的 UnionFind 类来维护连通性——每点亮一个格子，就和四周已经被点亮的邻居合并，然后检查起点和终点是否在同一个集合里。稳如老狗。&lt;/p>
&lt;p>等等。&lt;/p>
&lt;p>你你盯着刚跑完的多源 BFS 发了一会儿呆，忽然醒悟：&lt;strong>BFS 本身就是按距离一层一层扩散&lt;/strong>的啊！ 小偷在距离 0 的层，然后距离 1 的层，距离 2 的层……这分明就是天然的分组！早知如此，在 BFS 的时候顺手把同一距离的格子收集进 &lt;code>groups[d]&lt;/code> 里，岂不比事后再排序聪明一百倍？&lt;/p>
&lt;p>于是你立即改写 BFS，分层收集格子。等 BFS 结束，你手里不仅有一份安全地图，还多了一份从 0 到最大距离的分层花名册，直接&lt;strong>从最大距离倒序遍历&lt;/strong>，一层一层点亮，连排序的 O(n² log n) 都省成了 O(n²)，常数小到令人发指。&lt;/p>
&lt;p>既然分层已天然就绪，你索性把并查集也精简到底：什么 &lt;code>UnionFind&lt;/code> 类、花里胡哨的封装统统不要。就一个 &lt;code>fa&lt;/code> 数组（&lt;code>list&lt;/code> 存父节点），外加一个带路径压缩的 &lt;code>find&lt;/code> 函数，一行 &lt;code>fa[x] = find(fa[x])&lt;/code> 搞定一切。合并时直接 &lt;code>fa[find(a)] = find(b)&lt;/code>，简单粗暴，却能在近乎 O(1) 的均摊时间内把两个格子所在的区域焊在一起。&lt;/p>
&lt;p>先 BFS 分层建图，再逆序点灯 + 极简并查集判定连通，一气呵成。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">maximumSafenessFactor&lt;/span>(grid):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n &lt;span style="color:#f92672">=&lt;/span> len(grid)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> grid[&lt;span style="color:#ae81ff">0&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">or&lt;/span> grid[n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>][n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 多源BFS，同时按距离分层 &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dis &lt;span style="color:#f92672">=&lt;/span> [[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> n &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q &lt;span style="color:#f92672">=&lt;/span> []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 初始化：所有小偷位置入队&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(n):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(n):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> grid[i][j] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q&lt;span style="color:#f92672">.&lt;/span>append((i, j))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dis[i][j] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> groups &lt;span style="color:#f92672">=&lt;/span> [q] &lt;span style="color:#75715e"># groups[d]存储所有距离为d的点（分层记录）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> q:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tmp &lt;span style="color:#f92672">=&lt;/span> q &lt;span style="color:#75715e"># 当前层的所有点&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q &lt;span style="color:#f92672">=&lt;/span> []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i, j &lt;span style="color:#f92672">in&lt;/span> tmp:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> x, y &lt;span style="color:#f92672">in&lt;/span> (i &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, j), (i &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, j), (i, j &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>), (i, j &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">&amp;lt;=&lt;/span> x &lt;span style="color:#f92672">&amp;lt;&lt;/span> n &lt;span style="color:#f92672">and&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">&amp;lt;=&lt;/span> y &lt;span style="color:#f92672">&amp;lt;&lt;/span> n &lt;span style="color:#f92672">and&lt;/span> dis[x][y] &lt;span style="color:#f92672">&amp;lt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dis[x][y] &lt;span style="color:#f92672">=&lt;/span> len(groups) &lt;span style="color:#75715e"># 新一层的距离&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q&lt;span style="color:#f92672">.&lt;/span>append((x, y))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> groups&lt;span style="color:#f92672">.&lt;/span>append(q) &lt;span style="color:#75715e"># 将当前层加入groups&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 极简并查集 &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fa &lt;span style="color:#f92672">=&lt;/span> list(range(n &lt;span style="color:#f92672">*&lt;/span> n))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">find&lt;/span>(x: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> fa[x] &lt;span style="color:#f92672">!=&lt;/span> x:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fa[x] &lt;span style="color:#f92672">=&lt;/span> find(fa[x])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> fa[x]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 从最大距离倒序遍历，逐层点亮 &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> d &lt;span style="color:#f92672">in&lt;/span> range(len(groups) &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 激活所有距离为d的点&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i, j &lt;span style="color:#f92672">in&lt;/span> groups[d]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 与四周距离 ≥ d 的已点亮邻居合并&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> x, y &lt;span style="color:#f92672">in&lt;/span> (i &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, j), (i &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, j), (i, j &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>), (i, j &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">&amp;lt;=&lt;/span> x &lt;span style="color:#f92672">&amp;lt;&lt;/span> n &lt;span style="color:#f92672">and&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">&amp;lt;=&lt;/span> y &lt;span style="color:#f92672">&amp;lt;&lt;/span> n &lt;span style="color:#f92672">and&lt;/span> dis[x][y] &lt;span style="color:#f92672">&amp;gt;=&lt;/span> d:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fa[find(x &lt;span style="color:#f92672">*&lt;/span> n &lt;span style="color:#f92672">+&lt;/span> y)] &lt;span style="color:#f92672">=&lt;/span> find(i &lt;span style="color:#f92672">*&lt;/span> n &lt;span style="color:#f92672">+&lt;/span> j)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 检查起点和终点是否连通&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> find(&lt;span style="color:#ae81ff">0&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> find(n &lt;span style="color:#f92672">*&lt;/span> n &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> d
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>时间复杂度：O(n²)，多源 BFS 分层 O(n²)，倒序点灯并查集近似 O(n²)。
空间复杂度：O(n²)。&lt;/p>
&lt;hr>
&lt;h5 id="尾声">尾声
&lt;/h5>&lt;p>三种解法，三种不同的快乐：二分是铁憨憨的稳妥，Dijkstra 是贪心轻功的优雅，并查集分层是逆向思维的通透。&lt;/p>
&lt;p>无论你选哪条路，底层都是同一张由多源 BFS 绘制的“安全地图”。它告诉你每个格子离最近的坏蛋有多远，而你要做的，不过是在这张图上用自己的方式写一个“勇者不寄”的故事。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode1358 滑动窗口的一体两面：追安全点 vs 数左端点</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-30/</link><pubDate>Tue, 30 Jun 2026 11:28:05 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-30/</guid><description>&lt;h2 id="滑动窗口的一体两面追安全点-vs-数左端点">滑动窗口的一体两面：追安全点 vs 数左端点
&lt;/h2>&lt;p>有一个笔直的街道上住着 &lt;strong>n&lt;/strong> 个居民，每个居民只会塞给你一种魔法糖果：&lt;strong>a 糖&lt;/strong>、&lt;strong>b 糖&lt;/strong>或 &lt;strong>c 糖&lt;/strong>。你是一个勇者，想要挑战一个成就：“在一次&lt;strong>连续&lt;/strong>的逛街中，收集齐a、b、c 三种糖果”。规则很简单：你从某个起点居民开始，一路往东走，到某个终点居民结束。如果在这段路程中，你的口袋里三种糖至少各有一颗，就算一次成功的逛街。你想知道整条街上一共有多少种成功的逛街方案（不同的起点‑终点组合）。&lt;/p>
&lt;hr>
&lt;p>首先考虑一种情况：如果整条街上根本没有 a 糖，或者没有 b 糖，或者没有 c 糖，那你无论怎么逛，都不可能集齐三种，答案注定是 &lt;strong>0&lt;/strong>。&lt;/p>
&lt;p>但假设三种糖都有，游戏正式开始。&lt;/p>
&lt;p>你随便选了个起点 &lt;code>left&lt;/code>，口袋空空地出发了。每经过一个居民，就收下他给的糖，并且在一个小本子上记下每种糖的数量。你必须在某处结束逛街，但有个诅咒：如果你在逛街结束的时候，口袋里的糖果种类不足三种，诅咒就会发动，把你变成一只青蛙。所以你必须一直往前走，直到集齐三种糖，才敢停下。&lt;/p>
&lt;p>你硬着头皮往前走，直到走到某个站点 &lt;code>right&lt;/code> 时，惊喜地发现口袋里的糖果种类终于凑齐了三种！你安全了，可以在这里结束逛街了。更重要的是，你发现只要有了 &lt;code>right&lt;/code>这个安全点，在 &lt;code>right&lt;/code>之后任何一个站点结束都能保证口袋里的三种糖都只多不少，诅咒永远不会发动。所以以 &lt;code>left&lt;/code> 为起点的成功方案，就是从 &lt;code>right&lt;/code> 到街道末尾的所有站点数，也就是 &lt;strong>&lt;code>n - right&lt;/code>&lt;/strong> 种。&lt;/p>
&lt;p>你高兴地记下这个数目。但你是个贪婪的冒险者，怎么能满足于一个起点的统计？你要榨干整条街的剩余价值。于是，你把 &lt;code>left&lt;/code> 居民给你的那颗糖无情地扔掉——因为接下来你打算从 &lt;code>left+1&lt;/code> 出发，&lt;code>left&lt;/code>和她的糖果已经是过去时了。扔掉之后，你瞅了一眼小本子：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>如果本子上三种糖的数量都还大于 0&lt;/strong>，说明即使从 &lt;code>left+1&lt;/code> 出发，走到当前的 &lt;code>right&lt;/code> 也已经是安全的，你连一步都不用多走，就可以立刻收割同样的 &lt;code>n - right&lt;/code> 种方案。于是你美滋滋地把这个数又加了一遍。&lt;/li>
&lt;li>&lt;strong>如果扔掉 &lt;code>left&lt;/code> 的糖后，某种糖的数量变成了 0&lt;/strong>，口袋里的种类掉回了两种——不好！青蛙诅咒感应到了你的软弱，开始蠢蠢欲动！ 你吓得赶紧从当前的 &lt;code>right&lt;/code> 继续向东跑，一边跑一边收新的糖果，直到再次集齐三种，找到一个新安全点 &lt;code>right'&lt;/code>。此时你才长舒一口气，把 &lt;code>n - right'&lt;/code> 加入账本。&lt;/li>
&lt;/ul>
&lt;p>你就这样不断重复：扔掉最左边起点的糖，如果种类不够三就拼命往右跑，直到再次安全；如果够三种就直接算账。直到你的起点 &lt;code>left&lt;/code> 越过了街道尽头，这场与青蛙诅咒的博弈才算结束。而你账本上累加的数字，就是所有的成功逛街方案数。&lt;/p>
&lt;hr>
&lt;p>这个不断调整左起点、右安全点的过程，就是&lt;strong>滑动窗口&lt;/strong>。你的左指针 &lt;code>left&lt;/code> 就是窗口左边界，右指针 &lt;code>right&lt;/code> 是右边界，窗口里的糖果种类始终维持在“刚好集齐三种”的最左安全位置。每当种类够三，你就用 &lt;code>n - right&lt;/code> 收割一波答案，然后左指针右移，窗口收缩；一旦种类掉下去，就立刻扩展右指针去救急。两个指针都只往右不回头，全程 &lt;strong>O(n)&lt;/strong>，完美通关。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">numberOfSubstrings&lt;/span>(s: str) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n &lt;span style="color:#f92672">=&lt;/span> len(s)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt &lt;span style="color:#f92672">=&lt;/span> {&lt;span style="color:#e6db74">&amp;#39;a&amp;#39;&lt;/span>: &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;b&amp;#39;&lt;/span>: &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;c&amp;#39;&lt;/span>: &lt;span style="color:#ae81ff">0&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> left &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> right &lt;span style="color:#f92672">in&lt;/span> range(n):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 右指针扩展，收下糖果&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt[s[right]] &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 当三种糖果都至少有一颗时，可以安全结束逛街&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> cnt[&lt;span style="color:#e6db74">&amp;#39;a&amp;#39;&lt;/span>] &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">and&lt;/span> cnt[&lt;span style="color:#e6db74">&amp;#39;b&amp;#39;&lt;/span>] &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">and&lt;/span> cnt[&lt;span style="color:#e6db74">&amp;#39;c&amp;#39;&lt;/span>] &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 以 left 为起点的所有安全终点数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">+=&lt;/span> n &lt;span style="color:#f92672">-&lt;/span> right
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 扔掉 left 的糖果，左指针右移&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt[s[left]] &lt;span style="color:#f92672">-=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> left &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>时间复杂度：O(n)，每个指针最多移动 n 次。空间复杂度：O(1)，只维护了一个固定大小的计数字典。&lt;/p>
&lt;hr>
&lt;p>上面的思路直白得像刚拿到驾照的新手上路——左指针每次抬离合都担心熄火。你总觉得还能再优雅一点。&lt;/p>
&lt;p>你决定换个角度看这个问题。&lt;/p>
&lt;p>刚才是固定左端点，辛辛苦苦找第一个合法的右端点。但如果反过来——固定右端点，去找&lt;strong>最远的合法左端点&lt;/strong>呢？&lt;/p>
&lt;p>假设你现在站在位置 &lt;code>right&lt;/code>，刚收下这颗糖。此时窗口 &lt;code>[left, right]&lt;/code> 内三种糖都齐了。那么对于任意一个比 &lt;code>left&lt;/code> 还靠左的起点 &lt;code>left'&lt;/code>，子串 &lt;code>s[left':right+1]&lt;/code> 里的糖果只会更多，肯定也包含三种。&lt;/p>
&lt;p>也就是说，以 &lt;code>right&lt;/code> 为终点的合法子串数量，恰好就是 &lt;code>left&lt;/code> 的值（左端点可以选 0, 1, 2, …, left-1，一共 left 种可能）。这让你想到了一个更优雅的滑动窗口实现——&lt;/p>
&lt;p>你不再需要每找到一个安全点就去算 &lt;code>n - right&lt;/code>，而是站在每个右端点上，只问一句话——“最靠右的、让窗口刚好不满足三种糖的左边界在哪？”这个左边界就是 &lt;code>left&lt;/code>，而 &lt;code>0&lt;/code> 到 &lt;code>left-1&lt;/code> 统统合法。&lt;/p>
&lt;p>于是你可以在一次遍历中，一边往右走一边默默维护这个 &lt;code>left&lt;/code>，每走一步就把 &lt;code>left&lt;/code> 加到答案里，连内层 &lt;code>while&lt;/code> 里收割答案的环节都省了，循环结构更干净。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">numberOfSubstrings&lt;/span>(s: str) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n &lt;span style="color:#f92672">=&lt;/span> len(s)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt &lt;span style="color:#f92672">=&lt;/span> {&lt;span style="color:#e6db74">&amp;#39;a&amp;#39;&lt;/span>: &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;b&amp;#39;&lt;/span>: &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;c&amp;#39;&lt;/span>: &lt;span style="color:#ae81ff">0&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> left &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> right &lt;span style="color:#f92672">in&lt;/span> range(n):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt[s[right]] &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 只要三种糖都齐了，就拼命往右推左边界，直到刚好不齐&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> cnt[&lt;span style="color:#e6db74">&amp;#39;a&amp;#39;&lt;/span>] &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">and&lt;/span> cnt[&lt;span style="color:#e6db74">&amp;#39;b&amp;#39;&lt;/span>] &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#f92672">and&lt;/span> cnt[&lt;span style="color:#e6db74">&amp;#39;c&amp;#39;&lt;/span>] &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt[s[left]] &lt;span style="color:#f92672">-=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> left &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 此时 left 正好是第一个让窗口不满足三种糖的左端点 =&amp;gt; 0 到 left-1 都合法，共 left 个&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">+=&lt;/span> left
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>时间复杂度:O(n)。空间复杂度:O(1)。&lt;/p>
&lt;hr>
&lt;p>写完你长舒一口气，两个版本都像你的亲儿子一样珍贵。一个追着安全点跑，紧张刺激，像在玩神庙逃亡；一个固定右端点默默计数，像个老会计拨算盘，从容不迫。但无论哪个，青蛙诅咒都只能蹲在街角干瞪眼——你的滑动窗口指针永远不回退，时间复杂度 O(n)，空间 O(1)，一丝破绽都没有。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode1967 村长云服务器 IV：当暴力美学遇上AC自动机</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-29/</link><pubDate>Mon, 29 Jun 2026 15:03:17 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-29/</guid><description>&lt;h3 id="村长云服务器-iv当暴力美学遇上ac自动机">💻村长云服务器 IV：当暴力美学遇上AC自动机！
&lt;/h3>&lt;p>村长抱着那台熟悉的二手服务器冲进院子，机箱上的标签从&amp;rsquo;村长云服务器·公测版&amp;rsquo; 又升级成了&amp;rsquo;&amp;lsquo;村长云服务器·旗舰版&amp;rsquo;&amp;rsquo;——手写字歪歪扭扭，一看就是老头自己拿马克笔描的。风扇还是那个风扇，嗡嗡声堪比直升机，升了版本号没升硬件，典型的“软件升级，硬件抗压”。&lt;/p>
&lt;p>“小白！老板娘说抽奖系统现在流量暴涨，用户发的弹幕里老有人刷敏感词！什么&amp;rsquo;代抽&amp;rsquo;、&amp;lsquo;外挂&amp;rsquo;、&amp;lsquo;假一赔十&amp;rsquo;——把评论区搞得乌烟瘴气！”村长把显示器往桌上一搁，屏幕上滚动着一条超长弹幕文本，旁边密密麻麻列着一百多个敏感词，从&amp;quot;代抽&amp;quot;到&amp;quot;假货&amp;quot;，从&amp;quot;退款&amp;quot;到&amp;quot;差评&amp;quot;，琳琅满目，活像一本黑话词典。&lt;/p>
&lt;p>“老板娘要我上线一个‘敏感词拦截系统’，统计文本里到底命中了多少个敏感词！” 村长咽了口唾沫：“她还说了，如果今天搞不定，就把我的服务器拿去垫桌角！”&lt;/p>
&lt;p>我瞥了眼数据范围：&lt;code>word&lt;/code> 长度 100，&lt;code>patterns&lt;/code> 数量 100，每个模式串长度不超过 100——这任务倒是不难。&lt;/p>
&lt;hr>
&lt;h4 id="暴力一行流">暴力一行流
&lt;/h4>&lt;p>“这不就是找子串吗？”我嘴角一勾，“Python 里有 in 运算符，一行流搞定！看我的 Pythonic 大法——”&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">numOfStrings&lt;/span>(patterns: List[str], word: str) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> sum(p &lt;span style="color:#f92672">in&lt;/span> word &lt;span style="color:#66d9ef">for&lt;/span> p &lt;span style="color:#f92672">in&lt;/span> patterns)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>一气呵成。我得意地晃了晃鼠标，“这就叫站在巨人的肩膀上——哦不，是站在 CPython 的肩膀上！&lt;code>in&lt;/code> 运算符底层调用的是 C 实现的字符串查找算法，常数小、效率高，对于这种小规模数据绰绰有余。人生苦短，我用 Python，能一行解决的事，绝不多写第二行！”&lt;/p>
&lt;p>话音未落，后脑勺&amp;quot;啪&amp;quot;地挨了轻轻一下——“一行流能 AC，是因为评测机今天心情好，数据没上强度。”&lt;/p>
&lt;p>老勇者不知何时已站在我身后，手眼神锐利如刀：“如果 &lt;code>patterns&lt;/code> 有十万个，&lt;code>word&lt;/code> 长度百万，你这 &lt;code>in&lt;/code> 每次都要从头扫描 &lt;code>word&lt;/code>。数据量一上去，CPython 的肩膀也扛不住。”&lt;/p>
&lt;p>我愣了一下：“呃……十万个模式串？百万长度的文本？老板娘的抽奖系统怎么可能会有这么大流量？”&lt;/p>
&lt;p>老勇者微微一笑：“年轻人，格局打开。你现在做的小题，是未来大战的预演。听说过多模式匹配吗？那可是搜索引擎、病毒扫描、内容审核系统的核心武器。今天好好学一招，将来遇到千万级数据就不至于被按在地上摩擦。”&lt;/p>
&lt;hr>
&lt;h4 id="多模式匹配">多模式匹配
&lt;/h4>&lt;p>“小子，听好了。”老勇者从我的本子上撕了张纸，拿起笔就开始画，“在一个文本串中同时查找多个模式串——这叫多模式匹配问题（Multi-Pattern Matching）。对于这种问题，有几把经典武器。”&lt;/p>
&lt;p>“第一把，&lt;strong>暴力遍历&lt;/strong>。”他指了指屏幕上的 &lt;code>in&lt;/code> 操作符，“就是你刚才那行代码。每个模式串独立扫描 word，相当于派一百个侦探，每个人都要把整栋楼从头搜到尾。数据小的时候确实能用，数据一大，就是百人团灭现场。O(m × n)，不解释。”&lt;/p>
&lt;p>“第二把，&lt;strong>KMP 单模式优化&lt;/strong>。”他在纸上画了一排箭头，“预处理模式串，构建一个部分匹配表，在失配时跳过已经匹配过的前缀，避免重复比较。相当于每个侦探都带着一张‘搜过的地方不用再搜’的线索图。但问题是——你还是要派一百个侦探分别搜一遍楼——KMP 只优化了单个侦探的效率，没减少侦探的数量。有 m 个模式串，就要跑 m 次 KMP。”&lt;/p>
&lt;p>“第三把：&lt;strong>Trie 树（字典树）&lt;/strong>。”老勇者在纸上又画了一棵树，节点上标满了字母，“把所有模式串插进一棵树里，共享前缀。比如 &amp;lsquo;abc&amp;rsquo; 和 &amp;lsquo;abd&amp;rsquo;，它们的前缀 &amp;lsquo;ab&amp;rsquo; 可以共用，走到分叉路口再各走各的。空间换时间。”&lt;/p>
&lt;p>他停顿了一下，用笔重重一点：“但 Trie 有个致命缺陷：当你在某个节点失配时，必须回到根节点重新开始。 比如你匹配了 &amp;lsquo;abcde&amp;rsquo;，下一个字符不是 &amp;lsquo;f&amp;rsquo;，你就得从根节点的 &amp;lsquo;a&amp;rsquo; 重新来。文本串的指针要回溯，最坏情况依然是 O(n × m)。相当于侦探走到死胡同，必须跑回大楼入口重新找路。纯 Trie 没有记忆，不会跳跃，只能硬着头皮回溯。”&lt;/p>
&lt;p>他扫了我一眼，确认我跟上了节奏，然后在纸的正中央写下四个大字，字迹遒劲有力：&lt;/p>
&lt;p>“第四把：&lt;strong>AC 自动机（Aho-Corasick Automaton）&lt;/strong>。”&lt;/p>
&lt;p>“多模式匹配的终极武器。”老勇者的声音低沉而充满力量，“ Trie 树 + KMP 失败指针思想的融合体——Trie 负责存储所有模式串，KMP 的失配思想负责处理失配时的跳转。所有模式串构建成一棵 Trie，然后加上两个指针——fail 和 last——让文本串在自动机上只跑一趟，就能同时匹配所有模式串。”&lt;/p>
&lt;p>他在 Trie 树上画了一堆虚线箭头：“核心在于两个指针：&lt;strong>fail 指针（失配指针）&lt;/strong> 和 &lt;strong>last 指针（后缀链接）&lt;/strong>！”&lt;/p>
&lt;p>&lt;strong>Fail 指针：失配时的传送门&lt;/strong>&lt;/p>
&lt;p>“核心一：&lt;strong>fail 指针&lt;/strong>。fail 指针指向的是&amp;rsquo;当前字符串的最长真后缀&amp;rsquo;，且这个后缀也是某个模式串的前缀。” 老勇者指了指图上的节点，“当你在当前节点失配（下一个字符不匹配）时，就跳到 &lt;code>fail&lt;/code> 指向的节点继续尝试匹配，不需要回到根节点重新开始！”&lt;/p>
&lt;p>他顿了顿：“比如你匹配了 &amp;lsquo;abcd&amp;rsquo;，失配了，fail 指针可能指向 &amp;lsquo;bcd&amp;rsquo;、&amp;lsquo;cd&amp;rsquo; 或 &amp;rsquo;d&amp;rsquo; 对应的节点——具体是哪个，取决于哪个后缀同时也是某个模式串的前缀。”&lt;/p>
&lt;p>“这和 KMP 的 next 数组异曲同工，但它是跨模式串的——KMP 只在单个模式串内部跳转，AC 自动机的 fail 可以在不同模式串之间跳跃。相当于侦探在迷宫里走到死胡同，不是传送回城，而是通过密道直接跳到另一条分支上。文本串的指针永不回溯——这是 AC 自动机的灵魂。”&lt;/p>
&lt;p>&lt;strong>Last 指针：直达模式串末尾的捷径&lt;/strong>&lt;/p>
&lt;p>“核心二：&lt;strong>last 指针&lt;/strong>。”他继续画箭头，“ fail 指针有个小问题：它跳到的节点，不一定是某个模式串的末尾。 比如你匹配到 &amp;lsquo;abcde&amp;rsquo;，fail 指针可能跳到 &amp;lsquo;cde&amp;rsquo;，但 &amp;lsquo;cde&amp;rsquo; 可能只是个中间节点，不是完整的模式串。如果只靠 fail，你还得继续沿着 fail 链一层一层往上跳，直到找到某个模式串的末尾，或者跳回根节点。”&lt;/p>
&lt;p>“这时候就需要 last 指针了——后缀链接，直接指向 fail 链上第一个是模式串末尾的节点。如果整条 fail 链上都没有模式串末尾，last 就指向根节点。它相当于 fail 链上的‘直达电梯’——不用一层一层爬楼梯，直接按顶层按钮就到了。”&lt;/p>
&lt;p>他笔锋一转：“回到刚才的例子。你站在 ‘abcde’ 节点，想收集所有以当前位置结尾的模式串。先检查自己：如果是模式串末尾，记下来。然后沿着 last 指针一跳，直接飞到 fail 链上下一个模式串末尾（比如 ‘cde’ 如果恰好是某个模式串，last 就指向它）。再检查，再跳，直到跳到根节点为止。整个过程，你只访问了模式串末尾节点，中间那些不是末尾的节点全被跳过了。这就是 O(1) 均摊时间的秘密——每收集一个命中才走一步，零命中零步，十个命中十步。比沿着 fail 链一个一个节点傻找，效率高了一个数量级。”&lt;/p>
&lt;p>老勇者放下笔，总结道：“AC 自动机的生命周期分三步。第一步，构建 Trie，把所有模式串插进去。第二步，构建 fail 和 last 指针，BFS 一遍搞定。第三步，查询文本串，逐个读入字符，在自动机上转移状态——失配时靠 fail 跳转，命中时靠 last 收集。时间复杂度 O(n + m + z)，n 是文本长，m 是所有模式串总长，z 是匹配次数。”&lt;/p>
&lt;p>他盯着我，一字一顿地说：“看到没？跟模式串数量无关。 相当于只派一个侦探，但他脑子里装着所有敏感词的线索地图，还有传送门（fail）和快捷通道（last），搜一遍楼就把所有敏感词全揪出来。”&lt;/p>
&lt;p>我听得头皮发麻：“把暴力匹配的重复劳动，通过预处理转化成智能跳转？”&lt;/p>
&lt;p>“没错！”老勇者眼中闪过一丝赞许，“AC 自动机的思想非常优雅——它把多模式匹配问题转化成了确定有限状态机（DFA）的问题。你每读入一个字符，就在状态机里转移一次，瞬间就能知道当前匹配到了哪些模式串。这就像玩游戏时开了全图挂+自动寻路，一眼就能看到所有敌人的位置，还能自动规划最优路径。”&lt;/p>
&lt;p>我感觉灵光一闪：“AC 自动机就是把 Trie 树上的匹配路径编码成了状态，fail 指针就是失配时的状态转移！”&lt;/p>
&lt;p>“悟性不错。”老勇者难得地点点头，“那现在，想不想亲手实现一把这个&amp;rsquo;全图挂&amp;rsquo;？”&lt;/p>
&lt;p>我摩拳擦掌：“当然！给我代码模板，我要开始抄——哦不，是开始学习了！”&lt;/p>
&lt;hr>
&lt;h4 id="ac-自动机">AC 自动机
&lt;/h4>&lt;p>&lt;strong>第一步：定义 Trie 树节点&lt;/strong>&lt;/p>
&lt;p>首先，Trie 树的每个节点代表一个字符，从根节点到某个节点的路径就是一个字符串。需要四个字段：&lt;code>son&lt;/code> 数组存储 26 个字母的子节点，&lt;code>fail&lt;/code> 是失配跳转指针，&lt;code>last&lt;/code> 是直达下一个模式串末尾的后缀链接，&lt;code>cnt&lt;/code> 用来记录这个节点是多少个模式串的结尾。&amp;quot;&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Node&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> __slots__ &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;son&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;fail&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;last&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;cnt&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> __init__(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>son &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#66d9ef">None&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">26&lt;/span> &lt;span style="color:#75715e"># 26个小写字母的子节点（a-z对应索引0-25）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>fail &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span> &lt;span style="color:#75715e"># 失配指针：当前字符不匹配时跳到哪里&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>last &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span> &lt;span style="color:#75715e"># 后缀链接：快速跳到下一个一定是模式串末尾的节点&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>cnt &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#75715e"># 该节点是多少个模式串的末尾（计数器）&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>第二步：构建 AC 自动机&lt;/strong>&lt;/p>
&lt;p>接下来，把所有模式串插入 Trie 树。这部分和普通字典树一模一样——遍历每个字符，沿着 &lt;code>son&lt;/code> 往下走，没有子节点就新建，走到末尾时 &lt;code>cnt++&lt;/code>，表示这里是一个模式串的终点。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">AhoCorasick&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> __init__(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>root &lt;span style="color:#f92672">=&lt;/span> Node()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">put&lt;/span>(self, pattern: str) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cur &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>root
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> ch &lt;span style="color:#f92672">in&lt;/span> pattern:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> i &lt;span style="color:#f92672">=&lt;/span> ord(ch) &lt;span style="color:#f92672">-&lt;/span> ord(&lt;span style="color:#e6db74">&amp;#39;a&amp;#39;&lt;/span>) &lt;span style="color:#75715e"># 字符转索引&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> cur&lt;span style="color:#f92672">.&lt;/span>son[i] &lt;span style="color:#f92672">is&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cur&lt;span style="color:#f92672">.&lt;/span>son[i] &lt;span style="color:#f92672">=&lt;/span> Node() &lt;span style="color:#75715e"># 没有这个子节点就创建&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cur &lt;span style="color:#f92672">=&lt;/span> cur&lt;span style="color:#f92672">.&lt;/span>son[i]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cur&lt;span style="color:#f92672">.&lt;/span>cnt &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 标记这是一个模式串的末尾&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>“注意看”，老勇者提醒道，“cnt 是累加的。如果 patterns 里有三个 &amp;ldquo;a&amp;rdquo;，那么表示 &amp;ldquo;a&amp;rdquo; 的那个节点的 cnt 就是 3。这样在查询时，我们一次性就能知道有多少个相同的模式串在这里匹配。”&lt;/p>
&lt;p>&lt;strong>第三步：构建 fail 和 last 指针（BFS）&lt;/strong>&lt;/p>
&lt;p>“重头戏来了——&lt;strong>构建 fail 和 last 指针&lt;/strong>。这是 AC 自动机的灵魂所在。”老勇者搓了搓手，仿佛在展示什么绝世武功，“用 BFS（广度优先搜索）逐层处理整棵 Trie 树。”&lt;/p>
&lt;p>初始化部分：根节点的 &lt;code>fail&lt;/code> 和 &lt;code>last&lt;/code> 都指向自己，因为根节点没有父节点，失配了也只能回到自己。然后遍历根节点的每个子节点：如果 &lt;code>son[i]&lt;/code> 为空，就让它直接指向根节点，形成自循环——这样匹配时遇到不存在的字符能自动回到起点；如果 &lt;code>son[i]&lt;/code> 存在，它的 &lt;code>fail&lt;/code> 和 &lt;code>last&lt;/code> 都指向根节点（第一层节点的 fail 只能是根），然后入队等待后续处理。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">build_fail&lt;/span>(self) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>root&lt;span style="color:#f92672">.&lt;/span>fail &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>root&lt;span style="color:#f92672">.&lt;/span>last &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>root &lt;span style="color:#75715e"># 根节点的 fail 和 last 都指向自己&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q &lt;span style="color:#f92672">=&lt;/span> deque() &lt;span style="color:#75715e"># BFS 队列 &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 初始化第一层节点（根的直接子节点）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i, son &lt;span style="color:#f92672">in&lt;/span> enumerate(self&lt;span style="color:#f92672">.&lt;/span>root&lt;span style="color:#f92672">.&lt;/span>son):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> son &lt;span style="color:#f92672">is&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>root&lt;span style="color:#f92672">.&lt;/span>son[i] &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>root &lt;span style="color:#75715e"># 虚拟子节点：直接指向根节点，方便后续失配时跳转&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> son&lt;span style="color:#f92672">.&lt;/span>fail &lt;span style="color:#f92672">=&lt;/span> son&lt;span style="color:#f92672">.&lt;/span>last &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>root &lt;span style="color:#75715e"># 第一层的 fail 和 last 都指向根&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q&lt;span style="color:#f92672">.&lt;/span>append(son) &lt;span style="color:#75715e"># 入队，后续处理它的子节点&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># BFS 遍历整棵树，逐层构建 fail 和 last 指针&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> q:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cur &lt;span style="color:#f92672">=&lt;/span> q&lt;span style="color:#f92672">.&lt;/span>popleft() &lt;span style="color:#75715e"># 取出队首节点&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i, son &lt;span style="color:#f92672">in&lt;/span> enumerate(cur&lt;span style="color:#f92672">.&lt;/span>son): &lt;span style="color:#75715e"># 遍历当前节点的26个子节点&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> son &lt;span style="color:#f92672">is&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 路径压缩：空子节点直接指向 cur.fail 的对应子节点&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 这样匹配时遇到不存在的字符，一步就能跳到下一个可能匹配的位置&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cur&lt;span style="color:#f92672">.&lt;/span>son[i] &lt;span style="color:#f92672">=&lt;/span> cur&lt;span style="color:#f92672">.&lt;/span>fail&lt;span style="color:#f92672">.&lt;/span>son[i]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 计算 son 节点的 fail 指针：沿着父节点的 fail 链找&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># cur.fail.son[i] 就是&amp;#34;以 cur 的最长可匹配后缀为前缀，且下一个字符也是 ch 的节点&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> son&lt;span style="color:#f92672">.&lt;/span>fail &lt;span style="color:#f92672">=&lt;/span> cur&lt;span style="color:#f92672">.&lt;/span>fail&lt;span style="color:#f92672">.&lt;/span>son[i]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 计算 son 节点的 last 指针：直达下一个一定是模式串末尾的节点&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 如果 fail 节点本身就是某个模式串的末尾（cnt &amp;gt; 0），last 就指向 fail&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 否则指向 fail.last，继续沿着后缀链接往上找&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> son&lt;span style="color:#f92672">.&lt;/span>last &lt;span style="color:#f92672">=&lt;/span> son&lt;span style="color:#f92672">.&lt;/span>fail &lt;span style="color:#66d9ef">if&lt;/span> son&lt;span style="color:#f92672">.&lt;/span>fail&lt;span style="color:#f92672">.&lt;/span>cnt &lt;span style="color:#66d9ef">else&lt;/span> son&lt;span style="color:#f92672">.&lt;/span>fail&lt;span style="color:#f92672">.&lt;/span>last
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> q&lt;span style="color:#f92672">.&lt;/span>append(son) &lt;span style="color:#75715e"># 处理完当前节点，把它加入队列，后续处理它的子节点&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我挠挠头，指着那行 &lt;code>cur.son[i] = cur.fail.son[i] &lt;/code>问：“等等，这里为什么要修改 &lt;code>son&lt;/code> 数组？这不是把原来的 Trie 树结构改了吗？”&lt;/p>
&lt;p>“这就是&lt;strong>路径压缩&lt;/strong>的精髓！”老勇者眼睛一亮，“原本如果 &lt;code>son[i]&lt;/code> 为空，匹配时你得手动沿着 fail 链一层层往上找，直到找到某个节点有 &lt;code>son[i]&lt;/code>。现在直接把空子节点指向 &lt;code>cur.fail.son[i]&lt;/code>，匹配时不管 &lt;code>son[i]&lt;/code> 存不存在，直接 &lt;code>cur = cur.son[i]&lt;/code> 跳过去就完事——要么是真正的子节点，匹配成功；要么是 fail 链上的后备节点，失配后跳转到下一个可能位置。省掉了中间跑路的时间，一步到位。”&lt;/p>
&lt;p>&lt;strong>第四步：查询文本串&lt;/strong>&lt;/p>
&lt;p>最后是查询阶段。从根节点出发，遍历 word 的每个字符。因为路径压缩了，&lt;code>son[i]&lt;/code> 总有值——要么是真正的子节点，要么是 fail 链上的后备节点——所以直接 &lt;code>cur = cur.son[i]&lt;/code> 就行，全程不需要手动判断失配。每到一个新节点，沿着 &lt;code>last&lt;/code> 链收集所有命中的模式串。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Solution&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">numOfStrings&lt;/span>(self, patterns: List[str], word: str) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ac &lt;span style="color:#f92672">=&lt;/span> AhoCorasick()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> pattern &lt;span style="color:#f92672">in&lt;/span> patterns:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ac&lt;span style="color:#f92672">.&lt;/span>put(pattern) &lt;span style="color:#75715e"># 把所有模式串插入 AC 自动机&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ac&lt;span style="color:#f92672">.&lt;/span>build_fail() &lt;span style="color:#75715e"># 构建 fail 和 last 指针&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ord_a &lt;span style="color:#f92672">=&lt;/span> ord(&lt;span style="color:#e6db74">&amp;#39;a&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cur &lt;span style="color:#f92672">=&lt;/span> ac&lt;span style="color:#f92672">.&lt;/span>root &lt;span style="color:#75715e"># 当前状态，从根节点开始&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#75715e"># 统计匹配的模式串数量&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 遍历文本串的每个字符&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> ch &lt;span style="color:#f92672">in&lt;/span> word:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cur &lt;span style="color:#f92672">=&lt;/span> cur&lt;span style="color:#f92672">.&lt;/span>son[ord(ch) &lt;span style="color:#f92672">-&lt;/span> ord_a] &lt;span style="color:#75715e"># 自动机转移，无需判空 &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> match_node &lt;span style="color:#f92672">=&lt;/span> cur
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> match_node&lt;span style="color:#f92672">.&lt;/span>cnt &lt;span style="color:#f92672">&amp;gt;=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>: &lt;span style="color:#75715e"># cnt &amp;gt;= 0 表示这个节点还没被统计过&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">+=&lt;/span> match_node&lt;span style="color:#f92672">.&lt;/span>cnt &lt;span style="color:#75715e"># 累加这个节点是多少个模式串的末尾&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> match_node&lt;span style="color:#f92672">.&lt;/span>cnt &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 标记为已统计，避免重复计数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> match_node &lt;span style="color:#f92672">=&lt;/span> match_node&lt;span style="color:#f92672">.&lt;/span>last &lt;span style="color:#75715e"># 跳到下一个可能是模式串末尾的节点&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我盯着那个 &lt;code>while match_node.cnt &amp;gt;= 0&lt;/code> 的循环问：“为什么要标记为 &lt;code>-1&lt;/code>？这个循环是在干嘛？”&lt;/p>
&lt;p>“避免重复统计同一个模式串。”老勇者解释道，“假设 &lt;code>patterns = [&amp;quot;a&amp;quot;, &amp;quot;ab&amp;quot;, &amp;quot;abc&amp;quot;]，word = &amp;quot;abc&amp;quot;&lt;/code>。当匹配到 &lt;code>'c'&lt;/code> 时，当前节点表示 &lt;code>&amp;quot;abc&amp;quot;&lt;/code>，它本身是模式串末尾，&lt;code>cnt=1&lt;/code>；它的 &lt;code>last&lt;/code> 链可能指向 &lt;code>&amp;quot;ab&amp;quot;&lt;/code> 对应的节点，也是模式串末尾；继续沿着 &lt;code>last&lt;/code> 链走，可能还指向 &lt;code>&amp;quot;a&amp;quot;&lt;/code> 对应的节点。需要沿着这条链把所有匹配的模式串全统计一遍。”&lt;/p>
&lt;p>“但需要统计的是‘patterns 中有多少个字符串是 word 的子串’，而不是‘总共匹配了多少次’。如果 &lt;code>word&lt;/code> 后面又出现了一个 &lt;code>&amp;quot;abc&amp;quot;&lt;/code>，你不能把 &lt;code>&amp;quot;abc&amp;quot;&lt;/code> 再算一遍。所以第一次匹配到时，把 &lt;code>cnt&lt;/code> 累加到答案，然后立刻标记为 -1——之后不管 word 里再出现多少次 &lt;code>&amp;quot;abc&amp;quot;&lt;/code>，while 循环的条件 &lt;code>cnt &amp;gt;= 0&lt;/code> 都不满足，直接跳过。这就叫‘见一次就标记，绝不重复计数’。”&lt;/p>
&lt;p>“哦！”我恍然大悟，“&lt;code>last&lt;/code> 链负责一次匹配中收集所有命中的模式串，&lt;code>cnt = -1&lt;/code> 负责跨匹配去重——两条机制，一个管纵向深度，一个管横向广度，完美配合！”&lt;/p>
&lt;p>“没错！”老勇者赞许地点点头，“这下你真懂了。”&lt;/p>
&lt;hr>
&lt;h4 id="尾声">尾声
&lt;/h4>&lt;p>提交，AC！这次不是靠 CPython 的肩膀，而是靠自动机的铁拳——文本串只扫一遍，跟模式串数量无关。&lt;/p>
&lt;p>&lt;code>p in word&lt;/code> 的暴力一行流，每次 &lt;code>in&lt;/code> 都是独立的扫描，相当于给每个敏感词派一个侦探，让他们各自为战，每个人都要把整栋大楼从头到尾搜一遍；而 AC 自动机，则是把所有的扫描合并成了一次遍历——只派一个侦探，但他脑子里装着所有敏感词的线索地图，脚下踩着传送门（fail 指针），手里拿着快捷通道（last 指针），搜一遍楼就能把所有敏感词连锅端！从 O(m × n) 到 O(n + m)，差距不是常数倍，是数量级的碾压。&lt;/p>
&lt;p>“好！好！好！”村长看着运行结果，乐得合不拢嘴，一把抱住那台还在嗡嗡作响的二手机箱，“这下服务器保住了！我的‘村长云旗舰版’不用去垫桌角了！”&lt;/p>
&lt;p>他激动地抹了把眼泪，但随即又凑到我身边，搓着手问道：“小白啊，真遇到大规模流量——比如老板娘的抽奖系统突然爆火，同时有十万个用户在线疯狂刷屏，老板娘要拦截十万个敏感词、处理百万字的文本……就用这个 AC 自动机，顶得住吗？”&lt;/p>
&lt;p>我看着他那张写满担忧的脸，忍不住笑了：“村长，把心放回肚子里！AC 自动机的时间复杂度是 O(n + m + z)——跟模式串的数量完全无关！就算有一万个敏感词，也只是 Trie 树稍微大一点，查询的时候，文本串依然只需要从头到尾扫一遍！”&lt;/p>
&lt;p>我瞅着屏幕上的AC自动机，掷地有声地说：“有了 AC 自动机，以后就算流量翻十倍、一百倍，你的服务器也照样扛得住！”&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode1846 数组变形记：从混乱到有序的贪心进化</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-28/</link><pubDate>Sun, 28 Jun 2026 16:41:39 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-28/</guid><description>&lt;h2 id="数组变形记从混乱到有序的贪心进化">数组变形记：从混乱到有序的贪心进化
&lt;/h2>&lt;p>你是一个肝帝玩家，背包里塞满了各种等级的装备——从+1的破烂新手剑到+999的传说级神器，应有尽有。&lt;/p>
&lt;p>游戏规则：&lt;/p>
&lt;ul>
&lt;li>你可以把装备降级（+999→+1，血亏但没办法，就当被策划砍了一刀）&lt;/li>
&lt;li>可以调整穿戴顺序（今天想当萌新，明天想装大佬）&lt;/li>
&lt;li>第一件装备必须是 +1（新手村规定，大佬也得从菜鸟做起）&lt;/li>
&lt;li>相邻装备等级差不能超过 1（你不能穿着+1的内裤，外面套+999的铠甲，NPC会报警的）&lt;/li>
&lt;/ul>
&lt;p>问：你这波骚操作下来，最高能凑出多少级的装备？&lt;/p>
&lt;p>这其实是个**「贪心算法」**的经典套路——就像玩《文明6》铺城，你总想把科技值堆到最高，但每个城市只能研究相邻的科技树，不能跳级。你想让最后一个城市科技等级最高？那得从头开始规划，一步一个脚印。&lt;/p>
&lt;hr>
&lt;h5 id="v10贪心--排序">&lt;strong>V1.0：贪心 + 排序&lt;/strong>
&lt;/h5>&lt;p>既然可以任意重排，那最直观的思路就是&lt;strong>从小到大排序&lt;/strong>。为什么？&lt;/p>
&lt;p>想象你在搭一座楼梯，每级台阶最多比上一级高 1。你手里有一堆高度不同的砖块，只能削不能垫（这什么奇葩施工队？）。你想让楼梯尽可能高，是不是应该：&lt;/p>
&lt;ul>
&lt;li>先把矮砖放前面当底座（+1 的新手砖）&lt;/li>
&lt;li>高砖放后面冲顶（+999 的传说砖，可能被削成 +5）&lt;/li>
&lt;/ul>
&lt;p>你要是反过来，把 +999 的砖放第一块，对不起，规则说第一块必须是 +1，你直接血亏 998 级。把高砖放前面，后面接个矮砖，瞬间断档，就像你穿了神装头盔，脚上还是草鞋，整体战力被短板卡死，白瞎了前面的高等级。&lt;/p>
&lt;p>这就是贪心的本质：排序后，让每个位置尽可能贴近上界。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">maximumElementAfterDecrementingAndRearranging&lt;/span>(arr: List[int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> arr&lt;span style="color:#f92672">.&lt;/span>sort()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> arr[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, len(arr)):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> arr[i] &lt;span style="color:#f92672">=&lt;/span> min(arr[i], arr[i&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> arr[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>就像打 Boss 时的连招系统：你必须按顺序出招，每一招的伤害不能超过上一招 +1，但你可以通过&amp;quot;降伤&amp;quot;来保证连招不断。最终你的总伤害取决于连招长度和每一步的上限。&lt;/p>
&lt;p>时间复杂度 O(n log n)，空间复杂度 O(1)（如果不算排序的栈空间）。&lt;/p>
&lt;p>但是！这个解法需要修改原数组。作为一个有洁癖的程序员，你眉头紧锁——在真正的工程里，修改入参就像在多人协作时擅自改别人代码——万一其他线程还在读这个数组呢？ 万一上游调用方还指着原始数据做日志呢？ 这感觉就像你在《魔兽世界》团队副本里，突然把坦克的装备全换成布甲——团灭只在一瞬间。&lt;/p>
&lt;p>更何况，每次循环都要访问 arr[i-1]，虽然没啥性能问题，但总让人觉得代码有点&amp;quot;紧耦合&amp;quot;，不够优雅。就像你写代码时非要依赖上一个函数的返回值，而不是用参数传递，调试的时候能让你怀疑人生。&lt;/p>
&lt;p>于是你开始琢磨：能不能不碰原数组，用一个变量来搞定？&lt;/p>
&lt;hr>
&lt;h5 id="v20极简贪心状态压缩">&lt;strong>V2.0：极简贪心，状态压缩&lt;/strong>
&lt;/h5>&lt;p>让我们来做个&lt;strong>状态压缩&lt;/strong>：我们真的需要知道数组的具体形态吗？&lt;/p>
&lt;p>不需要！我们只需要知道**&amp;ldquo;当前能构建的最大值&amp;rdquo;**是多少——这就像玩游戏时，你只看角色等级面板上的数字，不需要记住他穿了什么装备、背包里有什么道具——等级是核心状态，装备只是实现细节。&lt;/p>
&lt;p>也就是说，把状态压缩成一个变量 ans，初始为 0（还没开始构建）。然后遍历排序后的数组：&lt;/p>
&lt;ul>
&lt;li>
&lt;p>如果当前元素 a &amp;gt; ans：说明我们可以用它来把 ans 提升 1（因为相邻差 1，所以最多 +1）&lt;/p>
&lt;/li>
&lt;li>
&lt;p>否则：这个元素太小或刚好，无法帮助我们提升最大值，直接跳过——就像打《原神》时抽到重复角色，虽然有点用（可以当狗粮），但不能让你突破等级上限。&lt;/p>
&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">maximumElementAfterDecrementingAndRearranging&lt;/span>(arr: List[int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> arr&lt;span style="color:#f92672">.&lt;/span>sort() &lt;span style="color:#75715e"># 先排个序，从小到大排列&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#75715e"># 当前能达到的最高等级（初始为 0，新手村都没出）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> a &lt;span style="color:#f92672">in&lt;/span> arr: &lt;span style="color:#75715e"># 遍历每个装备&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> a &lt;span style="color:#f92672">&amp;gt;&lt;/span> ans: &lt;span style="color:#75715e"># 如果这个装备等级 &amp;gt; 当前最高等级&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 那就能提升 1 级（贪心：能升就升，绝不客气）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> ans &lt;span style="color:#75715e"># 返回最终等级&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>你品，你细品：这不就是《文明6》里的科技树吗？你每研究一个前置科技，下一个科技才能解锁。如果你的科技点（a）大于当前时代（ans），你就可以进入下一个时代（ans += 1）。如果小于等于，说明你卡科技了，等下一轮吧。&lt;/p>
&lt;p>空间复杂度：O(1)，只用了一个变量 ans，环保且不修改原数组，线程安全！&lt;/p>
&lt;p>但是！ 排序法始终有一个痛点：&lt;strong>O(n log n) 的时间复杂度&lt;/strong>。对于 &lt;code>n=10^5&lt;/code>，虽然也够用，就像你用60Hz显示器打游戏，能玩——但作为一个有追求的程序员，你怎么会满足于此？——毕竟，性能优化就像游戏里的&amp;quot;帧数提升&amp;quot;，能 60 帧绝不 30 帧，能 O(n) 绝不 O(n log n)。&lt;/p>
&lt;hr>
&lt;h5 id="v30计数排序--桶思想">&lt;strong>V3.0：计数排序 + 桶思想&lt;/strong>
&lt;/h5>&lt;p>&lt;strong>我们其实不需要完整的排序。&lt;/strong>&lt;/p>
&lt;p>为什么？因为最终答案的上界就是 &lt;code>n&lt;/code>（数组长度），我们最多构建出 &lt;code>[1, 2, 3, ..., n]&lt;/code>，最大值就是 &lt;code>n&lt;/code>。&lt;/p>
&lt;p>为什么答案上界是 &lt;code>n&lt;/code>？因为数组长度为 &lt;code>n&lt;/code>，最理想情况就是 &lt;code>[1, 2, 3, ..., n]&lt;/code>，最大值 = &lt;code>n&lt;/code>。即使所有元素都是 &lt;code>10^9&lt;/code>，你也只能构建出 &lt;code>1, 2, 3, ..., n&lt;/code> 这样的序列，最大值就是 &lt;code>n&lt;/code>。所以，&lt;strong>大于 n 的元素和等于 n 的元素没有区别&lt;/strong>——它们都只是“足够大的砖块”，都可以用来帮你一步步往上堆。&lt;/p>
&lt;p>&lt;strong>这就像给酒店分配房间&lt;/strong>：不管客人多有钱，最多只能住总统套房（第 &lt;code>n&lt;/code> 层）。再有钱也只能住那里，不能住到天上去。&lt;/p>
&lt;p>既然值域有限（1 ~ n），那我直接&lt;strong>空间换时间&lt;/strong>，上&lt;strong>计数排序&lt;/strong>（桶排序）！&lt;/p>
&lt;p>不需要玩什么快排归并，直接用桶统计每个值出现多少次。因为值域是 &lt;code>n&lt;/code>，所以计数数组长度也就是 &lt;code>n+1&lt;/code>，小得可怜——对于 n=10^5 来说，开个 100001 大小的数组，也就几百 KB。&lt;/p>
&lt;p>核心思路分三步：&lt;/p>
&lt;ul>
&lt;li>第一步：统计资源 遍历原数组，把每个元素塞进对应的桶里：
如果值 &amp;lt;= n，直接计入对应的桶 cnt[value] += 1
如果值 &amp;gt; n，统一计入 cnt[n] += 1（总统套房原则：再有钱也只能住顶层）&lt;/li>
&lt;li>第二步：贪心升级 从 1 到 n 依次处理每个桶，用贪心的思想计算能达到的最大值。
我们希望构建的序列是 1, 2, 3, 4, 5, &amp;hellip;，每遇到一个足够大的元素，ans 就 +1。
用计数数组 cnt[i] 表示有多少个元素的值可以用来构建等级 i。
当我们处理到值 i 时，我们有 cnt[i] 个&amp;quot;资源&amp;quot;可以用来把 ans 往上推。但 ans 不能超过 i，因为你最多只能构建到 i——就像你手里只有 1 到 5 级的砖块，再怎么堆也堆不出 6 级台阶。这是规则限制，不是技术问题。&lt;/li>
&lt;li>第三步：返回答案 遍历完所有桶后，ans 就是最终能达到的最高等级。&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">maximumElementAfterDecrementingAndRearranging&lt;/span>(arr: List[int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n &lt;span style="color:#f92672">=&lt;/span> len(arr)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (n &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#75715e"># 计数桶，下标 0 不用，1~n 有效&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> a &lt;span style="color:#f92672">in&lt;/span> arr:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt[min(a, n)] &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 大于 n 的统统塞进 n 号桶（总统套房）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, n &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>): &lt;span style="color:#75715e"># 从 1 到 n 依次处理&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> min(ans &lt;span style="color:#f92672">+&lt;/span> cnt[i], i) &lt;span style="color:#75715e"># 用资源提升，但上限是 i&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>用 O(n) 的桶，换掉 O(n log n) 的排序。就像你在游戏里用“氪金”换“肝度”：虽然花了点空间（桶），但时间上直接起飞——从 O(n log n) 降到 O(n)。&lt;/p>
&lt;hr>
&lt;p>收工！&lt;/p>
&lt;p>从O(n log n)肝到O(n)，从&amp;quot;改入参&amp;quot;进化到&amp;quot;只读不写&amp;quot;，从&amp;quot;看装备&amp;quot;抽象到&amp;quot;只看等级&amp;quot;——这波优化就像给游戏换了块RTX 4090，丝滑得不像话。&lt;/p>
&lt;p>刷完题，你信心满满地打开《艾尔登法环》，准备把&amp;quot;贪心策略&amp;quot;用在Boss战上：先打简单的攒资源，再去挑战女武神。然后发现自己被一锤子砸回篝火——&lt;strong>游戏里的Boss根本不吃你这一套，该秒还是秒，管你+几的装备。&lt;/strong>&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3020 平方峰过山车：平方峰上顶点单，指数飞升序列短</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-27/</link><pubDate>Sat, 27 Jun 2026 17:53:51 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-27/</guid><description>&lt;h2 id="平方峰过山车平方峰上顶点单指数飞升序列短">🎢平方峰过山车：平方峰上顶点单，指数飞升序列短
&lt;/h2>&lt;p>今天一早，村长的飞鸽传书就砸在我桌上——那个奇奇怪怪游乐园又来新活了。上次锯齿过山车的奖金还没结清呢，这次又搞了个什么姐妹项目——平方峰过山车！&lt;/p>
&lt;p>“又是过山车？这游乐园是跟过山车杠上了还是预算只够建一种项目？” 我展开任务书，只见上面画着一张极其诡异的设计图——轨道中间高、两边低，还不是均匀分布的，像一座被镜子对折过的陡峭山峰！&lt;/p>
&lt;p>“这爬坡方式也太反人类了吧！从起点开始，每次不是乘2，是平方！平方！x变成x²，再变成x⁴，一路指数爆炸往上飙，到了峰值再对称地开方降回来——起步是2G的推背感，到山顶直接变成256G，是要把乘客压成二维生物吗？！”&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>平方峰过山车设计规范&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>从数组 nums 中选子集，使子集的元素能排成这样的对称模式：&lt;code>[x,x²,x⁴,...,x^k,...,x⁴,x²,x]&lt;/code>&lt;/li>
&lt;li>也就是说，先不断平方上升到顶，再对称下降&lt;/li>
&lt;li>例如：[2, 4, 16, 4, 2] 、[3, 9, 3] 都符合；但 [2, 4, 8, 4, 2] 不符合（8 不是 4 的平方）&lt;/li>
&lt;li>目标：找出能满足这个模式的最大子集的大小。&lt;/li>
&lt;/ul>&lt;/blockquote>
&lt;p>“这帮人是不是对‘刺激’有什么误解？”我忍不住吐槽，“重力加速度不够，用平方来凑？别的游乐园追求的是‘游客的欢笑’，他们追求的是‘游客的尖叫和扁平化’！”&lt;/p>
&lt;p>但我目光下移，突然看到了村长在图纸最下方用红笔加粗的一行小字：&lt;/p>
&lt;blockquote>
&lt;p>村长注：nums 长度最大 10^5，每个数最大 10^9。奖金翻倍！务必搞定！！&lt;/p>&lt;/blockquote>
&lt;p>“奖金翻倍……”我盯着那四个字，原本吐槽的嘴瞬间闭上了。上次锯齿过山车的钱还没到账，这次翻倍——虽然不知道基数是多少，但翻倍这个词就像游戏里爆出双倍经验卡一样让人心跳加速。&lt;/p>
&lt;p>我十指交叉，发出一声清脆的响声，脑子迅速活动起来——这模式有个致命的特点：序列关于正中心对称。&lt;/p>
&lt;hr>
&lt;h5 id="第一步对称结构">第一步：对称结构
&lt;/h5>&lt;p>对称意味着配对，配对意味着频次。序列里除了中心点可以独一份，其他所有值都必须成对出现。&lt;/p>
&lt;p>也就是说，对于[x, x², x⁴, &amp;hellip;, x^k, &amp;hellip;, x⁴, x², x]，除了山顶那个峰值可以独一份，其他所有值都必须成对出现（左右各一个）。所以，答案一定是个奇数：峰值占一个，左右各一对占两个，像一座完美的山峰。&lt;/p>
&lt;p>结构搞清楚了。那直接暴力枚举每个数作为起点，不断平方往上找，统计最长合法序列，可行吗？&lt;/p>
&lt;p>问题是：枚举每个数作为起点的时候，这个序列会往上爬多高？&lt;/p>
&lt;h5 id="第二步平方增长是最强剪刀">第二步：平方增长是最强剪刀
&lt;/h5>&lt;p>我盯着那个“平方”操作——平方增长是什么概念？2 平方变 4，4 平方变 16，16 平方变 256，256 平方变 65536，65536 平方直接炸穿 10^9。从最小的 2 开始，平方五次就超过任务给出的数值上限了。也就是说，序列的长度绝对不会超过 2×5+1=11——才十一个元素！&lt;/p>
&lt;p>这意味着暴力枚举每个不同的数作为起点时，最多只需要尝试 5-6 次平方操作！总计算量完全可控。平方增长简直就是最强的剪刀，一刀把指数级可能性砍成了常数级。&lt;/p>
&lt;p>“先搞个哈希表统计每个数字出现的次数。”我掏出键盘，“然后对每个数字进行&amp;rsquo;攀登&amp;rsquo;，取最大的序列长度！”&lt;/p>
&lt;h5 id="第三步特判-1">第三步：特判 1
&lt;/h5>&lt;p>等等——手指悬在键盘上，一个极其特殊的数字从脑子里蹦了出来： 1！——1 的平方永远是 1，如果把它当成普通数字处理，平方操作会陷入死循环！&lt;/p>
&lt;p>1 必须单独处理，不能进入循环。序列里可以塞任意多个 1，但任务要求选子集，1 的个数受限于数组里实际出现了多少个 1。如果数组里有 5 个 1，最多取 5 个排成 [1,1,1,1,1]；如果有 4 个 1，最多取 3 个——因为中心点要独一份，两侧必须对称。所以，1 的答案就是：奇数直接取，偶数减一。&lt;/p>
&lt;p>搞定 1 这个特例，把它从哈希表里踢出去，防止后续循环踩坑。&lt;/p>
&lt;h5 id="第四步暴力枚举">第四步：暴力枚举
&lt;/h5>&lt;p>剩下的逻辑就清晰了：枚举每个不同的数作为起点 x，尝试不断平方，看能爬多高。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> collections &lt;span style="color:#f92672">import&lt;/span> Counter
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Solution&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">maximumLength&lt;/span>(self, nums: list[int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt &lt;span style="color:#f92672">=&lt;/span> Counter(nums)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 特判 1：1的平方还是1，只能取奇数个&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> (cnt[&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#f92672">|&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 偶数减一，奇数不变&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">del&lt;/span> cnt[&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#75715e"># 处理完1，踢出去，防止死循环&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> num &lt;span style="color:#f92672">in&lt;/span> set(nums):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> num &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span> &lt;span style="color:#75715e"># 1 已经处理过了&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> temp &lt;span style="color:#f92672">=&lt;/span> num
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 只要当前数至少有2个，就可以左右各放一个&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> cnt&lt;span style="color:#f92672">.&lt;/span>get(temp, &lt;span style="color:#ae81ff">0&lt;/span>) &lt;span style="color:#f92672">&amp;gt;=&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#75715e"># 左右各一个，+2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> temp &lt;span style="color:#f92672">*=&lt;/span> temp &lt;span style="color:#75715e"># 平方，继续上升&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 退出循环时，检查顶点是否存在&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> max(ans, res &lt;span style="color:#f92672">+&lt;/span> (&lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> cnt&lt;span style="color:#f92672">.&lt;/span>get(temp, &lt;span style="color:#ae81ff">0&lt;/span>) &lt;span style="color:#f92672">&amp;gt;=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>O(n) 统计频次，O(n · log log M) 暴力扩展——M 是最大值 10^9，最大序列长度不超过 11，总复杂度约等于 O(n)，干净利落。&lt;/p>
&lt;hr>
&lt;p>村长拿着结果，乐颠颠地往游乐园方向跑了，说这次要是能把锯齿过山车的尾款和平方峰的翻倍奖金一起讨回来——他就给那台二手服务器换个静音风扇。看着老头的背影消失在村口，我忍不住开始好奇这个游乐园的策划到底是什么来头。&lt;/p>
&lt;p>锯齿过山车好歹还是线性增长，相邻节点差不超过1，轨道起伏有规律可循；到了平方峰，直接放飞自我，起步就是平方，指数爆炸往上飙，到了峰值再开方降回来。这脑回路，策划怕不是从数学系毕业的——乘客不在他们的考量范围内，他们在乎的是“这个递推式够不够漂亮”。&lt;/p>
&lt;p>&lt;strong>平方峰上顶点单，指数飞升序列短。&lt;/strong>
&lt;strong>数字一哥提前踢，暴力枚举直通关。&lt;/strong>&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3739 村长云服务器 II：三年日志又何妨，哈希O(n)镇全场</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-26/</link><pubDate>Fri, 26 Jun 2026 11:25:42 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-26/</guid><description>&lt;h2 id="-村长云服务器-ii三年日志又何妨哈希on镇全场">🖥️ 村长云服务器 II：三年日志又何妨，哈希O(n)镇全场
&lt;/h2>&lt;p>上回说到，村长的二手服务器靠着我从暴力到前缀和再到哈希增量的三重大法稳住了阵脚，故障窗口一个不少全揪了出来。老头抱着服务器乐颠颠地去找老板娘邀功。我当时拍着胸脯吹了牛，说三十年也照跑不误。&lt;/p>
&lt;p>才过了一天，牛就真找上门了。今天一大早，村长又抱着那台熟悉的二手服务器冲进来，机箱上的标签竟从“村长云服务器·内测版”升级成了“村长云服务器·公测版”，只是那风扇的咆哮声听起来，像是随时准备原地起飞。&lt;/p>
&lt;p>“小白！老板娘看了昨天的故障窗口报告，非常满意！”村长把显示器往桌上一搁，屏幕上的日志流像瀑布一样疯狂刷屏，滚动速度快得能闪瞎眼，“ 她说‘村长云果然靠谱’，反手就把抽奖活动的规模扩大了十倍！现在不只是小卖部，全村五十家商铺都用上了我的服务器！”&lt;/p>
&lt;p>村长擦着额头的汗，&amp;ldquo;而且这只是开始！老板娘说活动要持续一个月，到时候日志量可能突破百万！我昨天不信邪，想试试你最开始那个暴力法还能不能跑——就是你写的第一版，双重循环那个——结果……”他咽了口唾沫，心有余悸地说，“跑了三分钟还没出结果，老板娘差点就要换老李的服务器了！”&lt;/p>
&lt;p>我接过显示器，扫了一眼数据范围：n = 10^5。十万条日志。暴力O(n²)肯定会被创飞，但昨天在老勇者的点化下，我已经提前从O(n²)一路优化到O(n log n)再到O(n)，哈希表+动态维护f的终极版本就躺在我的魔法书里。&lt;/p>
&lt;p>“村长，您这不是自找苦吃吗？”我靠在椅背上，连袖子都懒得撸，“明明昨天老勇者已经把终极法宝塞您眼前了，您偏要回去翻那本已经淘汰的暴力秘籍。“&lt;/p>
&lt;p>我调出昨天哈希表+增量维护的终极版本。cnt是哈希表，记录每种前缀和出现了几次；s是当前前缀和，f是动态维护的变量，每步更新只需O(1)。一趟遍历，没有嵌套循环，没有二分查找。一个字符都不用改，直接复制粘贴。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> collections &lt;span style="color:#f92672">import&lt;/span> defaultdict
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Solution&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">countMajoritySubarrays&lt;/span>(self, nums: List[int], target: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt &lt;span style="color:#f92672">=&lt;/span> defaultdict(int) &lt;span style="color:#75715e"># 初始化哈希表，记录每个前缀和出现的次数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 哨兵：还没开始时的前缀和为0，出现1次&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> s &lt;span style="color:#f92672">=&lt;/span> f &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#75715e"># ans最终答案，s当前前缀和，f当前合法起点个数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> num &lt;span style="color:#f92672">in&lt;/span> nums:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> num &lt;span style="color:#f92672">==&lt;/span> target:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f &lt;span style="color:#f92672">+=&lt;/span> cnt[s] &lt;span style="color:#75715e"># s 即将变成 s+1，原本前缀和等于 s 的那些起点现在都合法了&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> s &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 更新前缀和&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> s &lt;span style="color:#f92672">-=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 先更新前缀和&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f &lt;span style="color:#f92672">-=&lt;/span> cnt[s] &lt;span style="color:#75715e"># s 已经变成 s-1，原本前缀和等于 s-1 的那些起点现在不合法了&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">+=&lt;/span> f &lt;span style="color:#75715e"># 累加当前位置的贡献&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt[s] &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 将当前前缀和的出现次数加1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>提交，AC。“看到没？”我拍着屏幕上那段哈希表代码，像个刚打赢Boss的菜鸟勇者，“这就是昨天老勇者敲出来的终极奥义——步长±1，增量维护，哈希O(1)查找！整体O(n)通关！O(n)是什么意思？就是日志量翻十倍，时间也只翻十倍！昨天几千条跑了几毫秒，今天十万条顶多也就几十毫秒！”&lt;/p>
&lt;p>村长盯着代码里的&lt;code>cnt[s]&lt;/code>和&lt;code>f&lt;/code>变量，挠了挠头：“等等，让我捋捋……遇到FAIL就&lt;code>s -= 1&lt;/code>然后&lt;code>f -= cnt[s]&lt;/code>，遇到SUCCESS就&lt;code>f += cnt[s]&lt;/code>然后&lt;code>s += 1&lt;/code>——每次更新只要O(1)，一趟循环完事？就这么简单？”&lt;/p>
&lt;p>“就这么简单！步长±1这个性质，让原本需要SortedList二分查找的操作，退化成了加减法。不是数据结构选得好，是数据变化的规律挖得深——老勇者原话。”&lt;/p>
&lt;p>村长若有所思地点点头，然后问了一个让我差点从椅子上摔下来的问题：“那如果老板娘把活动搞到太阳系毁灭呢？日志量无限大怎么办？”&lt;/p>
&lt;p>“理论上，只要磁盘能存下，O(n)就能跑。磁盘存不下的话——那就不是算法的问题，是您该给服务器加硬盘了。或者换个老板娘，让她别把活动搞那么大。”&lt;/p>
&lt;hr>
&lt;p>看着村长抱着服务器远去的背影，我靠在椅背上，脑海中回放着这两天的算法之旅，从O(n²)到O(n)，三步进化：&lt;/p>
&lt;ol>
&lt;li>暴力枚举 O(n²) ：双重循环，硬刚所有子数组。直观但低效，像用放大镜在沙漠里数沙子。&lt;/li>
&lt;li>前缀和+SortedList O(n log n)：把区间问题转成查询问题，用平衡树加速。快了不少，但要在红黑树里爬来爬去。&lt;/li>
&lt;li>前缀和 + 哈希增量 O(n)：抓住步长±1的命门，用变量f动态维护。不再依赖重型数据结构，像直接开了上帝视角，一步到位。&lt;/li>
&lt;/ol>
&lt;p>三步进化，是一场思维方式的三重升级——从“暴力硬刚”到“找规律走捷径”，从“依赖重型数据结构”到“洞察数据本质”。&lt;/p>
&lt;p>脑子一旦转起来就停不住。今天十万条的日志O(n)秒过，但&lt;strong>如果老板娘的需求继续升级呢？&lt;/strong>&lt;/p>
&lt;p>如果老板娘再搞大点——全村五十家商铺、每人每秒都在抽奖、日志量破亿——单机O(n)迟早撞上物理天花板。到时候就得把日志分片，扔到多台服务器上并行处理——每台机器独立算自己那段的净胜球前缀和，最后汇总的时候要处理跨分片边界的区间。这个“跨片合并”才是分布式最难啃的骨头，搞不好要请MapReduce出山。&lt;/p>
&lt;p>或者老板娘换了个需求——不要离线统计了，要实时监控，每秒刷新一次故障窗口数量——那哈希表就不够用了，得上支持动态前缀和查询的结构——树状数组或者线段树，每次日志写入O(log n)更新，每次查询O(log n)出结果。从离线批处理变成在线流式，数据结构跟着需求一起升级。&lt;/p>
&lt;p>再或者，老板娘不仅想知道故障窗口有几个，还想知道“最长的故障窗口有多长”、“故障密度最高的时间段在哪里”——那光靠f变量就兜不住了，得上更复杂的状态追踪，最长窗口、密度峰值，每一个新需求都是一个新的算法副本。&lt;/p>
&lt;p>我突然感觉背上一片冷汗——倒不是被这些未来需求吓的，而是忽然意识到，算法工程师的日常，就像是一条永不停歇的日志流。需求来的时候像是 DDoS 攻击，流量瞬间爆表，CPU 直接拉满；开发的时候像在打一个无限副本的 Boss，刚交完差还没来得及存档，产品经理就带着新需求闪亮登场了——发需求的时间复杂度是O(1)，随时随地，想到就来；而客户的想象力空间复杂度是O(∞)，永不封顶，比老板娘的抽奖活动还能膨胀。&lt;/p>
&lt;p>不过转念一想，昨天不也是从O(n²)一路进化过来了吗？兵来将挡，需求来了解法挡。&lt;/p>
&lt;p>&lt;strong>三年日志哈希扛，昨日神功今上场。&lt;/strong>
&lt;strong>客户之心深似海，日日精进破新墙。&lt;/strong>&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3737 村长云服务器危机：从暴力枚举到哈希增量的三重奏</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-25/</link><pubDate>Thu, 25 Jun 2026 14:12:59 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-25/</guid><description>&lt;h2 id="-村长云服务器危机从暴力枚举到哈希增量的三重奏">🎮 村长云服务器危机：从暴力枚举到哈希增量的三重奏
&lt;/h2>&lt;p>“小白！我搞了台二手服务器！” 今天一大早，村长抱着一个嗡嗡作响的铁盒子冲进来，机箱风扇发出拖拉机般的轰鸣声。机箱上贴着歪歪扭扭的标签——村长云服务器·内测版，旁边还画了一个丑萌丑萌的云朵图案。&lt;/p>
&lt;p>他把显示器往桌上一搁，屏幕上是一条不断刷新的日志流，每行要么是绿色的&amp;quot;SUCCESS&amp;quot;，要么是红色的 &amp;ldquo;FAIL&amp;rdquo;，密密麻麻地滚动着，像黑客帝国里的代码雨。&lt;/p>
&lt;p>“你看这个监控系统，”村长指着屏幕，自豪中带着几分心虚，“它每秒记录一次请求状态——成功或者失败。小卖部老板娘把她的抽奖系统挂在我这台服务器上了，结果运行了三天，抽奖页面崩了五回！”&lt;/p>
&lt;p>他压低声音，神色紧张：“老板娘说如果再崩，她就换隔壁村老李的服务器。我得找出所有出问题的时间段，证明我的服务器只是‘短暂波动’，不是‘性能不行’！”&lt;/p>
&lt;p>我凑近一看，日志已经跑了好几千条，密密麻麻的绿色和红色交替闪烁，像一棵圣诞树。村长调出一个运维面板，上面写着故障判定规则：&lt;/p>
&lt;blockquote>
&lt;p>如果一段连续时间内，失败请求的次数超过总请求数的一半，这段区间就是“故障窗口”。&lt;/p>&lt;/blockquote>
&lt;p>“也就是说，我需要统计所有可能的故障窗口，看看哪些时间段服务器最脆弱——” 村长搓着手，一脸殷勤地看着我，“小白，你不是最擅长统计什么‘子数组个数’吗？帮我算算？算完了我给你充一百块服务器体验券！”&lt;/p>
&lt;p>我盯着屏幕上的日志流：“这不就是把 FAIL 当成 target，SUCCESS 当成其他元素，找所有 target 出现次数超过一半的子数组吗——&amp;quot; 于是，我拍拍胸脯，自信地说：”村长，没问题！净胜球统计，FAIL +1，SUCCESS -1，净胜球大于 0 就是故障窗口。先上暴力跑通逻辑！“&lt;/p>
&lt;hr>
&lt;h4 id="第一幕暴力初探">第一幕·暴力初探
&lt;/h4>&lt;p>双重循环枚举所有可能的连续时间段（也就是所有子数组）。外层循环定起点，内层循环往后扩展，同时维护一个净胜球计数器cnt——遇到target就+1，遇到非target就-1。cnt大于0的瞬间，说明从起点到当前位置这段区间里，target的出场次数已经压过了其他元素，故障窗口成立，答案加一。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">countMajoritySubarrays&lt;/span>(nums: List[int], target: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n &lt;span style="color:#f92672">=&lt;/span> len(nums)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(n): &lt;span style="color:#75715e"># 枚举子数组的起始位置&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#75715e"># target 相对于其他元素的净胜球&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(i, n): &lt;span style="color:#75715e"># 枚举子数组的结束位置&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> nums[j] &lt;span style="color:#f92672">==&lt;/span> target &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 如果是target，+1；否则-1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> cnt &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>: &lt;span style="color:#75715e"># 如果target的净胜球大于0，说明它是主要元素&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 计数器加1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>跑了一遍样例——完美。我正准备提交，瞥见村长的服务器日志已经跑到了好几千条，红色的 FAIL 和绿色的 SUCCESS 还在屏幕上疯狂滚动。“村长，您这日志数据量多大？”我问道。&lt;/p>
&lt;p>“不多不多，目前才几千条，”村长摆摆手，一脸轻松，“不过老板娘说抽奖活动要持续一年！每天都有新日志！到时候我这&amp;rsquo;村长云&amp;rsquo;的数据量可是海量的！”&lt;/p>
&lt;p>我心里暗暗叫苦——几千条用 O(n²) 还能扛住，但一年下来怕是能积累到十万条甚至更多——那时候就是上百亿次操作了。O(n²) 的暴力法绝对会炸，连渣都不剩。&lt;/p>
&lt;p>“不行，我得提前想好优化方案。”我盯着代码里那个 &lt;code>cnt&lt;/code> 变量，陷入了沉思。&lt;/p>
&lt;hr>
&lt;h4 id="第二幕前缀和sortedlist">&lt;strong>第二幕·前缀和+SortedList&lt;/strong>
&lt;/h4>&lt;p>暴力法的瓶颈很明显：对于每一个起点&lt;code>i&lt;/code>，暴力法都要从&lt;code>i&lt;/code>出发一步一步往右走，边加边判——相邻两个起点 i 和 i+1 之间，大量中间计算结果被白白扔掉了。明明&lt;code>[i, j]&lt;/code>和&lt;code>[i+1, j]&lt;/code>的净胜球只差&lt;code>nums[i]&lt;/code>这一个元素的贡献，但暴力法把整段重新算了一遍。重复计算，O(n²)的万恶之源。&lt;/p>
&lt;p>如果能用什么东西把“从开头到某个位置”的净胜球存下来，那任意区间&lt;code>[i, j]&lt;/code>的净胜球就变成了两个值的减法。等等——这不就是&lt;strong>前缀和&lt;/strong>吗！&lt;/p>
&lt;p>我在草稿纸上快速推导：&lt;/p>
&lt;blockquote>
&lt;p>定义 prefix[k] 为前 k 条记录的净胜球，prefix[0] = 0，表示还没开始时的空状态。&lt;/p>
&lt;p>对于任意区间 (i, j]，它的净胜球就是 prefix[j] - prefix[i]。&lt;/p>
&lt;p>要让它成为故障窗口，需要 prefix[j] - prefix[i] &amp;gt; 0，&lt;/p>
&lt;p>移项得：&lt;strong>prefix[j] &amp;gt; prefix[i]&lt;/strong>&lt;/p>&lt;/blockquote>
&lt;p>“对于每个位置 j，我不再需要枚举所有可能的起点 i，而是只需要统计前面有多少个位置的前缀和小于 prefix[j]！”&lt;/p>
&lt;p>原本要枚举所有子数组的问题，现在变成了一个动态查询问题——随着遍历的进行，不断维护一个历史前缀和的集合，并快速查询“有多少个历史值小于当前值”。&lt;/p>
&lt;p>用什么数据结构存前缀和来支持快速查询呢？我在脑子里飞快地过了一遍：普通数组？查询需要遍历，O(n)，pass。排序数组？插入需要移动元素，还是 O(n)，pass。平衡二叉搜索树——插入和查询都是 O(log n)，完美！Python 标准库里虽然没有平衡树，但 &lt;code>sortedcontainers&lt;/code> 里的 &lt;code>SortedList&lt;/code> 就是这个角色的天选之子——内部维护有序列表，支持 O(log n) 的二分查找和插入，简直是给这道题量身定做的！&lt;/p>
&lt;p>思路瞬间清晰起来：&lt;/p>
&lt;ol>
&lt;li>初始化一个 SortedList，放入初始前缀和 0（对应prefix[0]，表示还没开始时的状态）&lt;/li>
&lt;li>遍历数组，维护当前前缀和 s：如果当前元素是 target，前缀和 +1；否则 -1&lt;/li>
&lt;li>对于每个位置，用 &lt;code>bisect_left(s)&lt;/code> 查询有多少个历史前缀和小于 s，累加到答案中&lt;/li>
&lt;li>将当前前缀和 s 加入 SortedList，供后续位置查询。&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> sortedcontainers &lt;span style="color:#f92672">import&lt;/span> SortedList
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">countMajoritySubarrays&lt;/span>(nums: List[int], target: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sl &lt;span style="color:#f92672">=&lt;/span> SortedList([&lt;span style="color:#ae81ff">0&lt;/span>]) &lt;span style="color:#75715e"># 初始化前缀和有序列表，包含哨兵0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> s &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#75715e"># ans是答案，s是当前前缀和&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> x &lt;span style="color:#f92672">in&lt;/span> nums:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> s &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> x &lt;span style="color:#f92672">==&lt;/span> target &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 更新前缀和&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">+=&lt;/span> sl&lt;span style="color:#f92672">.&lt;/span>bisect_left(s) &lt;span style="color:#75715e"># 查找小于s的前缀和个数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sl&lt;span style="color:#f92672">.&lt;/span>add(s) &lt;span style="color:#75715e"># 将当前前缀和加入有序列表&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>提交，AC弹出来的速度比服务器日志刷新还快。暴力到有序，时间复杂度就从 O(n²) 降到了 O(n log n)！就算日志增长到十万条，也能在合理时间内算完！&lt;/p>
&lt;p>村长看着运维面板上标注出来的故障窗口，乐得合不拢嘴：“这些时间段的故障窗口果然集中在下半夜！我就说下半夜没人值班，服务器自己跑着跑着就崩了！看老板娘还怎么怪我的二手服务器不行！”&lt;/p>
&lt;p>“数据结构的力量！” 我得意地晃了晃鼠标，“能用 O(n log n) 绝不用 O(n²)，能秀操作绝不写普通代码。村长，您这二手服务器配上我这优化算法，就是生产级稳定性——” 话还没说完，后脑勺挨了精准的一下。&lt;/p>
&lt;hr>
&lt;h4 id="第三幕哈希表--动态维护">第三幕·哈希表 + 动态维护
&lt;/h4>&lt;p>“SortedList 都掏出来了，有长进。”老勇者扫了眼屏幕上那段 SortedList 代码，嘴角微微上扬。&lt;/p>
&lt;p>“O(n log n) 能过，”他话锋一转，“但你这还是在用有序结构做查找。SortedList 内部维护了一棵红黑树，每次插入和查询都要在树里走一圈，常数因子不小。”&lt;/p>
&lt;p>我愣了一下：“什么意思？这可是 O(n log n) 的高效算法啊！”&lt;/p>
&lt;p>老勇者拿过笔，在纸上画了一条上下波动的折线，“你仔细看，你这前缀和 s 每次怎么变的？”&lt;/p>
&lt;p>“遇到 target 加一，遇到非 target 减一。步子很小，每次只变 ±1。”&lt;/p>
&lt;p>“这就是关键——步子很小。”老勇者用笔尖在折线上点了点，“你想过没有，既然 s 每次只变化 1，那&amp;rsquo;有多少个历史前缀和小于 s&amp;rsquo;这个数量，是不是也不需要每次都重新计算？”&lt;/p>
&lt;p>我皱起眉头：“不重新计算？s 变了之后，符合条件的历史位置会变啊……”&lt;/p>
&lt;p>“没错，但它们的变化是有规律的。”老勇者眼中闪过一丝狡黠，“如果你维护一个变量 &lt;code>f&lt;/code>，表示&amp;rsquo;以当前位置为右端点时，满足 &lt;code>prefix[i] &amp;lt; s&lt;/code>的左端点个数&amp;rsquo;。当 &lt;code>s&lt;/code> 变化时，&lt;code>f&lt;/code> 不需要从头算——它只需要做一次&lt;strong>增量更新&lt;/strong>。”&lt;/p>
&lt;p>我若有所思：“增量更新？每次加多少，减多少？”&lt;/p>
&lt;p>“看 s 怎么变。”老勇者在纸上写下递推关系，“如果当前元素是 target，s 会增加 1，变成 new_s = s + 1 。原来那些 前缀和 &amp;lt; s 的 i 依然满足条件。此外，那些前缀和恰好等于s 的 i ，现在也满足条件了——因为 s &amp;lt; new_s 。”&lt;/p>
&lt;p>他停顿了一下，盯着我问：“所以，f 应该增加多少？”&lt;/p>
&lt;p>我脑子飞速运转：“应该增加……那些 前缀和恰好等于 s 的历史位置个数？”&lt;/p>
&lt;p>“正是！”老勇者满意地点头，“如果我们用一个哈希表 cnt 来记录每个前缀和出现的次数，那么 cnt[s] 就是前缀和恰好等于 s 的历史位置个数。所以当 s 增加 1 时，f 应该增加 cnt[s]。”&lt;/p>
&lt;p>老勇者又把笔尖折向下方：“反过来，如果当前元素不是 target，s 会减少 1，变成 new_s = s - 1。原来那些前缀和&amp;lt; s 的历史位置中，那些前缀和恰好等于 new_s（也就是 s - 1）的位置，现在不再满足条件了——因为现在要求的是小于 new_s，而它们等于 new_s，不满足&amp;rsquo;严格小于&amp;rsquo;。”&lt;/p>
&lt;p>他抬起头，眼神锐利：“所以，f 应该减去多少？”&lt;/p>
&lt;p>我恍然大悟：“应该减去那些 前缀和恰好等于 new_s 的历史位置个数，也就是 cnt[s - 1]！”&lt;/p>
&lt;p>“没错！”老勇者露出欣慰的笑容，“通过这种方式，每次只需要 O(1) 的时间就能更新 f，连二分查找都省了！”&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">countMajoritySubarrays&lt;/span>(nums: List[int], target: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt &lt;span style="color:#f92672">=&lt;/span> defaultdict(int)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 初始前缀和0出现一次（哨兵）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> s &lt;span style="color:#f92672">=&lt;/span> f &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> x &lt;span style="color:#f92672">in&lt;/span> nums:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> x &lt;span style="color:#f92672">==&lt;/span> target:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f &lt;span style="color:#f92672">+=&lt;/span> cnt[s] &lt;span style="color:#75715e"># 新增那些前缀和恰好等于当前s的起始位置&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> s &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> s &lt;span style="color:#f92672">-=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f &lt;span style="color:#f92672">-=&lt;/span> cnt[s] &lt;span style="color:#75715e"># 减去那些前缀和恰好等于新s的起始位置（不再满足条件）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">+=&lt;/span> f &lt;span style="color:#75715e"># 累加当前位置的贡献&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt[s] &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 将当前前缀和加入哈希表&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>提交，AC 弹出来的速度比村长那台二手服务器的风扇还快——O(n) 空间，O(n) 时间！&lt;/p>
&lt;p>“每次更新f只需要O(1)，然后把f累加到答案，再把新的s加入哈希表。”我激动地指着屏幕，“一趟遍历，没有任何嵌套循环，也没有任何二分查找！纯纯的O(n)！从O(n²)暴力到O(n log n)有序集合，再到O(n)哈希表——这优化曲线比过山车还刺激！”&lt;/p>
&lt;p>老勇者点了点头：“步长±1这个性质，让你不用再问‘有多少个小于s’，只需要在s变化的时候把答案顺手更新一下。SortedList是通用解，哈希表+变量f是特化解——知道什么时候该通用，什么时候该特化，这趟副本才算没白刷。”&lt;/p>
&lt;hr>
&lt;h4 id="尾声">尾声
&lt;/h4>&lt;p>村长抱着服务器乐颠颠地回去找老板娘邀功了，跑出去老远还能听见他在喊：“小白！老板娘说抽奖活动可能要搞三年，日志量会爆炸——你那O(n)扛得住吗？”&lt;/p>
&lt;p>我靠在椅背上，总觉得这句式有点耳熟——上次村长说‘这个需求很简单’，隔天就甩过来一个‘锯齿过山车 II’。这次直接喊三年，怕不又是‘村长云监控 II’的预告，那种在数据量后面加好几个零的加强版。&lt;/p>
&lt;p>“扛得住！”我扯着嗓子回了一句，“三年日志算什么，三十年也照跑不误！O(n)的意思就是日志翻倍，时间只翻倍——除非老板娘把抽奖活动搞到太阳系毁灭，不然我这算法稳如老狗！”&lt;/p>
&lt;p>我把三版代码并排收进魔法书，在旁边补了几句：&lt;/p>
&lt;p>**暴力枚举探本质，前缀和转区间查。 **
&lt;strong>SortedList虽快非极致，哈希增量最潇洒。&lt;/strong>&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3700 锯齿过山车·续：数量飙十亿，矩阵快速幂！</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-24/</link><pubDate>Wed, 24 Jun 2026 17:20:11 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-24/</guid><description>&lt;h2 id="-锯齿过山车续数量飙十亿矩阵快速幂">🎢 锯齿过山车·续：数量飙十亿，矩阵快速幂！
&lt;/h2>&lt;p>上回说到，我用&lt;strong>前缀和加原地滚动&lt;/strong>，把计算锯齿过山车轨道方案数的复杂度从 O(nk²) 压到了 O(nk)，村长乐颠颠地去游乐园领赏金了。老勇者临走时扔下一句：“如果n飙到10^6，前缀和也不够用，那就得上矩阵快速幂了。”&lt;/p>
&lt;p>我本以为这只是老勇者随口一提的“远期预告”，毕竟谁能闲着没事建一条十亿个节点的过山车？&lt;/p>
&lt;p>没想到今天一大早，村长灰头土脸地就撞了进来：“小白！！” 他双手撑在桌子上，眼神里写满了绝望：“游乐园没给奖金！他们说上次的方案只适用于小规模轨道，现在要建一条真正的‘超级锯齿过山车’！轨道节点数量从 2000 暴涨到……到……”&lt;/p>
&lt;p>他颤抖着伸出手指，声音都在劈叉：“十亿！十亿个节点！从村头一直铺到海岸线上的那种！你上次那套 O(n×k) 算法直接跪在地上摩擦，连渣都不剩啊！”&lt;/p>
&lt;p>我接过他手里皱巴巴的需求文档，还是那座熟悉的锯齿过山车，但设计图上的参数变了——&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>锯齿过山车·超长版&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>轨道点数 n&lt;/strong>：3 ≤ n ≤ 10^9（昨天才 2000，今天直接通货膨胀？）&lt;/li>
&lt;li>&lt;strong>高度范围 [l, r]&lt;/strong>：1 ≤ l &amp;lt; r ≤ 75（昨天是 2000，今天反而缩水了？）&lt;/li>
&lt;li>&lt;strong>其他规则不变&lt;/strong>：相邻不等，连续三个不能严格递增或递减&lt;/li>
&lt;li>&lt;strong>目标&lt;/strong>：求方案总数，对 10^9+7 取模&lt;/li>
&lt;/ul>&lt;/blockquote>
&lt;p>&lt;code>10^9&lt;/code>，**十亿次循环。**就算每次只做一次加法，O(nk) 在 n=10^9、k=75 时也是 750 亿次运算！就算评测机是超算，每秒处理 10^9 次，也得跑 75 秒！况且它的耐心只有 2 秒——暴力是死路，前缀和也是死路——前缀和再快也甩不掉外层那十亿次迭代啊！&lt;/p>
&lt;p>&amp;ldquo;矩阵快速幂……&amp;ldquo;我喃喃自语，脑子里回放着老勇者昨天的话，&amp;ldquo;能把 O(n) 的线性递推优化成 O(log n)……&amp;rdquo;&lt;/p>
&lt;p>log₂(10^9) ≈ 30。也就是说，如果能用上矩阵快速幂，原本需要十亿次的迭代，现在只需要 &lt;strong>30 次&lt;/strong>！这压缩比，简直是用了量子传送门直接从起点跳到终点！&lt;/p>
&lt;hr>
&lt;h4 id="矩阵快速幂听起来像是高维魔法">矩阵快速幂？听起来像是高维魔法
&lt;/h4>&lt;p>我深吸一口气，强迫自己冷静下来，开始拆解这个听起来很唬人的概念。&lt;/p>
&lt;p>&lt;strong>矩阵快速幂&lt;/strong>，名字听着玄乎，像是什么 SSR 级角色的终极技能。但剥开外壳看本质，它其实就是把&lt;strong>快速幂的思想从标量扩展到矩阵&lt;/strong>，从而把 n 次矩阵乘法压缩到 log n 次。&lt;/p>
&lt;p>先来个&lt;strong>快速幂的极简回顾&lt;/strong>，免得待会儿在矩阵的迷宫里迷路：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>快速幂的核心思想&lt;/strong>：计算 a^n 不需要傻乎乎地乘 n 次，而可以利用&lt;strong>二进制分解&lt;/strong>。&lt;/p>
&lt;p>比如要计算 a^13，把 13 写成二进制形式：13 = 1101(₂)。这意味着：a^13 = a^(8+4+1) = a^8 × a^4 × a^1&lt;/p>
&lt;p>具体做法就像是在玩&amp;quot;翻倍游戏&amp;rdquo;：&lt;/p>
&lt;ul>
&lt;li>初始化结果 &lt;code>res = 1&lt;/code>，底数 &lt;code>base = a&lt;/code>&lt;/li>
&lt;li>从低位到高位遍历 n 的二进制位&lt;/li>
&lt;li>如果当前位是 1，就把当前的 &lt;code>base&lt;/code> 累乘到 &lt;code>res&lt;/code> 中&lt;/li>
&lt;li>每步让 &lt;code>base&lt;/code> 自乘（&lt;code>base = base × base&lt;/code>），n 右移一位&lt;/li>
&lt;/ul>
&lt;p>通过二进制分解，只需要计算 &lt;strong>log n&lt;/strong> 次乘法，而不是 n 次。
时间复杂度从 O(n) 降到 O(log n)，压缩比，简直像是用了空间折叠技术！&lt;/p>&lt;/blockquote>
&lt;pre tabindex="0">&lt;code>def pow_mul(a, n):
res = 1
while n:
if n &amp;amp; 1: # 如果当前二进制位是 1
res *= a # 累乘到结果
a *= a # 底数自乘
n &amp;gt;&amp;gt;= 1 # n 右移一位
return res
&lt;/code>&lt;/pre>&lt;p>温习完毕，再回到这次的任务上来。&lt;/p>
&lt;p>我的 DP 转移是&lt;strong>线性&lt;/strong>的——每一步都只做加法和数乘，没有非线性操作（比如取 max、min 或者条件判断）。这意味着，整个转移过程完全可以表示成一个&lt;strong>矩阵乘法&lt;/strong>！&lt;/p>
&lt;p>如果我能构造一个转移矩阵 &lt;strong>M&lt;/strong> 和一个初始状态向量 &lt;strong>F₁&lt;/strong>，使得：&lt;/p>
&lt;blockquote>
&lt;p>F₂ = M × F₁
F₃ = M × F₂ = M² × F₁
&amp;hellip;
Fₙ = M^(n-1) × F₁&lt;/p>&lt;/blockquote>
&lt;p>那么我只需要计算 &lt;strong>M^(n-1)&lt;/strong>，然后乘以 &lt;strong>F₁&lt;/strong>，就能直接得到第 n 层的状态！而 &lt;strong>M^(n-1)&lt;/strong> 可以用快速幂在 &lt;strong>O(log n)&lt;/strong> 次矩阵乘法内完成！&lt;/p>
&lt;p>我在纸上又画了个对比图：&lt;/p>
&lt;blockquote>
&lt;p>普通 DP: F₁ → F₂ → F₃ → &amp;hellip; → Fₙ (n-1 步，O(n) 时间)
矩阵快速幂: F₁ ⇒ M^(n-1)×F₁ = Fₙ (log n 次矩阵乘法，O(log n) 时间)&lt;/p>&lt;/blockquote>
&lt;p>这简直像是给你的 DP 装上了&lt;strong>火箭推进器&lt;/strong>——原本要一步步走的 n 步，现在只要 log n 次&amp;quot;瞬移&amp;quot;就能到达！从村头到海岸线，以前要走十亿步，现在只要 30 次传送门跳跃！&lt;/p>
&lt;p>思路通了！我兴奋地掏出键盘，准备大干一场。&lt;/p>
&lt;hr>
&lt;h4 id="第一步把-dp-状态转化成向量形式">第一步：把 DP 状态转化成向量形式
&lt;/h4>&lt;p>&amp;ldquo;首先，我需要把 DP 状态转化成向量形式。&amp;ldquo;我自言自语道，手指在桌面上敲着节拍。&lt;/p>
&lt;p>&amp;ldquo;昨天的状态是 &lt;code>f0[j]&lt;/code> 和 &lt;code>f1[j]&lt;/code>，分别表示最后一个元素是 j 且最后两个元素递增/递减的方案数。那我可以把这些状态拼成一个长度为 2k 的列向量……&amp;rdquo;&lt;/p>
&lt;p>我在草稿纸上写下状态向量的结构：&lt;/p>
&lt;blockquote>
&lt;p>状态向量 &lt;strong>F&lt;/strong> = [f0[0], f0[1], &amp;hellip;, f0[k-1], f1[0], f1[1], &amp;hellip;, f1[k-1]]ᵀ&lt;/p>&lt;/blockquote>
&lt;p>长度为 2k，当 k=75 时就是 &lt;strong>150 维&lt;/strong>。这个维度完全可控，既不会大到内存爆炸，也不会小到失去表达能力。完美！&lt;/p>
&lt;hr>
&lt;h4 id="第二步构造转移矩阵-m">第二步：构造转移矩阵 M
&lt;/h4>&lt;p>&amp;ldquo;然后，我需要构造一个 2k × 2k 的转移矩阵 &lt;strong>M&lt;/strong>，使得 &lt;strong>F&lt;/strong>_{i+1} = &lt;strong>M&lt;/strong> × &lt;strong>F&lt;/strong>_i。&amp;rdquo;&lt;/p>
&lt;p>我盯着纸上那个 150×150 的空矩阵格子，感觉像是在面对一个巨大的数独谜题。&lt;strong>怎么填？&lt;/strong>&lt;/p>
&lt;p>先回顾一下昨天的状态转移公式：&lt;/p>
&lt;blockquote>
&lt;p>对于 f0_new[j]（以 j 结尾且最后两步递增）： f0_new[j] = sum(f1_old[p]) where p &amp;lt; j
对于 f1_new[j]（以 j 结尾且最后两步递减）： f1_new[j] = sum(f0_old[p]) where p &amp;gt; j&lt;/p>&lt;/blockquote>
&lt;p>这意味着，&lt;strong>F&lt;/strong>&lt;em>{i+1}&lt;/em> 中的每一个分量，都依赖于 &lt;strong>F&lt;/strong>_i 中多个分量的累加和。&lt;/p>
&lt;p>我皱起眉头：&amp;ldquo;每个新状态都依赖于多个旧状态的累加，这怎么用矩阵表示？区间求和的关系怎么转化成矩阵的系数？这难道要我给每个位置都写一个求和公式？&amp;rdquo;&lt;/p>
&lt;p>就在我抓耳挠腮之际，脑海中突然闪过一道灵光——&lt;strong>矩阵乘法的本质就是&amp;rsquo;加权求和&amp;rsquo;！&lt;/strong>&lt;/p>
&lt;p>当我计算 &lt;strong>M × F&lt;/strong> 时，结果向量的第 i 个元素等于：&lt;/p>
&lt;blockquote>
&lt;p>(M × F)[i] = Σ M[i] [j] × F[j] （对所有 j 求和）&lt;/p>&lt;/blockquote>
&lt;p>这不就是矩阵 M 的第 i 行与向量 F 的&lt;strong>点积&lt;/strong>吗？&lt;/p>
&lt;p>也就是说，如果我想让：&lt;/p>
&lt;blockquote>
&lt;p>f0_new[j] = sum(f1_old[k]) where k &amp;lt; j&lt;/p>&lt;/blockquote>
&lt;p>我只需要在矩阵 M 的第 j 行，把所有对应 &lt;code>f1_old[k]&lt;/code>（k &amp;lt; j）的位置填成 &lt;strong>1&lt;/strong>，其他位置填 &lt;strong>0&lt;/strong>！这样，当矩阵乘法执行时，这些 1 就会把对应的 f1_old[k] &amp;ldquo;挑选&amp;quot;出来并累加，而 0 会把不相关的元素过滤掉！&lt;/p>
&lt;p>&lt;strong>这就像是给矩阵装上了过滤器——只让需要的元素通过，其他的统统拦截！&lt;/strong>&lt;/p>
&lt;p>我激动地在草稿纸上画出了转移矩阵的结构：&lt;/p>
&lt;blockquote>
&lt;p>对于f0[j] = f1[0] + f1[1] + &amp;hellip; + f1[j-1] ， 转移矩阵里对应的那部分是一个下三角全1矩阵。
对于f1[j] = f0[j+1] + f0[j+2] + &amp;hellip; + f0[k-1]，对应的是一个上三角全1矩阵。&lt;/p>
&lt;p>转移矩阵 M 的结构（2k × 2k）：&lt;/p>
&lt;p>| 0 B | ← 上半部分：f0_new 依赖 f1_old
| C 0 | ← 下半部分：f1_new 依赖 f0_old&lt;/p>
&lt;p>其中：
B 是 k×k 的下三角全 1 矩阵（B[i] [j]=1 当且仅当 j &amp;lt; i）
C 是 k×k 的上三角全 1 矩阵（C[i] [j]=1 当且仅当 j &amp;gt; i）
0 是 k×k 的全零矩阵&lt;/p>&lt;/blockquote>
&lt;p>我盯着这个&lt;strong>分块反对角矩阵&lt;/strong>，感觉自己像是破解了一个古老的密码。原来，看似复杂的区间求和，在矩阵的世界里，不过是填 0 和 1 的游戏罢了！&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 转移矩阵 M，大小2k × 2k&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>m &lt;span style="color:#f92672">=&lt;/span> [[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (k&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#ae81ff">2&lt;/span>) &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(k&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#ae81ff">2&lt;/span>)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 上半部分：f0[i] 依赖所有 f1[j] where j &amp;lt; i&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(k):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(i):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m[i][k&lt;span style="color:#f92672">+&lt;/span>j] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 第 i 行，第 k+j 列为 1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 下半部分：f1[i] 依赖所有 f0[j] where j &amp;gt; i&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(k):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(i&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, k):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m[k&lt;span style="color:#f92672">+&lt;/span>i][j] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 第 k+i 行，第 j 列为 1&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h4 id="第三步组装矩阵快速幂把零件拼成火箭">第三步：组装矩阵快速幂——把零件拼成火箭
&lt;/h4>&lt;p>想清楚矩阵的构造，剩下的就是体力活了。我需要实现三个组件：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>矩阵乘法 &lt;code>mul&lt;/code>&lt;/strong>：计算两个 2k × 2k 矩阵的乘积&lt;/li>
&lt;li>&lt;strong>矩阵快速幂 &lt;code>pow_mul&lt;/code>&lt;/strong>：计算 M^(n-1) × F₁&lt;/li>
&lt;li>&lt;strong>主函数 &lt;code>zigZagArrays&lt;/code>&lt;/strong>：构造矩阵、调用快速幂、统计结果&lt;/li>
&lt;/ol>
&lt;p>我开始写完整的代码，就像是在组装一台精密的仪器：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>MOD &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">10&lt;/span>&lt;span style="color:#f92672">**&lt;/span>&lt;span style="color:#ae81ff">9&lt;/span> &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">7&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">mul&lt;/span>(a: List[List[int]], b: List[List[int]]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> List[List[int]]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;矩阵乘法：C = A × B&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> [[sum(x&lt;span style="color:#f92672">*&lt;/span>y &lt;span style="color:#66d9ef">for&lt;/span> x, y &lt;span style="color:#f92672">in&lt;/span> zip(row, col))&lt;span style="color:#f92672">%&lt;/span>MOD &lt;span style="color:#66d9ef">for&lt;/span> col &lt;span style="color:#f92672">in&lt;/span> zip(&lt;span style="color:#f92672">*&lt;/span>b)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> row &lt;span style="color:#f92672">in&lt;/span> a]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">pow_mul&lt;/span>(a: List[List[int]], n: int, f1: List[List[int]]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> List[List[int]]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;矩阵快速幂：计算 A^n × F1&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res &lt;span style="color:#f92672">=&lt;/span> f1
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> n:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> n &lt;span style="color:#f92672">&amp;amp;&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res &lt;span style="color:#f92672">=&lt;/span> mul(a, res)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> a &lt;span style="color:#f92672">=&lt;/span> mul(a, a)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n &lt;span style="color:#f92672">&amp;gt;&amp;gt;=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> res
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Solution&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">zigZagArrays&lt;/span>(self, n: int, l: int, r: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> k &lt;span style="color:#f92672">=&lt;/span> r &lt;span style="color:#f92672">-&lt;/span> l &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 高度范围的长度&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 构造转移矩阵 M，大小 2k × 2k&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m &lt;span style="color:#f92672">=&lt;/span> [[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (k&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#ae81ff">2&lt;/span>) &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(k&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#ae81ff">2&lt;/span>)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(k):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(i):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m[i][k&lt;span style="color:#f92672">+&lt;/span>j] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(k):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(i&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, k):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m[k&lt;span style="color:#f92672">+&lt;/span>i][j] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 初始状态向量 F1，所有值为 1（第一个元素可以是任意值）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f1 &lt;span style="color:#f92672">=&lt;/span> [[&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(k&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#ae81ff">2&lt;/span>)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 计算 M^(n-1) × F1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fn &lt;span style="color:#f92672">=&lt;/span> pow_mul(m, n&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, f1)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 统计所有状态的方案数之和&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> sum(row[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#66d9ef">for&lt;/span> row &lt;span style="color:#f92672">in&lt;/span> fn) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>小样例先跑下，完美通过！再算算复杂度：矩阵乘法 O((2k)³) = O(8k³) ≈ O(k³)。快速幂需要 log n 次矩阵乘法，所以总复杂度是 &lt;strong>O(k³ log n)&lt;/strong>。代入数值：k=75, n=10^9，log₂(10^9) ≈ 30。75³ × 30 ≈ 1.2 × 10^7 次运算。也就是说，总操作量大约一千两百万次。怎么说呢，这个量级就像是早高峰的地铁——挤是挤了点，但只要不被卡住，还是能在规定时间内到达终点。&lt;/p>
&lt;p>我深吸一口气，点击提交。屏幕闪烁了两秒——绿色的Accepted弹了出来！运行时间1.8秒，差点就超时了。&lt;/p>
&lt;p>“过了！”我差点从椅子上跳起来，“虽然跑得慢了点儿，但能在2秒内ac，就是胜利！” 十亿次迭代被压缩到三千万次矩阵运算，这压缩比简直像是把一整条过山车轨道卷成了一个弹簧！&lt;/p>
&lt;p>就在我得意洋洋，准备发朋友圈炫耀这波“极限压线”操作时，后脑勺又挨了精准的一下——力道刚好能把我从“我真牛逼”的幻觉里敲回现实。&lt;/p>
&lt;hr>
&lt;h4 id="老勇者登场优化的边界">老勇者登场——优化的边界
&lt;/h4>&lt;p>“不错嘛，”老勇者站在我身后，嘴角带着一丝笑意，“学会用朴素矩阵快速幂 AC 了。1.8秒，压线过——看来评测机今天心情不错，没卡你的常数因子。”&lt;/p>
&lt;p>我揉着后脑勺，有点不好意思地嘿嘿一笑：“运气好，运气好。不过您看这1.8秒，心悬得很。有没有什么优化方法，能让它过得更稳一点？”&lt;/p>
&lt;p>老勇者点了点屏幕：&amp;ldquo;你的解法很标准了——构造转移矩阵，实现矩阵快速幂，计算 M^(n-1) × F₁。这套流程你掌握得很扎实。矩阵快速幂的适用条件、转移矩阵的构造技巧，这两个核心点你都踩准了。”&lt;/p>
&lt;p>他顿了顿，扫了一眼我屏幕上那个矩阵乘法的代码：“不过，既然你问到了优化，那我就提供一个思路。”&lt;/p>
&lt;p>“你的转移矩阵是分块反对角矩阵，一半下三角全1，一半上三角全1。这种特殊结构，乘向量的时候其实不需要 O(k²) 的三重循环——下三角全1乘向量就是做前缀和，上三角全1乘向量就是做后缀和。跟昨天的原地滚动一个道理。一趟 O(k) 搞定，不用构建完整的转移矩阵。&lt;/p>
&lt;p>我脑子“嗡”了一下，有点懵：“您的意思是……矩阵快速幂里每一轮乘向量都能压到 O(k)，总复杂度压到 O(k² log n)？”&lt;/p>
&lt;p>“对。”老勇者点点头，话锋却一转，“但我只是提供这个思路，不要求你现在就去实现。”&lt;/p>
&lt;p>我愣了一下，满脸写着“为什么”：“为什么？这不是能让代码跑得更快吗？”&lt;/p>
&lt;p>“因为你已经 AC 了。”老勇者瞥了我一眼，“1.8秒和0.5秒，在判题机眼里都是绿色的‘通过’。但如果现在去实现这个优化，你得把矩阵乘法拆成两种情况——矩阵乘矩阵和矩阵乘向量。代码量翻倍，边界条件翻倍，调试时间也翻倍。为了省这1.3秒，把原本清晰的代码搅成一锅粥，不值得。”&lt;/p>
&lt;p>他敲了敲我的屏幕：“刷题不是打比赛，AC 之后多花一小时抠常数，不如把这一小时用来消化今天学到的矩阵快速幂骨架。优化是锦上添花，骨架才是雪中送炭。k=75 的时候 O(k³ log n) 够用，下次 k 飙到 500，再掏出前缀和优化不迟——到时候你自然会感谢今天先把骨架练熟了。”&lt;/p>
&lt;hr>
&lt;p>&lt;strong>尾声：&lt;/strong>&lt;/p>
&lt;p>我靠在椅背上，盯着屏幕上那个绿色的 Accepted，紧绷的肩膀终于放松下来。&lt;/p>
&lt;p>为了凑出那个 1.8 秒的极限压线，我敲代码时手心全是汗，生怕评测机稍微打个盹就给我个 TLE。现在想想，从十亿次循环压到三十次矩阵乘法，已经是一波酣畅淋漓的战斗了。至于前缀和优化矩阵乘法——遇到k飙到500的时候再说，毕竟骨架学透才是正经事——&lt;/p>
&lt;p>&lt;strong>十亿节点莫惊慌，线性递推矩阵扛。&lt;/strong>&lt;br>
&lt;strong>快速幂里藏乾坤，有缘再战五百强。&lt;/strong>&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3699 锯齿过山车：从暴力O(nk²)到原地滚动的三轮进化</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-23/</link><pubDate>Tue, 23 Jun 2026 16:26:37 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-23/</guid><description>&lt;h2 id="锯齿过山车从暴力onk到原地滚动的三轮进化">🎮锯齿过山车：从暴力O(nk²)到原地滚动的三轮进化
&lt;/h2>&lt;p>烈日当空，连村里那只掉毛信鸽都热得趴在窗台上，仿佛随时会触发什么 &lt;code>HeatStrokeException&lt;/code>。我正把头伸进锅炉房里仅存的阴凉角落，院门又被“砰”地一脚踹开了。&lt;/p>
&lt;p>“小白！游乐园那边来大工程了！” 村长一个滑铲冲进了屋。他满头大汗，手里举着一张画得歪歪扭扭的过山车设计图，上面密密麻麻的折线，看起来就像是我昨晚熬夜打排位时的心电图。&lt;/p>
&lt;p>“游乐园？”我转过身，警惕地看着他，“就是之前一位老爷子被‘递归过山车’甩上天，然后一直在天上 &lt;code>return&lt;/code> 不下来，最后触发&lt;code>Stack Overflow&lt;/code>的那个游乐园？”&lt;/p>
&lt;p>“哎呀，那是老黄历了！现在的游乐园可是正经项目！”村长把那张“心电图”拍在我的桌上，“新项目——锯齿过山车！要求轨道必须起伏跌宕！任意两个相邻节点高度不能相同，任意三个连续节点不能一路向上或一路向下！现在需要算出所有可能的轨道设计方案！”&lt;/p>
&lt;p>我瞥了一眼他塞过来的需求文档：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>锯齿过山车设计规范&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>轨道由 n 个点组成，每个点的高度在 [l, r] 范围内&lt;/li>
&lt;li>相邻两个点的高度不能相同（不然车会卡住）&lt;/li>
&lt;li>任意连续三个点不能严格递增或严格递减（不然乘客会被甩飞）&lt;/li>
&lt;li>求满足条件的轨道设计方案总数，对 10^9+7 取模&lt;/li>
&lt;/ul>
&lt;p>//////////////////////////////////////////////////\&lt;/p>&lt;/blockquote>
&lt;p>村长一边擦汗，一边压低声音补充道：“&lt;code>n&lt;/code> 最大 2000，高度范围也是 2000。如果今天能解决，奖金分你一半！”&lt;/p>
&lt;p>一听“奖金”两个字，我原本因为高温而处于低功耗模式的大脑瞬间超频。我一拍胸脯，豪气干云，“不就是个计数问题吗？动态规划一把梭！&amp;quot;&lt;/p>
&lt;hr>
&lt;h4 id="状态机-dp处理相邻约束的神器">状态机 DP——处理相邻约束的神器
&lt;/h4>&lt;p>既然牛皮已经吹出去了，接下来就是正经的思路分析时间。&lt;/p>
&lt;p>先分析‘锯齿形’的本质。我盯着过山车轨道的示意图，&lt;strong>三个节点的关系，其实只由前两个节点的大小关系和当前节点的值决定&lt;/strong>。具体来说，就像过山车的方向变化：&lt;/p>
&lt;ul>
&lt;li>如果前两个节点是递增（轨道在上升 /），那当前节点必须比前一个节点小（轨道开始下降\），形成‘增-减’锯齿（/\）。&lt;/li>
&lt;li>如果前两个节点是递减（轨道在下降\），那当前节点必须比前一个节点大（轨道开始上升/），形成‘减-增’锯齿（/）。&lt;/li>
&lt;/ul>
&lt;p>也就是说，DP 的状态需要记录两个信息：&lt;/p>
&lt;ol>
&lt;li>当前节点的值是多少（记作 j）&lt;/li>
&lt;li>前两个节点的关系是递增还是递减（记作 0 表示递减，1 表示递增）&lt;/li>
&lt;/ol>
&lt;p>所以，&lt;strong>状态设计&lt;/strong>很清晰：&lt;/p>
&lt;blockquote>
&lt;p>&lt;code>dp[j][0]&lt;/code>：表示当前轨道节点高度为 &lt;code>j&lt;/code>，且前两个节点是&lt;strong>递减&lt;/strong>关系的方案数。
&lt;code>dp[j][1]&lt;/code>：表示当前轨道节点高度为 &lt;code>j&lt;/code>，且前两个节点是&lt;strong>递增&lt;/strong>关系的方案数。&lt;/p>&lt;/blockquote>
&lt;p>至于&lt;strong>状态转移&lt;/strong>，其实就是模拟轨道的走向：&lt;/p>
&lt;blockquote>
&lt;p>当要计算新的 &lt;code>dp[j][1]&lt;/code>（递增状态）时，意味着前一个状态必须是递减的，且前一个位置的值要&lt;strong>小于&lt;/strong> j：
=&amp;gt; dp[j] [1] = 所有 dp[k] [0] 的和，其中 k &amp;lt; j&lt;/p>
&lt;p>当要计算新的 &lt;code>dp[j][0]&lt;/code>（递减状态）时，意味着前一个状态必须是递增的，且前一个位置的值要&lt;strong>大于&lt;/strong> j：
=&amp;gt; dp[j] [0] = 所有 dp[k] [1] 的和，其中 k &amp;gt; j&lt;/p>&lt;/blockquote>
&lt;p>&lt;strong>初始条件&lt;/strong>也很简单：&lt;/p>
&lt;blockquote>
&lt;p>对于第一个节点，它可以是 [l, r] 范围内的任意值，而且没有&amp;quot;前两个节点的关系&amp;quot;这个概念（毕竟只有一个点，谈不上增减）。所以可以认为第一个节点的所有取值都是合法的，方案数各为 1。&lt;/p>&lt;/blockquote>
&lt;p>想清楚这些，我突然意识到——这不就是经典的&lt;strong>状态机 DP&lt;/strong> 吗？&lt;strong>用状态记录局部信息（这里是增减关系），用转移保证全局约束（不能有连续三个递增或递减）&lt;/strong>。就像玩游戏时的存档读档，每个状态都是一个存档点，转移方程就是游戏规则。&lt;/p>
&lt;p>我兴奋地跳了起来：二维 DP ！状态数 O(n×k)，每次转移 O(k)，总复杂度 O(n×k²)！等等，n和k都是2000——”&lt;/p>
&lt;p>“等等——”，我掰着手指头算了一下，脸上的笑容逐渐凝固。8e9次运算。八十亿。就算我的CPU是液氮冷却的超算，这运算量一上，估计我也得跟那位递归过山车上的老爷子一样——迟迟return回不来。&lt;/p>
&lt;p>我咽了口唾沫，深吸了口气——不管了，先跑个逻辑验证再说！&lt;/p>
&lt;hr>
&lt;h4 id="暴力dp躲不开tle的阴影">暴力DP——躲不开TLE的阴影
&lt;/h4>&lt;p>我噼里啪啦敲下暴力 DP，忐忑地点下了“Submit”：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>MOD &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">10&lt;/span>&lt;span style="color:#f92672">**&lt;/span>&lt;span style="color:#ae81ff">9&lt;/span> &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">7&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>k &lt;span style="color:#f92672">=&lt;/span> r &lt;span style="color:#f92672">-&lt;/span> l &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 高度范围的长度&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># dp[j][0/1]: 最后一个节点值是 j+l，0表示递减，1表示递增&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dp &lt;span style="color:#f92672">=&lt;/span> [[&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(k)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 初始化：第一个节点可以是任意值&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(k):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dp[j][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dp[j][&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 从第二个节点开始迭代 DP&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">2&lt;/span>, n &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> new_dp &lt;span style="color:#f92672">=&lt;/span> [[&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(k)] &lt;span style="color:#75715e"># 创建新的 DP 数组，用于存储当前状态&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(k): &lt;span style="color:#75715e"># 当前节点可填的值（高度范围内）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 计算 new_dp[j][1]：递增状态，前一个状态必须是递减，且值 &amp;lt; j&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> prev &lt;span style="color:#f92672">in&lt;/span> range(j):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> new_dp[j][&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> (new_dp[j][&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">+&lt;/span> dp[prev][&lt;span style="color:#ae81ff">0&lt;/span>]) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 计算 new_dp[j][0]：递减状态，前一个状态必须是递增，且值 &amp;gt; j&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> prev &lt;span style="color:#f92672">in&lt;/span> range(j &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, k):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> new_dp[j][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> (new_dp[j][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">+&lt;/span> dp[prev][&lt;span style="color:#ae81ff">1&lt;/span>]) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dp &lt;span style="color:#f92672">=&lt;/span> new_dp &lt;span style="color:#75715e"># 用新层的状态替换旧层&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 最后一层DP完成后，统计所有可能的方案数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ans &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(k):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> (ans &lt;span style="color:#f92672">+&lt;/span> dp[j][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">+&lt;/span> dp[j][&lt;span style="color:#ae81ff">1&lt;/span>]) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>提交——红色TLE如期而至——八十亿次循环，Python跑完的时间够我把泡面从红烧牛肉味吃到全系列绝版。&lt;/p>
&lt;p>我盯着屏幕上那个红叉，陷入了沉思：“所以……我的状态转移是对的，只是姿势不对？”&lt;/p>
&lt;hr>
&lt;h4 id="前缀和优化区间求和必杀技">前缀和优化——区间求和必杀技
&lt;/h4>&lt;p>“你的内层循环在干什么？” 老勇者不知道什么时候出现在我身后，手上还拿着两瓶冰可乐。&lt;/p>
&lt;p>我像是看到了救星：“大佬！快救救孩子！” 老勇者递给我一瓶可乐，指了指我的代码：&lt;/p>
&lt;p>“你看，你的转移方程是这样的：&lt;/p>
&lt;ul>
&lt;li>&lt;code>new_dp[j][1] = sum(dp[k][0])&lt;/code>，其中 &lt;code>k &amp;lt; j&lt;/code>&lt;/li>
&lt;li>&lt;code>new_dp[j][0] = sum(dp[k][1])&lt;/code>，其中 &lt;code>k &amp;gt; j&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>这是什么——典型的&lt;strong>区间求和&lt;/strong>问题！你在内层循环里每次都重新遍历累加，当然慢。如果能在 O(1) 时间内得到任意区间的和，是不是就能把内层循环砍掉？”&lt;/p>
&lt;p>我眼睛一亮，脑海中仿佛有一道闪电劈过，照亮了我那被 TLE 阴影笼罩的大脑：“您的意思是……&lt;strong>前缀和&lt;/strong>？！”&lt;/p>
&lt;p>“一点就通。”老勇者满意地点点头，“前缀和的核心思想是&lt;strong>预处理+查表&lt;/strong>。具体到你的问题，就是把‘小于 j 的递减状态之和’ &lt;code>prefix[j]&lt;/code> ，和用于计算 ‘大于 j 的递增状态之和’ &lt;code>prefix[k]、prefix[j+1] &lt;/code>提前算好，O(1) 直接拿。这样内层循环就从 O(k) 降到了 O(1)，总复杂度从 O(n×k²) 降到了 O(n×k)。”&lt;/p>
&lt;p>我恍然大悟，猛地一拍大腿：“对！用 &lt;code>accumulate&lt;/code> 快速计算前缀和，把转移变成 O(1)！这像把每次都要现算的账本，提前做成了汇总表，查账的时候直接翻对应页就行！&amp;quot;&lt;/p>
&lt;p>老勇者点了点头，又补了一刀：&amp;ldquo;还有，你那个二维数组是不觉得碍眼吗？&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;你仔细看，&amp;ldquo;老勇者指了指代码，&amp;ldquo;你的 &lt;code>dp&lt;/code> 数组在每一轮迭代时，只依赖于上一层的状态，两层之间互不干扰。但你每轮都新建一个 &lt;code>new_dp&lt;/code>，内存不要钱啊？而且频繁创建新对象还会触发 GC，进一步拖慢速度。&amp;rdquo;&lt;/p>
&lt;p>“你有没有想过，”他顿了顿，“用两个一维数组 &lt;code>f0&lt;/code> 和 &lt;code>f1&lt;/code> 交替更新就行了。”&lt;/p>
&lt;p>“&lt;code>f0[j]&lt;/code> 表示最后一个节点值是 j 且前两个节点递增的方案数，&lt;code>f1[j]&lt;/code> 表示最后一个节点值是 j 且前两个节点递减的方案数。每轮迭代先算出两个前缀和数组 &lt;code>s0&lt;/code> 和 &lt;code>s1&lt;/code>，然后遍历每个高度 j，直接用前缀和更新 &lt;code>f0[j]&lt;/code> 和 &lt;code>f1[j]&lt;/code>。”&lt;/p>
&lt;p>我盯着他，脑子里像是有无数个齿轮在飞速转动，然后&amp;quot;咔哒&amp;quot;一声，全部咬合到位——&amp;ldquo;就像玩 RPG 游戏，前缀和是直接用传送卷轴到指定等级，原地优化是连背包都不用整理，直接把装备换成新的！&amp;rdquo;&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>MOD &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">10&lt;/span> &lt;span style="color:#f92672">**&lt;/span> &lt;span style="color:#ae81ff">9&lt;/span> &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">7&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>k &lt;span style="color:#f92672">=&lt;/span> r &lt;span style="color:#f92672">-&lt;/span> l &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>f0 &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> k
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>f1 &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> k
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 从第二个位置开始迭代&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> s0 &lt;span style="color:#f92672">=&lt;/span> list(accumulate(f0, initial&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">0&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> s1 &lt;span style="color:#f92672">=&lt;/span> list(accumulate(f1, initial&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">0&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(k):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f0[j] &lt;span style="color:#f92672">=&lt;/span> s1[j] &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f1[j] &lt;span style="color:#f92672">=&lt;/span> (s0[k] &lt;span style="color:#f92672">-&lt;/span> s0[j &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>]) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> (sum(f0) &lt;span style="color:#f92672">+&lt;/span> sum(f1)) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>提交，AC 瞬弹。绿色的勾号亮起的瞬间，我差点从椅子上蹦起来。我刚准备截屏纪念下，老勇者突然伸手按住了我的键盘。&lt;/p>
&lt;hr>
&lt;h4 id="原地滚动状态复用的巅峰">&lt;strong>原地滚动——状态复用的巅峰&lt;/strong>
&lt;/h4>&lt;p>“等等，”他眯起眼睛，“你那个&lt;code>s0&lt;/code>和&lt;code>s1&lt;/code>数组，是真的需要单独存起来吗？”&lt;/p>
&lt;p>我愣住了：“什么意思？不存前缀和数组……我怎么查表？&lt;code>s1[j]&lt;/code>是f1的前缀和，&lt;code>s0[k] - s0[j+1]&lt;/code>是f0的后缀和，没有这两个数组，难道每次都重新算一遍？”&lt;/p>
&lt;p>“你仔细看。”老勇者指着代码里的循环，“&lt;code>f1[j]&lt;/code> 需要的是 &lt;code>s1[j]&lt;/code>——也就是 &lt;code>f1&lt;/code> 中所有下标小于 &lt;code>j&lt;/code> 的值的和。&lt;code>f0[j]&lt;/code> 需要的是 &lt;code>s0[k] - s0[j+1]&lt;/code>——也就是 &lt;code>f0&lt;/code> 中所有下标大于 &lt;code>j&lt;/code> 的值的和。这两个东西，一定要提前把整个前缀和数组全算出来才能拿吗？”&lt;/p>
&lt;p>“前缀和是顺着走，后缀和是逆着走。”他在纸上画了一行从左到右的箭头，又在上面画了一行从右到左的箭头，“你完全可以在更新状态的同时，用一个变量维护当前累加值——顺着走的时候，每走一步就把当前值加入累加器，下一步的转移直接取这个累加器。逆着走同理，只是方向反过来。”&lt;/p>
&lt;p>老勇者嘴角勾起一丝神秘的微笑，像是在玩一个高深的文字游戏：”&lt;strong>原地滚动——前缀和和后缀和的计算就和状态更新融为一体&lt;/strong>，空间压到O(1)，时间还是O(nk)，常数还更小。”&lt;/p>
&lt;p>“……您的意思是，在更新 &lt;code>f0[j]&lt;/code> 和 &lt;code>f1[j]&lt;/code> 的同时，用一个变量 &lt;code>pre&lt;/code> 来实时维护前缀和？”我试图厘清头绪，感觉大脑CPU要干烧了。&lt;/p>
&lt;p>老勇者看我这副快要宕机的样子，叹了口气，敲了段示意代码：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 计算 f1[j]：依赖所有 f0[k] (k &amp;lt; j) 的和&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>pre &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(k):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f1[j] &lt;span style="color:#f92672">=&lt;/span> pre &lt;span style="color:#75715e"># pre 就是 sum(f0[0], f0[1], ..., f0[j-1]) &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pre &lt;span style="color:#f92672">+=&lt;/span> f0[j] &lt;span style="color:#75715e"># 把当前的 f0[j] 加入前缀和，为下一个 j 做准备&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>“看到没？” 老勇者指了指代码，“&lt;code>f1[j]&lt;/code> 依赖于 &lt;code>f0&lt;/code> 的前缀和，也就是所有下标小于 &lt;code>j&lt;/code> 的 &lt;code>f0&lt;/code> 之和。从左到右顺着遍历，用一个变量 &lt;code>pre&lt;/code> 一路累加 &lt;code>f0&lt;/code> 的值。走到 &lt;code>j&lt;/code> 的时候，&lt;code>pre&lt;/code> 恰好就是 &lt;code>f0[0]&lt;/code> 到 &lt;code>f0[j-1]&lt;/code> 的累加和——直接赋值给 &lt;code>f1[j]&lt;/code>，然后顺手把当前 &lt;code>f0[j]&lt;/code> 加进 &lt;code>pre&lt;/code> 里，为下一个位置做准备。”&lt;/p>
&lt;p>&amp;ldquo;不要额外开 &lt;code>s0&lt;/code> 数组的空间，一轮 O(k) 遍历就能&lt;strong>同时完成前缀和的计算和状态的更新&lt;/strong>。这就是&lt;strong>原地计算前缀和&lt;/strong>的精髓——边算边用，用完即弃。&amp;rdquo;&lt;/p>
&lt;p>我琢磨了一会，恍然大悟：“那对于 &lt;code>f0[j]&lt;/code>（递减状态），我需要的是所有大于 &lt;code>j&lt;/code> 的 &lt;code>f1[k]&lt;/code> 之和，也就是后缀和。我可以从右到左遍历，用同样的方法维护一个 &lt;code>suf&lt;/code> 变量！”&lt;/p>
&lt;p>“总算通了。”老勇者满意地笑了，“一趟顺着走更新 &lt;code>f1&lt;/code>，一趟逆着走更新 &lt;code>f0&lt;/code>。两趟遍历，两个变量，前缀和和后缀和就地计算，算完即用，用完即弃。根本不用调 &lt;code>accumulate&lt;/code> ！”&lt;/p>
&lt;p>他顿了顿，补充道：“还有最后一个细节——你有没有发现，每一轮迭代结束后，&lt;code>f0&lt;/code> 和 &lt;code>f1&lt;/code> 的角色其实互换了？”&lt;/p>
&lt;p>我仔细想了想，瞬时明白了：“对啊！因为下一轮的‘递增状态’依赖于上一轮的‘递减状态’，反之亦然。所以我可以在计算完后，直接交换 &lt;code>f0&lt;/code> 和 &lt;code>f1&lt;/code> 的引用，这样下一轮迭代时，原来的 &lt;code>f1&lt;/code> 就变成了新的 &lt;code>f0&lt;/code>，原来的 &lt;code>f0&lt;/code> 变成了新的 &lt;code>f1&lt;/code>。这样我就不需要额外的变量来存储新状态，直接在原数组上修改就行！”&lt;/p>
&lt;p>老勇者眼中闪过一丝赞赏：“没错。这就是&lt;strong>滚动数组&lt;/strong>的思想。把两轮之间的状态复用做到了极致——不仅省空间，还避免了频繁创建新对象的开销。”&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>MOD &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1_000_000_007&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>k &lt;span style="color:#f92672">=&lt;/span> r &lt;span style="color:#f92672">-&lt;/span> l &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>f0 &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> k
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>f1 &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> k
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 从第二个位置开始迭代&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 从左到右遍历，用 pre变量维护前缀和，同时更新 f1[i]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pre &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i, v &lt;span style="color:#f92672">in&lt;/span> enumerate(f1):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f1[i] &lt;span style="color:#f92672">=&lt;/span> pre &lt;span style="color:#f92672">%&lt;/span> MOD &lt;span style="color:#75715e"># 先赋值再累加，保证 f1[i] 得到的是 f0[0] 到 f0[i-1] 的和&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pre &lt;span style="color:#f92672">+=&lt;/span> v
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 从右到左遍历，用 suf变量维护后缀和，同时更新 f0[i]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> suf &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(k &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v &lt;span style="color:#f92672">=&lt;/span> f0[i]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f0[i] &lt;span style="color:#f92672">=&lt;/span> suf &lt;span style="color:#f92672">%&lt;/span> MOD &lt;span style="color:#75715e"># 同样先赋值再累加，保证 f0[i] 得到的是 f1[i+1] 到 f1[k-1] 的和&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> suf &lt;span style="color:#f92672">+=&lt;/span> v
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 交换 f0 和 f1，为下一轮迭代做准备&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f0, f1 &lt;span style="color:#f92672">=&lt;/span> f1, f0
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 统计所有可能的最后一个元素的方案数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> (sum(f0) &lt;span style="color:#f92672">+&lt;/span> sum(f1)) &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我看着这段代码，感觉它就像一件艺术品——简洁、优雅、高效。没有多余的数组，没有嵌套的循环，只有两个一维数组和三个变量，就完成了整个 DP 的计算。&lt;/p>
&lt;p>“这才是真正的&lt;strong>原地算法&lt;/strong>。”老勇者拿起桌上那瓶快乐水，畅快地灌了一大口，“时间复杂度 O(n×k)，空间复杂度 O(k)，而且常数因子极小。Python 跑四百万次运算，也就是一眨眼的功夫。”&lt;/p>
&lt;p>“大佬，”我深吸一口气，双手抱拳，“您这点拨简直是把我的脑子从单核直接超频到了多核啊！”&lt;/p>
&lt;p>老勇者微微一笑，放下快乐水，语气变得语重心长：“记住，优化的本质不是炫技，而是&lt;strong>找到问题的结构和规律&lt;/strong>。”&lt;/p>
&lt;p>他伸出三根手指，像是在数技能冷却时间：&lt;/p>
&lt;p>“第一，前缀和之所以能用，是因为你的转移方程涉及&lt;strong>区间求和&lt;/strong>——这是问题的数学结构。&lt;br>
第二，原地计算之所以可行，是因为你的遍历顺序和前缀和的累积方向&lt;strong>一致&lt;/strong>——这是算法的执行顺序。&lt;br>
第三，滚动数组之所以有效，是因为你的状态&lt;strong>只依赖于上一层&lt;/strong>——这是 DP 的状态依赖关系。&lt;/p>
&lt;p>这些技巧都不是孤立的魔法，而是建立在对问题深刻理解基础上的&lt;strong>组合拳&lt;/strong>。就像打游戏时的连招，不是死记硬背按键顺序，而是知道每个技能的冷却时间、攻击范围和衔接时机。”&lt;/p>
&lt;p>他站起身，拍了拍我的肩膀，力道不轻不重：“算法也是一样，懂原理，才能玩出花。你今天的表现不错——至少没在我说完&amp;rsquo;前缀和&amp;rsquo;之后问我&amp;rsquo;那是啥&amp;rsquo;。”&lt;/p>
&lt;p>我挠了挠头，有点不好意思：“那不是因为有您在嘛……”&lt;/p>
&lt;p>老勇者哼了一声，算是接受了我的马屁：“前缀和是优化 DP 的常规武器，今天你算是正式入门了。”&lt;/p>
&lt;p>他把可乐往口袋一揣，转身向门口走去。快走到门边时，突然停下脚步，回头补了句：“如果下次 &lt;code>n&lt;/code> 飙到 &lt;code>10^6&lt;/code>，前缀和也不够用，那就得上矩阵快速幂了。不过那是另一个副本的事了。”&lt;/p>
&lt;p>我站在原地，脑子里回荡着&amp;quot;矩阵快速幂&amp;quot;&amp;ldquo;另一个副本&amp;quot;这些词——矩阵快速幂？今天的脑容量都快被锯齿形数组撑爆了好吧！&lt;/p>
&lt;hr>
&lt;h4 id="尾声">尾声
&lt;/h4>&lt;p>窗外那只掉毛信鸽被热得翅膀都懒得扇，蹲在窗台上歪头看我，眼神里写满了“你这题优化得比我的羽毛还顺”。&lt;/p>
&lt;p>从开二维DP到前缀和优化，从前缀和数组到原地滚动——同一道题，三轮迭代，每一次都在追问同一个问题：这个中间结果真的需要存吗？能现场算的，就别存；能用变量滚动的，就别开数组。&lt;/p>
&lt;p>望着老勇者离去的背影，我默默在魔法书上写下今天的总结：&lt;/p>
&lt;blockquote>
&lt;p>以后遇到类似的 DP 问题，记得多问自己几个问题：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>转移方程能不能简化？&lt;/strong>（有没有重复计算可以消除？）&lt;/li>
&lt;li>&lt;strong>空间能不能进一步优化？&lt;/strong>（能不能原地计算？能不能滚动数组？）&lt;/li>
&lt;li>&lt;strong>状态定义能不能更精简？&lt;/strong>（有没有冗余信息可以砍掉？）&lt;/li>
&lt;/ol>&lt;/blockquote>
&lt;p>写完，我合上魔法书，长舒一口气。今天的收获，够我消化好一会了。&lt;/p>
&lt;p>又道是：&lt;/p>
&lt;p>&lt;strong>锯齿轨道zig又zag，暴力炸出 TLE。&lt;/strong>&lt;br>
&lt;strong>前缀和表砍循环，原地滚动省空间。&lt;/strong>
**大佬指点迷津处，矩阵快速幂再见。 **&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode1189 木桶短板定产量，Counter 计数展风光</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-22/</link><pubDate>Mon, 22 Jun 2026 11:32:53 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-22/</guid><description>&lt;h2 id="木桶短板定产量counter-计数展风光">🎮木桶短板定产量，Counter 计数展风光
&lt;/h2>&lt;p>今天，村长又双叒叕发布了个所谓的“简单日常任务”——说是野外刷新了一批&lt;strong>字母史莱姆&lt;/strong>，这玩意儿打死之后不掉金币，只疯狂掉落小写字母碎片。只要集齐一套 &amp;ldquo;balloon&amp;rdquo; 的碎片，就能搓出一个&lt;strong>气球炸弹&lt;/strong>，用来炸开村东头那个卡了三个版本的传送门。&lt;/p>
&lt;p>“小白啊，背包 text 里已经攒了一堆碎片了，赶紧算算能搓几个炸弹。”村长叼着烟斗，头也不抬地疯狂画饼。&lt;/p>
&lt;p>我默默打开背包，看着里面那串乱码一样的字符串：&amp;ldquo;nlaebolko&amp;rdquo;。好家伙，这字母碎片堆得，简直像极了抽卡游戏里满屏的R级狗粮。&lt;/p>
&lt;p>“这还不简单？”我挑了挑眉，嘴角勾起一抹“基操勿6”的自信微笑，“村长，您这任务，不就是个经典的&lt;strong>木桶效应&lt;/strong>嘛！木桶能装多少水，取决于最短的那块木板；我能合成多少次炸弹，取决于哪个字母最先被榨干！在编程界，这叫&lt;strong>取最小值（Min）&lt;/strong>，在管理学里，这叫‘寻找系统瓶颈’！”&lt;/p>
&lt;p>我心里暗暗吐槽：村长这老头，怕不是偷偷去B站刷了《管理学原理》的速成网课，学了个木桶理论就跑来给我当策划了？啧，理论一套套，代码写不好，说的就是他这种人。&lt;/p>
&lt;p>“要拼一个 &amp;lsquo;balloon&amp;rsquo;，配方是：1个 b、1个 a、2个 l、2个 o、1个 n。首先，我得给 text 做个全量扫描，统计每个字母的库存，然后找出最少的那个——”&lt;/p>
&lt;p>“手动写个字典遍历统计？太 low 了，简直是原始人行为！”我反手掏出一块散发着赛博朋克蓝光的&lt;strong>魔法计数板&lt;/strong>（俗称 collections.Counter），在村长面前晃了晃：&lt;/p>
&lt;blockquote>
&lt;p>🔧 &lt;strong>传说级道具：Counter&lt;/strong>
&lt;strong>来源&lt;/strong>：官方collections 模块
&lt;strong>功能&lt;/strong>：自动扫描可迭代对象，瞬间统计每个元素的爆率（次数），并返回一个字典-like 的对象
&lt;strong>效果&lt;/strong>：一行代码，优雅计数，时间复杂度稳稳压在 O(n)，告别 O(n²) 的龟速遍历！&lt;/p>&lt;/blockquote>
&lt;p>&amp;ldquo;一行代码，搞定计数！&amp;ldquo;我潇洒地敲下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">maxNumberOfBalloons&lt;/span>(text: str) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt &lt;span style="color:#f92672">=&lt;/span> Counter(text)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> min(cnt[&lt;span style="color:#e6db74">&amp;#39;b&amp;#39;&lt;/span>], cnt[&lt;span style="color:#e6db74">&amp;#39;a&amp;#39;&lt;/span>], cnt[&lt;span style="color:#e6db74">&amp;#39;l&amp;#39;&lt;/span>] &lt;span style="color:#f92672">//&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>, cnt[&lt;span style="color:#e6db74">&amp;#39;o&amp;#39;&lt;/span>] &lt;span style="color:#f92672">//&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>, cnt[&lt;span style="color:#e6db74">&amp;#39;n&amp;#39;&lt;/span>])
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>等等——如果 text 里根本没有某个字母会怎么样？ 比如 text=&amp;ldquo;leetcode&amp;rdquo;，里面就没一个 &amp;lsquo;b&amp;rsquo; 数。我试了一下：cnt[&amp;lsquo;b&amp;rsquo;] 会返回 0！因为 Counter 的特性是访问不存在的键时返回 0 而不是报错！所以 min(0, &amp;hellip;) 的结果就是 0，正好完美契合“缺一个字母就白搭”的逻辑。&lt;/p>
&lt;p>我一拍大腿，“ 这才是真正的&amp;rsquo;内置容错外挂&amp;rsquo;！连异常处理都省了！python官方是懂我们的！”&lt;/p>
&lt;p>我对着背包里的碎片，露出了奸商般的微笑——把 &amp;ldquo;nlaebolko&amp;rdquo; 传进去——cnt[&amp;lsquo;b&amp;rsquo;]=1, cnt[&amp;lsquo;a&amp;rsquo;]=1, cnt[&amp;rsquo;l&amp;rsquo;]//2=1, cnt[&amp;lsquo;o&amp;rsquo;]//2=1, cnt[&amp;rsquo;n&amp;rsquo;]=1，取最小值得 1。 “完美！刚好搓 1 个炸弹。”&lt;/p>
&lt;p>村长又丢来一串更长的：&amp;ldquo;loonbalxballpoon&amp;rdquo;。我重新调用：b=2, a=2, l=4//2=2, o=4//2=2, n=2，取最小值得 2。 “稳了，两个炸弹！”&lt;/p>
&lt;p>村长点点头，但嘴角带着一丝狡黠：“那要是以后让你合成 &amp;ldquo;apple&amp;rdquo; 呢？你不得重新写个函数？”&lt;/p>
&lt;p>我愣了一下，“村长，您这属于典型的‘需求变更’ ！我这刚写完 &amp;ldquo;balloon&amp;rdquo; ，您转头就要 &amp;ldquo;apple&amp;rdquo;，这搁我们开发圈，得加钱的！”&lt;/p>
&lt;p>但我很快稳住了阵脚，冷笑一声：““真以为我是那种写死配置的脚本小子吗？格局打开，咱们直接上&lt;strong>配置驱动&lt;/strong>！”&lt;/p>
&lt;p>我深吸一口气，开始向村长展示什么叫真正的“高内聚低耦合”：&lt;/p>
&lt;p>“把目标字符串也丢给 Counter 处理，直接拿到配方字典！然后，遍历这个配方字典，用背包里的库存除以配方需求量，最后再取个全局 min。这样一来，别说合成 &lt;code>'apple'&lt;/code>，你就是让我搓个 &lt;code>'abracadabra'&lt;/code>，我也能一行代码给你算出来！”&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">maxNumberOfBalloons&lt;/span>(text: str, word: str) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt_word &lt;span style="color:#f92672">=&lt;/span> Counter(word)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt_text &lt;span style="color:#f92672">=&lt;/span> Counter(text)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> min(cnt_text[char] &lt;span style="color:#f92672">//&lt;/span> cnt_word[char] &lt;span style="color:#66d9ef">for&lt;/span> char &lt;span style="color:#f92672">in&lt;/span> word)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>“看到没？生成器表达式加字典遍历，优雅，丝滑，毫无冗余！”我自信地瞅了眼村长。&lt;/p>
&lt;p>结果村长吧嗒吧嗒抽了两口烟斗，沉默了一会儿，憋出一句：“哦，懂了。那如果背包是空的呢？”&lt;/p>
&lt;p>我愣住了——如果cnt_text[ch] 访问不到就直接返回 0，0 // cnt_word[ch] 还是 0，min 里面有一个 0，整个结果就是 0。不会报错啊！&lt;/p>
&lt;p>&amp;ldquo;村长，&amp;ldquo;我眯起眼睛，往前凑了半步，&amp;ldquo;您这是在给我做&lt;strong>边界测试&lt;/strong>呢？”&lt;/p>
&lt;p>村长嘿嘿一笑：“不，我是在教你，什么叫&lt;strong>防御性编程&lt;/strong>。你以为这就完了？万一哪天系统升级，字母史莱姆掉落的碎片里混进了大写字母，或者带空格、带标点符号，你的 Counter 还能认出来吗？”&lt;/p>
&lt;p>我：“……”好家伙，这是从算法题直接跳到&lt;strong>数据清洗&lt;/strong>了是吧？&lt;/p>
&lt;p>我咬咬牙，把代码又往上叠了一层：“ 加层过滤！先来个 .lower() 统一转小写，再来个 filter(str.isalpha, &amp;hellip;) 把非字母字符全扬了，最后再丢给 Counter！这样总行了吧？上到国际通用字母，下到emoji，全部能处理！&amp;rdquo;&lt;/p>
&lt;p>村长满意地点点头，把烟斗往鞋底上磕了磕：“不错，孺子可教。不过……”&lt;/p>
&lt;p>“不过什么？！”我感觉脑门上突突的。&lt;/p>
&lt;p>“不过村东头那个传送门，”村长指了指远处，“其实不是卡BUG了。是上个月我我嫌密码太简单，改成了 &amp;lsquo;abracadabra&amp;rsquo;，结果转头就忘了……” 村长继续说道：“你刚才这么一说，我忽然想起来密码是啥了。”&lt;/p>
&lt;p>村长又补了句：“你搓的这几个炸弹也别浪费，拿去村西头的池塘炸点小鱼吧，最近鱼有点多。“&lt;/p>
&lt;p>我默默地把 Counter 收回背包，深吸一口气，微笑着说：“村长，您知道在计算机领域，把简单问题复杂化，然后假装自己解决了什么大难题的人，一般叫什么吗？”&lt;/p>
&lt;p>村长：“叫什么？”&lt;/p>
&lt;p>我：“&lt;strong>架构师&lt;/strong>。”&lt;/p>
&lt;p>村长：“……滚。”&lt;/p>
&lt;hr>
&lt;p>这正是：&lt;/p>
&lt;p>&lt;strong>气球合成别蛮干，Counter 计数展风光。&lt;/strong>&lt;/p>
&lt;p>&lt;strong>木桶短板定产量，村长一言全白忙。&lt;/strong>&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode1833 贪心扫货不迟疑，计数先求最大值</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-21/</link><pubDate>Sun, 21 Jun 2026 16:36:06 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-21/</guid><description>&lt;h2 id="-贪心扫货不迟疑计数先求最大值">🎮 贪心扫货不迟疑，计数先求最大值
&lt;/h2>&lt;p>炎炎夏日，Tony同学热得快要融化了。他冲进雪糕店，发现货架上摆着n支雪糕，每支价格不同（&lt;code>costs[i]&lt;/code>）。站在冰柜前，Tony双眼放光，脑海里只有一个声音在回荡：&lt;strong>“买到最多数量的雪糕！”&lt;/strong>&lt;/p>
&lt;p>但问题来了——Tony摸了摸口袋，只有 coins 块现金，如何在预算内实现雪糕自由？&lt;/p>
&lt;h4 id="第一幕贪心的直觉">第一幕：贪心的直觉
&lt;/h4>&lt;p>Tony的第一反应极其符合人类本能：&lt;strong>买最便宜的！&lt;/strong>&lt;/p>
&lt;p>这就好比下副本刷怪，你只有有限的血瓶（&lt;code>coins&lt;/code>），小怪（便宜雪糕）攻击力低，你能多砍几只攒经验；Boss（贵雪糕）一刀暴击你就得回泉水读秒。贪心算法的核心教义就是——&lt;strong>每次都做当前看似最优的选择，绝不回头，像个渣男。&lt;/strong>&lt;/p>
&lt;p>于是Tony开始扫荡货架：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Solution&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">maxIceCream&lt;/span>(self, costs: List[int], coins: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> costs&lt;span style="color:#f92672">.&lt;/span>sort() &lt;span style="color:#75715e"># 排序，从白菜价到天价&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i, cost &lt;span style="color:#f92672">in&lt;/span> enumerate(costs):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> coins &lt;span style="color:#f92672">&amp;lt;&lt;/span> cost: &lt;span style="color:#75715e"># 买不起了，GG&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> i
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> coins &lt;span style="color:#f92672">-=&lt;/span> cost &lt;span style="color:#75715e"># 咬牙买下，钱包-1s&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> len(costs) &lt;span style="color:#75715e"># 直接清空老板库存！&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>他抓着一大把雪糕就往收银台冲，嘴里还念叨着：“先买便宜的，剩下的钢镚儿还能凑一根，这波血赚！”&lt;/p>
&lt;p>就在这时，柜台后面慢悠悠地探出一颗脑袋——雪糕店老板，一个穿着格子衫、发际线略显峥嵘的中年男人，鼻梁上架着那副仿佛焊死在脸上的黑框眼镜。&lt;/p>
&lt;p>老板推了推眼镜，打量了Tony一番：“哟，小伙子，骨骼精奇啊！一眼就看穿了贪心的本质。不过嘛……”&lt;/p>
&lt;p>Tony一愣：“不过啥？”&lt;/p>
&lt;p>老板冷笑一声：“你有没有想过，如果雪糕价格范围很小，比如最贵的也就几块钱，你没必要把整个数组排序——&lt;code>O(n log n)&lt;/code> 虽然不慢，但还有更骚的操作。”&lt;/p>
&lt;p>Tony 眼睛一亮：”更骚的操作？“&lt;/p>
&lt;p>老板诡谲一笑，吐出四个字：“&lt;strong>计数排序（Counting Sort）&lt;/strong>。”&lt;/p>
&lt;h4 id="第二幕计数排序批发场景的神器">第二幕：计数排序——批发场景的神器
&lt;/h4>&lt;p>老板从柜台底下抽出一张皱巴巴的进货单，用圆珠笔在上面画了个表格：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>场景&lt;/strong>：假设雪糕价格只有 1~5 块钱，n=100000。
&lt;strong>普通排序&lt;/strong>：&lt;code>O(n log n)&lt;/code> ≈ 100000 × 17 = 170万次比较。
&lt;strong>计数排序&lt;/strong>：&lt;code>O(n + maxCost)&lt;/code> ≈ 100000 + 5 = 10万次操作。&lt;/p>&lt;/blockquote>
&lt;p>Tony眼睛都亮了：“快了一整个数量级？！”&lt;/p>
&lt;p>老板扬了扬下巴：“没错。计数排序不比较大小，它数数——统计每个价格出现了多少次，然后从便宜到贵依次购买。就好比你玩抽卡游戏，管它具体是哪张卡呢，你只看‘1星有几张’、‘2星有几张’，然后从低到高无脑合成突破，这效率不就上来了？”&lt;/p>
&lt;p>说罢，老板从柜台底下又摸出一把布满灰尘的机械键盘， &lt;code>C&lt;/code> 和 &lt;code>V&lt;/code> 键磨损严重，他手指像飞一样：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Solution&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">maxIceCream&lt;/span>(self, costs: List[int], coins: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 找到最贵的雪糕——决定桶的数量&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mx &lt;span style="color:#f92672">=&lt;/span> max(costs)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 开一个计数数组，像超市货架标签一样，每个价格一个格子&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (mx &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> cost &lt;span style="color:#f92672">in&lt;/span> costs:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt[cost] &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 从 1 块钱开始，按顺序扫货&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> cost &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, mx &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> coins &lt;span style="color:#f92672">&amp;lt;&lt;/span> cost: &lt;span style="color:#75715e"># 钱不够买当前价格的雪糕了&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">break&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> num &lt;span style="color:#f92672">=&lt;/span> min(cnt[cost], coins &lt;span style="color:#f92672">//&lt;/span> cost) &lt;span style="color:#75715e"># 预算能买得起几根？直接算出来&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> coins &lt;span style="color:#f92672">-=&lt;/span> num &lt;span style="color:#f92672">*&lt;/span> cost
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">+=&lt;/span> num
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Tony看得目瞪口呆：“ &lt;code>coins // price&lt;/code> 直接批量采购，比我之前那 &lt;code>for&lt;/code> 循环一根根买要快多了！&lt;strong>用空间换时间&lt;/strong>，拿内存换执行速度，经典套路！”&lt;/p>
&lt;p>老板满意地点头：“懂行！计数排序的精髓就是开个 &lt;code>count&lt;/code> 数组存每个价格的出现次数，空间复杂度 &lt;code>O(maxCost)&lt;/code>。价格区间越小，它越猛。我平时批发雪糕都是这么算的——不吹牛，进货单我从来不用计算器，脑子一跑计数排序，老板都得喊我一声‘人形自走ERP’。”&lt;/p>
&lt;p>他顿了顿，语气突然变得语重心长：“不过嘛……要是敢把价格上限干到一亿，开个一亿长度的数组——那场面，内存当场就得给你报个 &lt;code>MemoryError&lt;/code>。遇到这种情况，还是老老实实 &lt;code>sort()&lt;/code> 吧。毕竟咱是来买雪糕的，不是来给服务器做压力测试的，对吧？”&lt;/p>
&lt;h4 id="第三幕大佬的传说">&lt;strong>第三幕：大佬的传说&lt;/strong>
&lt;/h4>&lt;p>Tony 捧着雪糕，忍不住问：“老板，你以前是干嘛的？这代码写得比我老师还溜。”&lt;/p>
&lt;p>老板叹了口气，望向窗外的烈日，眼神仿佛穿透了雪糕店的天花板，伸向了遥远的服务器机房。&lt;/p>
&lt;p>“我以前啊……” 老板缓缓摘下黑框眼镜，用格子衫的衣角擦了擦，“ 在某大厂干过七年，负责核心交易链路的库存系统。每年大促那天，全球几亿人同时抢购，我就得在毫秒之间算出‘最多能买多少件商品’——跟你今天这雪糕问题，本质一模一样，就是规模大了亿点点。”&lt;/p>
&lt;p>Tony嘴里的冰棍差点掉下来：“卧槽，那你为啥跑来卖雪糕？”&lt;/p>
&lt;p>老板重新戴上眼镜，嘴角抽搐了一下：“那年双十一，我为了优化一个排序模块，强行上计数排序，结果商品价格范围没估算好——有个奢侈品的价格是九千九百九十九万……我抬手就开了一个一亿长度的 &lt;code>count&lt;/code> 数组，内存直接原地爆炸。整个订单系统卡了整整三秒钟。”&lt;/p>
&lt;p>他顿了顿，拿起一根绿豆冰棍狠狠咬了一口：“三秒钟意味着什么？三秒钟足够用户截屏发微博骂你十八遍，足够竞品写一篇‘某厂又崩了’的公众号爆文，足够老板的脸色从猪肝色变成五彩斑斓的黑。第二天，HR就把我叫进小黑屋，递给我一张纸条，上面写着：‘感谢贡献，江湖再见’。”&lt;/p>
&lt;p>Tony 听得头皮发麻：“所以你就拿着赔偿金开了雪糕店？”&lt;/p>
&lt;p>老板摊了摊手：“不然呢？我面了一圈，面试官问我‘你上次线上事故的原因是什么’，我说‘计数排序数组开太大了’，然后就没了下文。后来我一想，算了，自己当老板吧。至少在这里，雪糕价格上限不会超过二十块，我手动写个 &lt;code>count[21]&lt;/code> 的数组，内存绝对不会爆——爆了也就损失几根老冰棍，不至于让几亿人刷不出购物车。”&lt;/p>
&lt;p>Tony啃着最后一根冰棍，若有所思：“所以——计数排序本身没毛病，错的是没先看一眼 &lt;code>max(costs)&lt;/code>。”&lt;/p>
&lt;p>老板一拍大腿：“你总算开窍了！先看最大值再决定用啥算法，前置防御式编程’，是我拿七年职业生涯换来的经验教训。你要记住——&lt;strong>提前优化是万恶之源，但提前检查数据范围是保命之道&lt;/strong>。”&lt;/p>
&lt;p>Tony郑重地点点头，把最后一口冰棍吞下去，含含糊糊地说：“懂了，明天我就把这句话刻在键盘上。”&lt;/p>
&lt;p>这正是：&lt;/p>
&lt;p>&lt;strong>贪心扫货不迟疑，计数先求最大值。&lt;/strong>
&lt;strong>老板当年爆内存，如今卖糕教解题。&lt;/strong>&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode1840 十亿高楼莫开阵，万点锚链定乾坤</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-20/</link><pubDate>Sat, 20 Jun 2026 22:40:44 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-20/</guid><description>&lt;h2 id="-十亿高楼莫开阵万点锚链定乾坤">🎮 十亿高楼莫开阵，万点锚链定乾坤
&lt;/h2>&lt;p>你是一个刚拿到城市规划师资格证的新人，市长给了你一个看似简单的任务：&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;小伙子，给你 n 块地，给我建一排楼。记住三条规矩：&amp;rdquo;&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>&lt;strong>第一栋楼必须是平地（高度 0）&lt;/strong> —— &amp;ldquo;市政府的脸面，不能比隔壁穷&amp;rdquo;&lt;/li>
&lt;li>&lt;strong>相邻楼高度差不能超过 1&lt;/strong> —— &amp;ldquo;市民恐高，差距太大要打架的&amp;rdquo;&lt;/li>
&lt;li>&lt;strong>有些地块有硬性限高令&lt;/strong> —— &amp;ldquo;某些地段被大佬盯上了，你懂的&amp;rdquo;&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>目标：在不违反任何规则的前提下，让最高的楼尽可能高。&lt;/strong>&lt;/p>
&lt;p>听起来很简单？呵呵，天真。&lt;code>n&lt;/code> 最大能达到 &lt;code>10^9&lt;/code>，你要是敢开个数组暴力模拟，内存直接 OOM 给你看，连报错信息都来不及显示就 GG 了。&lt;/p>
&lt;hr>
&lt;h3 id="naive-玩家的陷阱">naive 玩家的陷阱：
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">maxBuilding&lt;/span>(self, n: int, restrictions: List[List[int]]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 直接开数组，炸你没商量&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> heights &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (n &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> heights[&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> idx, h &lt;span style="color:#f92672">in&lt;/span> restrictions:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> heights[idx] &lt;span style="color:#f92672">=&lt;/span> h
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 从前往后推&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">2&lt;/span>, n &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> heights[i] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">or&lt;/span> heights[i] &lt;span style="color:#f92672">&amp;gt;&lt;/span> heights[i &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> heights[i] &lt;span style="color:#f92672">=&lt;/span> heights[i &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 从后往前推&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(n &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> heights[i] &lt;span style="color:#f92672">&amp;gt;&lt;/span> heights[i &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> heights[i] &lt;span style="color:#f92672">=&lt;/span> heights[i &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> max(heights)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>逻辑非常直观，开一个长度为n+1的数组，1号楼高度0，所有限高令填进去，然后：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>从左往右扫一遍&lt;/strong>：每栋楼最多比左边高 1 层（&amp;ldquo;老王家的楼比你高一截，你不能比他高太多&amp;rdquo;）&lt;/li>
&lt;li>&lt;strong>从右往左扫一遍&lt;/strong>：每栋楼最多比右边高 1 层（&amp;ldquo;老李家的楼也得考虑，做人要圆滑&amp;rdquo;）&lt;/li>
&lt;li>&lt;strong>取最大值&lt;/strong>：完事！&lt;/li>
&lt;/ul>
&lt;p>思路没毛病，代码写得也挺工整，如果这是在面试的话，你已经成功拿到——&amp;ldquo;谢谢参与&amp;rdquo;！&lt;/p>
&lt;p>当 &lt;code>n = 10^9&lt;/code> 时，你将需要创建出一个长度为 &lt;code>10^9 + 1&lt;/code> 的列表。每个整数在 Python 里大约占用 28 字节（别问为什么这么多，问就是 Python 的对象头、引用计数、类型指针一堆开销），&lt;code>总内存 ≈ 10^9 × 28 bytes ≈ 28 GB&lt;/code>——28 GB！&lt;/p>
&lt;p>你的电脑会经历以下心路历程：&lt;/p>
&lt;blockquote>
&lt;p>第 1 秒： &amp;ldquo;嗯？用户要申请 28GB 内存？行吧，我看看&amp;hellip;&amp;rdquo;
第 2 秒： &amp;ldquo;等等，系统总共才 16GB，这哥们儿疯了吧？&amp;rdquo;
第 3 秒： &amp;ldquo;不行，我得阻止这场灾难——MemoryError！滚！&amp;rdquo;&lt;/p>&lt;/blockquote>
&lt;p>然后你的程序连个像样的报错都来不及打印，就直接被操作系统掐死了，就像游戏里刚出新手村就被满级 Boss 一巴掌拍回重生点，连复活币都来不及用。&lt;/p>
&lt;blockquote>
&lt;p>🕳 脑洞小剧场 1：
面试官：&amp;ldquo;你这个算法的空间复杂度是多少？&amp;rdquo;
你：&amp;ldquo;O(n)&amp;hellip; 呃，应该是线性级别？&amp;rdquo;
面试官：&amp;ldquo;那如果 n 是十亿呢？&amp;rdquo;
你：&amp;ldquo;那就&amp;hellip; 换台内存更大的机器？实在不行上分布式？&amp;rdquo;
面试官（微笑）：&amp;ldquo;好的，今天的面试就到这里，出门右转不送。&amp;rdquo;&lt;/p>
&lt;p>🕳 脑洞小剧场 2：
现实中的程序员：&amp;ldquo;我代码本地测试好好的啊，怎么一上线就崩？&amp;rdquo;
运维大哥（看着监控大屏上飙升的内存曲线）：&amp;ldquo;你这哪是写代码，你这是在挖矿吧？&amp;rdquo;
Python 解释器：&amp;ldquo;兄弟，我知道你是搞算法的，但你能不能有点 B 树？我这小身板扛不住你这么造啊！&amp;rdquo;&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h4 id="正确的姿势四两拨千斤">正确的姿势：四两拨千斤
&lt;/h4>&lt;p>既然不能遍历所有建筑，那就&lt;strong>只处理有限制条件的建筑&lt;/strong>！毕竟n大到十亿，但限制只有十万。而且，最重要的是，没有限制的建筑，它们的高度完全由相邻的限制点决定——就像游戏里的 NPC，行为完全由几个关键剧情点控制。你不需要模拟整个开放世界，只需要抓住主线任务的关键节点。&lt;/p>
&lt;p>也就是说，只需要把有限制的位置拎出来，两两相邻之间从两端往中间爬坡，最高点就是这段区间的天际线峰值。&lt;/p>
&lt;p>&lt;strong>第一步：添加两个&amp;quot;隐形守护者&amp;quot;&lt;/strong>，把1号楼和n号楼也当成限制点加进去：第一栋楼高度为 0（[1, 0]），最后一栋楼理论上可以无限高（[n, inf]）——至少在没受到限制传导之前是这样。&lt;/p>
&lt;p>&lt;strong>第二步：按建筑编号从小到大排序&lt;/strong>，就像把NPC按出场顺序排好。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Solution&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">maxBuilding&lt;/span>(self, n: int, restrictions: List[List[int]]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 第一步：加上两个&amp;#34;隐形锚点&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> restrictions &lt;span style="color:#f92672">+=&lt;/span> [[&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>], [n, inf]]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 第二步：按建筑编号从小到大排序&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> restrictions&lt;span style="color:#f92672">.&lt;/span>sort(key&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">lambda&lt;/span> a: a[&lt;span style="color:#ae81ff">0&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 第三步：准备两轮扫描：&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m &lt;span style="color:#f92672">=&lt;/span> len(restrictions)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> h &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> m &lt;span style="color:#75715e"># h[i] 表示第 i 个锚点的真实最大高度 &lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>第三步：双向松弛——左右横跳直到收敛&lt;/strong>&lt;/p>
&lt;p>从左往右扫第一遍。每两个相邻限制点之间隔了d栋楼，左端点高度已知，右端点有硬性上限。从左边出发，每往右一栋楼最多升1层，传过来的理论最大值是&lt;code>左端点高度 + d&lt;/code>，但它还有个硬性上限卡着，所以实际高度取两者的较小值。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 从左往右扫，传导&amp;#34;左边来的限制&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, m):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dist &lt;span style="color:#f92672">=&lt;/span> restrictions[i][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">-&lt;/span> restrictions[i&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> h[i] &lt;span style="color:#f92672">=&lt;/span> min(h[i&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">+&lt;/span> dist, restrictions[i][&lt;span style="color:#ae81ff">1&lt;/span>])
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第一遍扫完还不够。从右往左再扫一遍——限制是双向传导的——每个点的高度取“右边传来的理论最大值”和“当前高度”的较小值。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 从右往左扫，传导&amp;#34;右边来的限制&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(m&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">2&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dist &lt;span style="color:#f92672">=&lt;/span> restrictions[i&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">-&lt;/span> restrictions[i][&lt;span style="color:#ae81ff">0&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> h[i] &lt;span style="color:#f92672">=&lt;/span> min(h[i], h[i&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">+&lt;/span> dist)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>两轮传导下来，每个限制点的实际最大高度就定死了，确保每个点都收到了来自两侧的“最严要求”。&lt;/p>
&lt;p>就像是在玩一个‘传话筒’游戏。第一轮从左往右传，每个锚点说：‘我从左边过来，最多只能到这么高’。第二轮从右往左传，每个锚点又说：‘我从右边过来，最多也只能到这么高’。两轮传完，每个锚点的真实最大高度就出来了。&lt;/p>
&lt;blockquote>
&lt;p>这叫&amp;quot;动态规划的双向松弛操作&amp;quot;。听不懂没关系，你就理解为&amp;quot;左右横跳直到收敛&amp;quot;，类似于 Bellman-Ford 算法求最短路的思路，只不过这里是求&amp;quot;最高高度&amp;quot;。&lt;/p>&lt;/blockquote>
&lt;p>&lt;strong>最后一步：计算相邻限制点之间的峰值&lt;/strong>&lt;/p>
&lt;p>限制点的高度全部确定之后，剩下的就是算相邻两点之间的天际线峰值。&lt;/p>
&lt;p>两个限制点之间，左边高度&lt;code>h1&lt;/code>，右边高度&lt;code>h2&lt;/code>，距离&lt;code>d&lt;/code>。从左边往上爬，从右边往上爬，两条坡道在中间某处相遇，那个顶点的高度就是&lt;code>(d + h1 + h2) // 2&lt;/code>。就像在两个固定高度的塔之间搭一座拱桥，你要让桥的最高点尽可能高，但又不能让坡度太陡。这个公式就是在计算最优拱形的顶点。&lt;/p>
&lt;p>遍历所有相邻限制点对，取最大的顶点高度，就是全城的天际线之巅。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 两个锚点之间的最高高度 = (距离 + 左高 + 右高) // 2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> max(restrictions[i &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">-&lt;/span> restrictions[i][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">+&lt;/span> h[i] &lt;span style="color:#f92672">+&lt;/span> h[i &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(m &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)) &lt;span style="color:#f92672">//&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>时间O(m log m)，空间O(m)，只需要存储限制点，而不是所有建筑。&lt;/p>
&lt;p>把十亿次操作压缩成十万次，把28GB内存压缩成几个变量——不是靠更快的机器，是靠更聪明的思路。&lt;/p>
&lt;hr>
&lt;p>&lt;strong>通关口诀&lt;/strong>：&lt;/p>
&lt;p>&lt;strong>十亿高楼莫开阵，万点锚链定乾坤。&lt;/strong>&lt;/p>
&lt;p>&lt;strong>双向传导消冲突，拱桥公式现峰值。&lt;/strong>&lt;/p>
&lt;p>不要试图控制每一个细节（内存会炸），而是要抓住关键节点（限制点），让它们之间的区域自动达到最优状态。&lt;/p>
&lt;p>这不就是人生吗？——你不可能掌控每一件事，但你可以抓住那些&lt;strong>真正重要的转折点&lt;/strong>！&lt;/p>
&lt;p>等等——这是什么强行升华的鸡汤转折？！打住！！&lt;/p>
&lt;p>现实很骨感的，就算你抓住了人生的关键节点，你人生的内存该爆还是得爆。你老板不会因为你懂“拱桥公式”就给你涨薪，你对象也不会因为你会写“双向扫描”就不跟你分手。&lt;/p>
&lt;p>所以，收起你的文艺细胞，先把今天的拱桥公式消化透。起码保证下次面试，不会再因为这题收到面试官微笑着递来的 &lt;strong>MemoryError 好人卡&lt;/strong>。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode1732 莫道水题无意义，心态修炼在其中</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-19/</link><pubDate>Fri, 19 Jun 2026 23:32:30 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-19/</guid><description>&lt;h2 id="莫道水题无意义心态修炼在其中">🎮莫道水题无意义，心态修炼在其中
&lt;/h2>&lt;p>清晨的阳光刚透过窗帘缝隙，我正沉浸在&amp;quot;泡面配码，法力无边&amp;quot;的晨间仪式中，窗户再次传来了那熟悉的&amp;quot;砰&amp;quot;的一声闷响。那只掉毛的信鸽又一次精准地砸在了我的键盘旁边——差点把我的 Enter 键给砸飞了。&lt;/p>
&lt;p>我熟练地抽出纸条，村长的狂草一如既往地奔放：&amp;ldquo;小白！今天给你来个休闲养生局！自行车手骑行问题，求最高海拔！简单题，签到题，送分题！别问为什么又是签到，问就是端午节福利大礼包！&amp;rdquo;&lt;/p>
&lt;p>端午节福利？一个虚拟骑行的海拔计算题？我把纸条翻了个面——空白。再翻回来，确实只有这几行字。&lt;/p>
&lt;p>靠，这抠门老头，拿一道算法题当端午节福利？别人家的福利是粽子咸鸭蛋，我的福利是数组遍历求前缀和的最大值？要不要再贴个标签——“祝您端午安康，O(n)时间O(1)空间”？村长这是把抠门发挥到了大气层以外！要是让隔壁村的勇者知道，还以为我们村穷到连节日福利都得靠刷题兑换。&lt;/p>
&lt;p>窗外的信鸽咕咕叫了两声，眼神里写满了&amp;quot;你自己品&amp;quot;三个大字，然后扑棱着翅膀飞走了。&lt;/p>
&lt;h4 id="签到题的自我修养">签到题的自我修养
&lt;/h4>&lt;p>我把纸条揉成一团，拉过椅子坐下。嘴上嫌弃，身体诚实——来都来了，签到后再嗦面。&lt;/p>
&lt;p>“有一个自行车手打算进行一场公路骑行……路线由 n+1 个不同海拔的点组成，从海拔0开始。gain[i]是点i和点i+1的净海拔高度差。返回最高点的海拔。”&lt;/p>
&lt;p>再看示例：&lt;code>gain = [-5, 1, 5, 0, -7]&lt;/code>，海拔序列为 &lt;code>[0, -5, -4, 1, 1, -6]&lt;/code>，最高是1。&lt;/p>
&lt;p>&amp;ldquo;这哪是骑行啊，&amp;ldquo;我忍不住吐槽，&amp;ldquo;这哥们骑的是过山车吧？一会儿上一会儿下，还净海拔高度差，说得这么高大上，不就是 &amp;lsquo;当前位置 += gain[i]&amp;rsquo; 吗？&amp;rdquo;&lt;/p>
&lt;p>我挑了挑眉，这题就是求前缀和的最大值：从 0 出发，一路累加 gain[i]，沿途顺手记下最大值。&lt;/p>
&lt;h4 id="朴实无华的前缀和">朴实无华的前缀和
&lt;/h4>&lt;p>思路简单得像新手村教程关卡：一个变量 &lt;code>current&lt;/code> 记录当前海拔，每过一个赛段就加上高度差；另一个变量 &lt;code>ans&lt;/code> 记录沿途遇到的最高点。一趟遍历，两个变量，O(n)时间，O(1)空间，连数组都不用开。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Solution&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">largestAltitude&lt;/span>(self, gain: List[int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> tmp &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> val &lt;span style="color:#f92672">in&lt;/span> gain:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tmp &lt;span style="color:#f92672">+=&lt;/span> val &lt;span style="color:#75715e"># 累加当前的高度差&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> max(ans, tmp) &lt;span style="color:#75715e"># 更新最大值&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>点击提交，AC 瞬弹。我盯着那个绿色大勾，总觉得少了点什么。就像全副武装准备打 Boss，结果进门发现是个史莱姆，一刀下去就爆了，连经验值都没给几点。&lt;/p>
&lt;h4 id="pythonic-的优雅一击">Pythonic 的优雅一击
&lt;/h4>&lt;p>就在我准备关题吃面时，脑子里突然蹦出一个念头：“等等，有没有更 Pythonic 的写法？”&lt;/p>
&lt;p>一个熟悉的名字——&lt;code>itertools.accumulate&lt;/code>——浮现在我的脑袋里，仿佛在大喊“用我用我！”&lt;/p>
&lt;p>“accumulate 函数，用于生成累积和。”我回想起文档里的解释，“给它一个可迭代对象，它会返回一个迭代器，依次产生累积的结果。默认是累加，这不正好合适？” 更妙的是，&lt;code>accumulate&lt;/code> 还有一个 &lt;code>initial&lt;/code> 参数，可以指定初始值。这道题里，自行车手从海拔 0 开始，正好可以用 &lt;code>initial=0&lt;/code> 来表示起点。&lt;/p>
&lt;p>我兴奋地敲下了新的实现，“Pythonic 一行流！”&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> itertools &lt;span style="color:#f92672">import&lt;/span> accumulate
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Solution&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">largestAltitude&lt;/span>(self, gain: List[int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> max(accumulate(gain, initial&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">0&lt;/span>))
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>``accumulate(gain, initial=0)&lt;code>依次产出&lt;/code>0, gain[0], gain[0]+gain[1], &amp;hellip;&lt;code>——完整的海拔序列直接出来了。&lt;/code>max` 一把捞出最高点。&lt;/p>
&lt;p>再次提交，AC。&amp;ldquo;这才是 Python 程序员该有的样子嘛！&amp;ldquo;我得意地笑了笑，&amp;ldquo;用内置库函数代替手写循环，代码简洁优雅，还能装一波&amp;rsquo;我很懂 Python&amp;rsquo;的逼。&amp;rdquo;&lt;/p>
&lt;h4 id="尾声签到的意义">尾声：签到的意义
&lt;/h4>&lt;p>&amp;ldquo;所以说，&amp;ldquo;我一边嗦面一边自言自语，&amp;ldquo;这道题的意义到底是什么？村长不可能无缘无故给我塞一道&amp;rsquo;Hello World&amp;rsquo;级别的签到题吧？&amp;rdquo;&lt;/p>
&lt;p>信鸽不知什么时候又飞回来了，停在窗台上，冲我咕咕叫了两声。我盯着它看了一会儿，&amp;ldquo;莫非，村长这是在训练我的心态？——遇到难题不要慌，遇到水题不要浪！&amp;rdquo;&lt;/p>
&lt;p>信鸽歪着头看我，眼神里三分认可七分&amp;quot;你戏真多&amp;rdquo;。我翻开魔法书，在最新一页写下：&lt;/p>
&lt;p>&lt;strong>海拔爬升前缀和，一行代码定风波。&lt;/strong>
&lt;strong>莫道水题无意义，心态修炼在其中。&lt;/strong>&lt;/p>
&lt;p>写完，我合上魔法书。村长的端午节福利虽然抠门，但至少让我复习了一下 &lt;code>accumulate&lt;/code> 的用法。嗯，就当是吃了个代码味的粽子吧。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode1344 钟面追及本寻常，惊觉快慢指针藏</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-18/</link><pubDate>Thu, 18 Jun 2026 11:29:57 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-18/</guid><description>&lt;h2 id="钟面追及本寻常惊觉快慢指针藏">🎮钟面追及本寻常，惊觉快慢指针藏
&lt;/h2>&lt;p>我盯着屏幕上村长新发来的任务卷轴，眉头皱得能夹死一只苍蝇。&lt;/p>
&lt;p>“时钟指针的夹角……” 我念出声来，“给你两个数 hour 和 minutes，请返回在时钟上，由给定时间的时针和分针组成的较小角的角度？” 我揉了揉眼睛，把卷轴翻来覆去看了三遍，就……一个钟？&lt;/p>
&lt;p>“村长是不是把我的智商当成了可以随便揉捏的面团？” 我把卷轴往桌上一拍，昨天把我甩到 4.5 PB 的虚空里找第 k 个字符，今天让我算钟表夹角？这跨度简直就像刚打完《艾尔登法环》的玛莲妮亚，突然被塞了一盘《宝宝巴士》。&lt;/p>
&lt;p>窗外，那只掉毛的信鸽正好飞过，冲我咕咕叫了两声，语气里充满了“别高兴太早，这是暴风雨前的宁静”的意味。&lt;/p>
&lt;p>我懒得理它，拉过椅子坐下，顺手把泡面推到一边。既然村长要考我小学数学，那我就用最优雅的姿态把这个签到题拿下——就当是给被逆推心法摩擦的脑子做个马杀鸡。&lt;/p>
&lt;h3 id="真快慢指针">真·快慢指针
&lt;/h3>&lt;p>“经典的小学数学追及相遇问题——” 我拿起笔，在纸上画了一个圆，“只不过把直线轨道换成了圆形——分针一小时跑一圈，时针十二小时跑一圈。追及的两个‘人’换成了时针和分针，速度单位从‘米每秒’变成了‘度每分’。”&lt;/p>
&lt;p>整个钟面 360 度，12 个大格，每个大格 30 度，60 个小格，每个小格 6 度。分针是个急性子，1 小时跑一圈 360 度，每分钟跑 6 度。时针是个摸鱼大师，1 小时才挪 30 度，每分钟挪 0.5 度。我轻松地吹了声口哨：“分针每分钟 6 度，时针每分钟 0.5 度，相对速度 5.5 度每分——”&lt;/p>
&lt;p>等等。我盯着纸上的两个速度值——脑子里蹦出一个熟悉的画面。&lt;/p>
&lt;p>“一个跑得快，一个跑得慢，还在同一个环形轨道上转悠……”我猛地一拍大腿，整个人从椅子上弹了起来，“这特么不就是&lt;strong>真·快慢指针&lt;/strong>吗？！”&lt;/p>
&lt;p>想当年刷链表题的时候，为了判环、找入环点，硬生生造了一个 &lt;code>fast&lt;/code> 和一个 &lt;code>slow&lt;/code> 在链表上赛跑，一个走两步一个走一步，&lt;code>while&lt;/code> 循环里每轮都小心翼翼，生怕指针指飞了直接喜提 NullPointerException。&lt;/p>
&lt;p>结果现在？人家物理引擎直接给你安排好了！分针就是那个 &lt;code>fast&lt;/code> 指针，时针就是那个 &lt;code>slow&lt;/code> 指针，它俩天然就在环形轨道上跑，连 &lt;code>while&lt;/code> 循环都不用写，相对速度直接做减法就完事了。刷了那么多链表题，快慢指针的终极形态原来藏在钟面上。&lt;/p>
&lt;p>“村长这波属于是跨界致敬了。”我轻快地笑了笑，在纸上写下计算公式。&lt;/p>
&lt;p>以 12 点方向为 0 度基准：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>分针角度&lt;/strong> = &lt;code>minutes * 6&lt;/code>&lt;/li>
&lt;li>&lt;strong>时针角度&lt;/strong> = &lt;code>(hour % 12) * 30 + minutes * 0.5&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>那个 &lt;code>hour % 12&lt;/code> 纯粹是我的强迫症，十二点当成零来处理，不然算出来是三百六十度，数学上没错，但脑子里总会冒出“这不转了一圈回到原点吗”的自我怀疑。&lt;/p>
&lt;p>两针相减取绝对值，如果超过 180 度就用 360 减一下，取较小的那个角。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Solution&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">angleClock&lt;/span>(self, hour: int, minutes: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> float:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 分针角度：每分钟6度&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> min_degree &lt;span style="color:#f92672">=&lt;/span> minutes &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">6&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 时针角度：每小时30度 + 每分钟0.5度&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> hour_degree &lt;span style="color:#f92672">=&lt;/span> (hour &lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#ae81ff">12&lt;/span>) &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">30&lt;/span> &lt;span style="color:#f92672">+&lt;/span> minutes &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">0.5&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 两针夹角的绝对值&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> diff &lt;span style="color:#f92672">=&lt;/span> abs(hour_degree &lt;span style="color:#f92672">-&lt;/span> min_degree)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 取较小角（不超过180度）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> min(diff, &lt;span style="color:#ae81ff">360&lt;/span> &lt;span style="color:#f92672">-&lt;/span> diff)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>点击提交，AC 几乎是在我眨眼的瞬间就弹了出来。&lt;/p>
&lt;p>我靠在椅背上，盯着那个绿勾，突然感到一阵前所未有的空虚。没有熟悉的后脑勺崩，没有老勇者的灵魂拷问，连泡面都还没来得及坨。这也太简单了吧，简单到让人觉得有点敷衍。&lt;/p>
&lt;p>但转念一想，村长这波跨界致敬还是挺有意思的——小学数学的皮，链表算法的魂。翻开魔法书，我在最新一页写下：&lt;/p>
&lt;p>&lt;strong>钟面追及本寻常，惊觉快慢指针藏。&lt;/strong>
&lt;strong>寥寥几行破签到，难得泡面尚未凉&lt;/strong>。&lt;/p>
&lt;p>写完，我端起泡面，美美地嗦了一大口，享受着这难得的清静。&lt;/p>
&lt;p>“砰！” 窗外又传来一声熟悉的闷响。&lt;/p>
&lt;p>我条件反射地护住泡面碗，抬头一看——那只掉毛的信鸽又来了，嘴里叼着一张新的小纸条。我把纸条抽出来，上面是村长歪歪扭扭的字迹：“小白，今天热热身。明天再上强度。另外，鸽粮经费已经批了，但被我拿去抽卡了，你继续喂它点泡面。——村长”&lt;/p>
&lt;p>“……” 我把纸条揉成一团，精准地投进了垃圾桶。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3614 倒转乾坤寻真身，去冗存真见精神</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-17/</link><pubDate>Wed, 17 Jun 2026 12:08:54 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-17/</guid><description>&lt;h2 id="倒转乾坤寻真身去冗存真见精神">🎮倒转乾坤寻真身，去冗存真见精神
&lt;/h2>&lt;p>早上，我刚把一碗热气腾腾的泡面端上桌，准备享受这碳水与油脂交织的美好清晨，窗户又发出了熟悉的“砰砰”声。我眼疾手快地把碗往旁边一挪，果不其然，那只掉毛的信鸽像一架失去动力的无人机，精准地砸向桌面。&lt;/p>
&lt;p>“你就不能换个着陆点？”我把鸽子往旁边拨了拨，熟练地抽出它嘴里叼着的羊皮纸。展开一看，村长的狂草再次扑面而来：“小白！字母大陆又更新了！这次不要求返回整个密文，只要求返回最终密文的第 k 个字母（下标从 0 开始）！如果 k 越界了，就返回一个 &lt;code>.&lt;/code>！另外，字符串长度上限已经放宽到 10^5 ，祝你好运！”&lt;/p>
&lt;p>“果然来了。” 我盯着那个 10^5，昨天 19 个井号就炸出 52 万个字符，模拟还能硬扛；今天 &lt;code>s&lt;/code> 的长度放到十万，里面说不定藏着一整支井号军团。模拟要是敢上场，评测机怕是要把我的提交记录裱起来挂在“反面教材”专栏里。&lt;/p>
&lt;p>我深吸一口气，把昨天老勇者画的那张竖杠图从魔法书里抽出来，平铺在桌上。&lt;/p>
&lt;p>昨天老勇者已经给我透了底，说这类题是“空间逆推”的主场——“&lt;code>#&lt;/code>号是复制对称，&lt;code>%&lt;/code>号是镜像对称，&lt;code>*&lt;/code>号是末端删减。找第 k 个字符，就是在一场逆向追踪里，从终点坐标一路剥开层层嵌套的几何变换，精准锁定最初的‘本体’。”&lt;/p>
&lt;p>这段心法我在脑子里念叨了半宿，连做梦都在算对称位置。今天，是时候上招式了。&lt;/p>
&lt;h4 id="正向探路反向溯源">&lt;strong>正向探路，反向溯源&lt;/strong>
&lt;/h4>&lt;p>开干之前先盘逻辑。昨天老勇者留下的话头很清楚——逆向追踪，从终点坐标一路剥开层层嵌套的几何变换。但在那之前，我得先正向探路，记下沿路操作对字符串几何结构的影响。就像顺着折痕一路摸回纸片最初的形状，首先得知道每次对折、翻面、撕角之后，纸片变成了多大。不需要知道纸上写了什么字，只需要知道尺寸。&lt;/p>
&lt;p>“&lt;strong>第一步，正向探路&lt;/strong>。”&lt;/p>
&lt;p>我在纸上画了一个从左到右的箭头，“还是从左到右扫一遍操作序列，但不存实际字符，只记录每一步之后 result 的长度。字母来了，长度加一；星号来了，长度减一；井号来了，长度翻倍；百分号来了，长度不变。一趟扫完，我手里就有一张‘长度变化表’——每个操作时刻，result 有多长，清清楚楚。”&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 正向扫描，只记长度&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n &lt;span style="color:#f92672">=&lt;/span> len(s)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> size &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> n
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i, c &lt;span style="color:#f92672">in&lt;/span> enumerate(s):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> c&lt;span style="color:#f92672">.&lt;/span>isalpha():
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> size[i] &lt;span style="color:#f92672">=&lt;/span> (size[i &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#66d9ef">if&lt;/span> i &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>) &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> c &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#39;*&amp;#39;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> size[i] &lt;span style="color:#f92672">=&lt;/span> max(&lt;span style="color:#ae81ff">0&lt;/span>, (size[i &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#66d9ef">if&lt;/span> i &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>) &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> c &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#39;#&amp;#39;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> size[i] &lt;span style="color:#f92672">=&lt;/span> (size[i &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#66d9ef">if&lt;/span> i &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>) &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> size[i] &lt;span style="color:#f92672">=&lt;/span> size[i &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#66d9ef">if&lt;/span> i &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>全程只存长度，不存字母，井号再多也只翻倍长度值，不翻倍数据本身。不管字符串膨胀到多少个 2 的 n 次方，一个整数数组就能装下。&lt;code>size[i]&lt;/code> 就是第 i 步操作后，result 的真实长度。&lt;/p>
&lt;p>有了这张表，我就能从终点开始，拿着目标坐标 &lt;code>k&lt;/code> 一路往回倒推。&lt;/p>
&lt;p>“&lt;strong>第二步，反向溯源&lt;/strong>。”&lt;/p>
&lt;p>我盯着 &lt;code>size&lt;/code> 数组，就像看到了一条从终点延伸回起点的时光隧道。从最后一个操作开始往前推，每一步都追问同一个问题：当前位置上的字符，到底是从哪来的？&lt;/p>
&lt;p>如果当前操作是 &lt;code>#&lt;/code>号，说明字符串是左右对称拼接的，长度翻倍前是 m，翻倍后是 2m。k 落在左半边就不用管，落在右半边就映射回左半边的对应位置——&lt;code>k -= m&lt;/code>；&lt;/p>
&lt;p>如果是&lt;code>%&lt;/code>号，说明发生了镜像反转，直接把 k 映射到对称位置——&lt;code>size[i] - 1 - k&lt;/code>；&lt;/p>
&lt;p>如果是&lt;code>*&lt;/code>号，它只删了末尾，对前面的字符位置没影响，直接跳过；&lt;/p>
&lt;p>如果是字母，那就看 &lt;code>k&lt;/code> 是不是刚好指向当前长度的末尾——如果是，恭喜你，抓到本体了，直接返回这个字母。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 边界检查：如果 k 越界，直接返回 &amp;#39;.&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> k &lt;span style="color:#f92672">&amp;gt;=&lt;/span> size[n &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#39;.&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 反向溯源，从后往前剥开几何变换&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(n &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> c &lt;span style="color:#f92672">=&lt;/span> s[i]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> c&lt;span style="color:#f92672">.&lt;/span>isalpha():
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># k 正好指向当前末尾的字母&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> k &lt;span style="color:#f92672">==&lt;/span> size[i] &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> c
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> c &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#39;*&amp;#39;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 退格只删末尾，不影响前面的位置&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> c &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#39;#&amp;#39;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 落在右半边，映射回左半边&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m &lt;span style="color:#f92672">=&lt;/span> size[i] &lt;span style="color:#f92672">//&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> k &lt;span style="color:#f92672">&amp;gt;=&lt;/span> m:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> k &lt;span style="color:#f92672">-=&lt;/span> m
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>: &lt;span style="color:#75715e"># &amp;#39;%&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 镜像映射&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> k &lt;span style="color:#f92672">=&lt;/span> size[i] &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">-&lt;/span> k
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#39;.&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>写完提交，AC 弹出。我靠到椅背上，端起泡面准备嗦一口——面已经坨了，但这不重要——后脑勺又挨了精准的一下。时机还是那么恰到好处，刚好卡在我 AC 之后、吃面之前的那零点五秒。&lt;/p>
&lt;p>“不错，没让我失望。”老勇者的声音从背后传来，带着一丝赞许，“&lt;strong>正向打表记长度，反向溯源追位置&lt;/strong>。时间复杂度 O(n)，空间也控制在了 O(n)。看来昨天的‘空间结构’心法，你已经炼成招式了。”&lt;/p>
&lt;p>”不过——”他话锋一转，点了点屏幕上那个 size 数组，“你为了存这张‘长度变化表’，可是老老实实开了一个长度为 10^5 的数组。虽然没爆内存，但不够优雅。”&lt;/p>
&lt;p>我愣了一下：“不存这个，逆推的时候怎么知道每一步的长度？”&lt;/p>
&lt;p>“你忘了吗？”老勇者嘴角勾起一抹狡黠的笑，“所有的操作都是&lt;strong>可逆&lt;/strong>的。既然你能从前往后算出长度，为什么不能从后往前把长度‘还原’回去？”&lt;/p>
&lt;hr>
&lt;h4 id="去冗存真反向还原">&lt;strong>去冗存真，反向还原&lt;/strong>
&lt;/h4>&lt;p>“反向溯源的时候，真的需要整个 size 数组吗？”&lt;/p>
&lt;p>老勇者拉过椅子坐下，继续说：“你从最后一步开始往前推。最后一步的长度拿到后，你完全可以根据当前操作倒推出前一步的长度——字母就减一，星号就加一，井号就除以二，百分号不变。这样逆着推回去，每一步你都能实时算出前一步的长度。一个 sz 变量就够了，数组是冗余的。”&lt;/p>
&lt;p>我愣了半秒，恍然大悟——既然反向时能自己把长度重新算出来，那正向的数组打表确实可以扔了。&lt;/p>
&lt;p>“正向探路，反向还原。一个变量足矣。”老勇者靠回椅背上，表情仿佛在说“接下来的活你自己干”。&lt;/p>
&lt;p>我坐直身子，立刻动手优化。先正向扫一遍拿到最终长度 sz，然后反向遍历的同时用 sz 实时逆推。井号时看 k 在左半还是右半，同时把 sz 除以二；百分号时做镜像映射，sz 不变；星号时 sz 加一；字母时 sz 减一，如果 k 正好指向末尾就直接返回。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">processStr&lt;/span>(self, s: str, k: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> str:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n &lt;span style="color:#f92672">=&lt;/span> len(s)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 第一步：正向探路，最终长度sz&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sz &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> c &lt;span style="color:#f92672">in&lt;/span> s:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> c &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#39;*&amp;#39;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sz &lt;span style="color:#f92672">=&lt;/span> max(sz &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> c &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#39;#&amp;#39;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sz &lt;span style="color:#f92672">*=&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> c &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;%&amp;#39;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sz &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> k &lt;span style="color:#f92672">&amp;gt;=&lt;/span> sz:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#39;.&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 第二步：反向溯源，同时倒推还原 sz&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> c &lt;span style="color:#f92672">in&lt;/span> reversed(s):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> c &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#39;*&amp;#39;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 还原长度，退格是减一，往前推就加一&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sz &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> c &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#39;#&amp;#39;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 还原长度，除以 2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sz &lt;span style="color:#f92672">//=&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> k &lt;span style="color:#f92672">&amp;gt;=&lt;/span> sz:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> k &lt;span style="color:#f92672">-=&lt;/span> sz
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> c &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#39;%&amp;#39;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 长度不变，反转 k 的位置&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> k &lt;span style="color:#f92672">=&lt;/span> sz &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">-&lt;/span> k
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 还原长度：字母让长度加一，所以往前推就减一&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sz &lt;span style="color:#f92672">-=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> k &lt;span style="color:#f92672">==&lt;/span> sz:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> c
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#39;.&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>提交，再次 AC。代码更短，内存从 O(n) 压到 O(1)，连 size 数组的影都没了。正向的账在反向时一笔一笔倒着算清楚，数组是冗余，变量才是本质。&lt;/p>
&lt;p>老勇者看着屏幕上的绿色大勾，微微点头：“逆推的极致，是把正向的账也一并省掉。能现场反推的，就别提前存。记住了？”&lt;/p>
&lt;hr>
&lt;p>我端起泡面，终于嗦了一大口。面已经坨成了一团红烧牛肉味的线团，但汤汁吸得饱满，每一根面条都透着“ac完题”的满足感——比任何米其林摆盘都香。&lt;/p>
&lt;p>我把两版代码并排夹进魔法书里，在昨天“正序模拟过试炼，空间逆推破天机”的下边又加了几行：&lt;/p>
&lt;p>&lt;strong>正序模拟过试炼，空间逆推破天机。&lt;/strong>
&lt;strong>若问虚空第几字，倒转乾坤寻真身。&lt;/strong>
&lt;strong>长度可逆无需表，去冗存真见精神。&lt;/strong>&lt;/p>
&lt;p>写完最后一个字，我合上魔法书，起身去给那只还在掉毛的鸽子也分了点泡面。这家伙每次送信都预谋要砸我碗里，估计村长那个老抠门，连鸽粮钱都省下来充steam余额了，&lt;/p>
&lt;p>“多吃点，”我掰了半根面条放到它面前，“回去还得飞呢。上次村长偷偷买《艾尔登法环》dlc的时候，我就知道你没好日子过。” 鸽子低头啄了两口面条，抬起头冲我咕咕两声，不知道是感谢还是嫌面太坨，扑棱着翅膀飞走了，临走还不忘在我手背上留下一根灰羽当“回执”。&lt;/p>
&lt;p>我捏着那根羽毛笑了笑，把它夹进魔法书的扉页里。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3612 正序模拟过试炼，空间逆推破天机</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-16/</link><pubDate>Tue, 16 Jun 2026 12:56:34 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-16/</guid><description>&lt;h2 id="-正序模拟过试炼空间逆推破天机">🎮 正序模拟过试炼，空间逆推破天机
&lt;/h2>&lt;p>距离上次在字母大陆抓“双面间谍”已经过去好一阵子了。我本以为那片大陆的服务器早已关服，大家各自安好。没想到今天一大早，我正蹲在火炉边嗦泡面，窗户就被啄得砰砰作响——一只明显没睡醒的鸽子叼着封信，扑腾了两下，精准地一头栽进了我的泡面碗里。&lt;/p>
&lt;p>我捞出那封湿漉漉的羊皮纸信，展开一看，上面赫然是村长那仿佛被狗啃过一样的狂草：“小白！字母大陆更新资料片了！那些字母不知道怎么搞的，混进了一批‘魔法操作符’——星号、井号和百分号。现在整条字符串就像被施了咒一样，你得按照规则从左到右处理每个字符，才能还原出最终的真言！规则如下——”&lt;/p>
&lt;ul>
&lt;li>小写字母：正常添加到结果中；&lt;/li>
&lt;li>&lt;code>'*'&lt;/code>：退格符，删掉结果中的最后一个字母（如果有的话）；&lt;/li>
&lt;li>&lt;code>'#'&lt;/code>：复制符，把当前的结果复制一份，追加到自身后面；&lt;/li>
&lt;li>&lt;code>'%'&lt;/code>：反转符，把当前的结果整个倒过来。&lt;/li>
&lt;/ul>
&lt;p>“这不就是纯纯的&lt;strong>字符串模拟&lt;/strong>吗？”我看着这一大串规则，眉头微挑，“只要建个空列表，按顺序遍历一遍，遇到啥操作就执行啥，最后返回结果不就完事了？”&lt;/p>
&lt;hr>
&lt;h4 id="栈式模拟result就是一个操作栈">栈式模拟：result就是一个操作栈
&lt;/h4>&lt;p>我把那只还在碗里打嗝的鸽子拎出去，抄起键盘开始敲代码：这种从左到右逐字符处理的任务，核心就是一个可变的&lt;code>result&lt;/code>字符串。Python里直接拿&lt;code>list&lt;/code>当栈用——字母来了就&lt;code>append&lt;/code>，星号来了就&lt;code>pop&lt;/code>，井号和百分号稍微复杂点但也都是列表操作，整个流程就是一台简单的状态机。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">applyOperations&lt;/span>(self, s: str) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> str:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> result &lt;span style="color:#f92672">=&lt;/span> []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> ch &lt;span style="color:#f92672">in&lt;/span> s:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> ch &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#39;*&amp;#39;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> result:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> result&lt;span style="color:#f92672">.&lt;/span>pop() &lt;span style="color:#75715e"># 退格&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> ch &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#39;#&amp;#39;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> result &lt;span style="color:#f92672">=&lt;/span> result &lt;span style="color:#f92672">+&lt;/span> result &lt;span style="color:#75715e"># 复制&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> ch &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#39;%&amp;#39;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> result&lt;span style="color:#f92672">.&lt;/span>reverse() &lt;span style="color:#75715e"># 反转&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> result&lt;span style="color:#f92672">.&lt;/span>append(ch) &lt;span style="color:#75715e"># 追加&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#39;&amp;#39;&lt;/span>&lt;span style="color:#f92672">.&lt;/span>join(result)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>写完，提交，AC！我刚惬意地靠到椅背上，后脑勺又&amp;quot;啪&amp;quot;地挨了轻轻一下——力道极其精准，刚好能让我精神一振，又不至于把我拍痛。&lt;/p>
&lt;p>“又来？” 我捂着后脑勺，不用回头就知道是谁。&lt;/p>
&lt;p>“逻辑清晰，没有遗漏，时间复杂度也合格。” 老勇者慢条斯理地说道，“但是小白啊，你有没有想过一个问题？”&lt;/p>
&lt;p>我叹了口气，熟练地摆出虚心求教的姿态：“什么问题？难道我的 &lt;code>if-elif&lt;/code> 分支不够优雅？”&lt;/p>
&lt;p>“&lt;strong>内存问题&lt;/strong>。”老勇者指着屏幕上那行 &lt;code>result = result + result&lt;/code>，“你看这个 &lt;code>#&lt;/code> 操作，如果遇到连续的几个 &lt;code>#&lt;/code>，比如 &lt;code>a##&lt;/code>，你的 &lt;code>result&lt;/code> 会瞬间从长度 1 变成 2，再变成 4。每次翻倍，你都在重新分配一块新内存，把整个 result 从头到尾拷贝一遍。”&lt;/p>
&lt;p>他又指了指数据范围：“现在这题 &lt;code>s.length ≤ 20&lt;/code>，最极端的情况——一个 &lt;code>a&lt;/code> 后面跟 19 个 &lt;code>#&lt;/code> 号。19次翻倍之后，result 最终长度是 2 的 19 次方，大约 52 万个字符。内存占用也就 4 到 5 兆，中间所有拷贝操作加起来大概一百万次元素复制，Python 眨个眼就完事了。“&lt;/p>
&lt;p>老勇者顿了顿，眼神变得深邃起来：“ 你的模拟现在能 AC，不是因为它没问题，是数据量太小，小到把问题捂住了。这就好比你在澡盆里试航一艘纸船——不漏水，不代表它能出海。”&lt;/p>
&lt;p>“如果把 &lt;code>s&lt;/code> 的长度提到十万呢？或者仅仅是 &lt;code>a&lt;/code> 后面跟上 49 个 &lt;code>#&lt;/code> 号——49次翻倍，result 长度变成 2 的 49 次方，” 老勇者瞅了我一眼，“你知道那占多少内存吗？大约 4.5 PB。别说你的电脑，全村锅炉房的魔法硬盘加起来都塞不下。到时候直接一个 &lt;code>MemoryError&lt;/code>，原地炸锅。”&lt;/p>
&lt;p>我盯着代码，倒吸一口凉气。刚才跑样例还觉得模拟挺丝滑，现在一想——那不过是因为策划手下留情，把数据范围压到了 20。如果数据量不加限制，&lt;code>#&lt;/code> 号的指数膨胀就是一颗定时炸弹，只等大数据量的 II 版本一发布就引爆。&lt;/p>
&lt;p>“那 &amp;hellip;&amp;hellip; II 版本怎么办？”我挠着头，刚 AC 的喜悦凉了大半，“如果要返回整个字符串，模拟肯定扛不住啊！”&lt;/p>
&lt;p>“问得好。”老勇者神秘一笑，眼角的皱纹都透着狡黠，“如果 II 版本不问整个字符串，只问你一个问题——最终 result 的第 k 个字符是什么？你还会爆内存吗？”&lt;/p>
&lt;p>我愣住了。只找第 k 个字符？那确实不用把整个 result 算出来，只需要知道&lt;code>k&lt;/code>位置上的字符是谁就行了——就像你不需要把整卷录像带从头放一遍，只需定位到第 k 帧，看一眼那一帧的画面就够了。&lt;/p>
&lt;p>“可是……” 我追问，“不存整个 result，怎么知道第 k 个位置上的字符是从哪来的？”&lt;/p>
&lt;hr>
&lt;h4 id="找第-k-个字符那又是另一个故事了">找第 k 个字符？那又是另一个故事了
&lt;/h4>&lt;p>“换个角度——”&lt;/p>
&lt;p>老勇者拿过笔，在纸上画了一条长横线，然后在线中间画了一道竖杠，把横线一分为二。“你看，每次&lt;code>#&lt;/code>号操作，本质上是把当前字符串一式两份——左边是原来的，右边是复制出来的。&lt;code>%&lt;/code>号呢，是把左边倒过来。&lt;code>*&lt;/code> 号是把最右边那个删掉。” 他把笔尖在那条竖杠上敲了敲，“仔细看，每种操作，都在字符串的空间结构上留下了痕迹。”&lt;/p>
&lt;p>“ 假设你经过 n 次&lt;code>#&lt;/code>操作后，最终字符串的长度膨胀到了 2 的 n 次方。那么第 k 个字符，它一定在这个庞然大物的某一半里。要么在左半边，要么在右半边。如果是右半边，它一定是被某个&lt;code>#&lt;/code>号从左边复制过去的；如果是左半边，它根本没被那次&lt;code>#&lt;/code>号影响。&lt;code>%&lt;/code>号同理——如果一个字符在反转区间里，它的实际来源一定在对称位置。”&lt;/p>
&lt;p>我盯着纸上那条竖杠，脑子里突然闪过一道光：“等等——所以我不需要把整个字符串造出来？只需要拿着 k，从最后一个操作开始往前推，判断它落在当前区间的哪一半，然后反向映射回去？”&lt;/p>
&lt;p>“正是。”老勇者把笔一搁，“每次操作都在改变字符串的几何结构——&lt;code>#&lt;/code> 号是复制对称，&lt;code>%&lt;/code> 号是镜像对称，&lt;code>*&lt;/code> 号是末端删减。你从最终位置出发，一层一层剥开这些操作，就能追溯到最初的来源。”&lt;/p>
&lt;p>他顿了顿，看着我若有所思的表情，嘴角勾起一抹意味深长的笑：“今天先把这个‘空间结构’的直觉消化下。具体怎么从后往前推、怎么判断左半右半、怎么处理反转映射……明天再教你。”&lt;/p>
&lt;p>他站起身朝门口走去，走到门边又停了一下：“心法先入脑，招式才使得出来。”&lt;/p>
&lt;p>门轻轻关上。我重新看向屏幕上那段刚 AC 的模拟代码——刚才还觉得它挺靠谱，现在再看，那行 &lt;code>result = result + result&lt;/code> 简直像在嘲笑我：你能 AC，是因为策划给你放了片海。&lt;/p>
&lt;p>我默念着老勇者的心法：“&lt;code>#&lt;/code>号是复制对称，&lt;code>%&lt;/code>号是镜像对称，&lt;code>*&lt;/code>号是末端删减。找第 k 个字符，就是在一场逆向追踪里，从终点坐标一路剥开层层嵌套的几何变换，精准锁定最初的’本体‘。” 哪天村长真把我扔进 II 版本那 4.5 PB 的虚空里，我还得靠这套“空间逆推”的招式，捞出唯一的真相。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode2095 快慢指针找前驱，虚拟节点消特判</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-15/</link><pubDate>Mon, 15 Jun 2026 12:36:59 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-15/</guid><description>&lt;h2 id="-快慢指针找前驱虚拟节点消特判">🎮 快慢指针找前驱，虚拟节点消特判
&lt;/h2>&lt;p>昨天村长把“对称之盾”爆出来后，乐得像个两百斤的傻子。听说他那 Steam Deck 的风扇嗡嗡转了一宿，隔着半堵墙都能听见装备界面循环播放的BGM。今天一大早，我正蹲在锅炉边添柴，寻思着要不要摸个鱼，院门“砰”地一声又被一脚踹开。&lt;/p>
&lt;p>“小白！出Bug了！”村长冲进来，屏幕上还是昨天那条魔法项链，但这次画风突变——项链正中间的环正在疯狂闪烁刺眼的红光，还伴随着刺耳的警报声。&lt;/p>
&lt;p>“你看！锻造台提示说，项链的能量回路必须重新平衡！”村长把屏幕怼到我脸上，唾沫星子差点喷我眼睛里，“要删掉正中间那个环！只有把它‘优化’掉，整条链子的对称性才能稳定，我的盾牌属性才能生效！”&lt;/p>
&lt;p>我接过电脑，屏幕上那条项链依然排成一条直线，圆环之间用箭头连接。这次的任务不是求什么孪生和了，而是简单粗暴的物理超度——定位正中间的节点，把它从链上摘掉。如果节点数是奇数，中间只有一个；如果是偶数，中间有两个，取靠后的那个，也就是下标为 &lt;code>⌊n/2⌋&lt;/code> 的那个。&lt;/p>
&lt;p>“村长，这不就是经典的&lt;strong>删除链表中间节点&lt;/strong>吗？快慢指针的老本行。”我撸起袖子，“只不过这次不是找中点本身，而是要找它的&lt;strong>前驱节点&lt;/strong>——毕竟单向链表只认 next 不认 prev，要删掉中点，得让前一个节点直接跳过它，指向它的下一个节点，完成物理断联。”&lt;/p>
&lt;h4 id="快慢指针找前驱">快慢指针找前驱
&lt;/h4>&lt;p>我抄起键盘，开始盘逻辑。对付这种单链表找位置的活儿，还得是快慢指针这对老搭档—— &lt;code>fast&lt;/code> 每次走两步，&lt;code>slow&lt;/code> 每次走一步，等 &lt;code>fast&lt;/code> 走到尽头，&lt;code>slow&lt;/code> 刚好站在中间节点上。&lt;/p>
&lt;p>不过这次有个小细节：我们要删的恰好就是中间节点本身。单向链表是个无情的数据结构——每个节点只认 &lt;code>next&lt;/code>，不认 &lt;code>prev&lt;/code>，想删谁，得先找到它的前任。为了让 &lt;code>slow&lt;/code> 最终稳稳停在中间节点的前一个位置，我决定让 &lt;code>fast&lt;/code> 抢跑两步——等 &lt;code>fast&lt;/code> 跑到终点时，&lt;code>slow&lt;/code> 自然就卡在前驱的位置上了。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">deleteMiddle&lt;/span>(self, head: Optional[ListNode]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> Optional[ListNode]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 边界情况：只有一个节点，删掉后链表为空&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">not&lt;/span> head&lt;span style="color:#f92672">.&lt;/span>next:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> slow, fast &lt;span style="color:#f92672">=&lt;/span> head, head
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># fast 先走两步，让 slow 慢一拍，最终 slow 停在中间节点的前驱&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fast &lt;span style="color:#f92672">=&lt;/span> fast&lt;span style="color:#f92672">.&lt;/span>next&lt;span style="color:#f92672">.&lt;/span>next
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> fast &lt;span style="color:#f92672">and&lt;/span> fast&lt;span style="color:#f92672">.&lt;/span>next:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> slow &lt;span style="color:#f92672">=&lt;/span> slow&lt;span style="color:#f92672">.&lt;/span>next
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fast &lt;span style="color:#f92672">=&lt;/span> fast&lt;span style="color:#f92672">.&lt;/span>next&lt;span style="color:#f92672">.&lt;/span>next
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 物理断联：让前驱直接指向中间节点的下一个节点&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> slow&lt;span style="color:#f92672">.&lt;/span>next &lt;span style="color:#f92672">=&lt;/span> slow&lt;span style="color:#f92672">.&lt;/span>next&lt;span style="color:#f92672">.&lt;/span>next
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> head
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>提交，AC。我把屏幕转过去，正准备向村长邀功：“搞定！中间的红环已经被抠掉了！”&lt;/p>
&lt;p>“啪。” 后脑勺突然挨了轻轻一下。老勇者不知何时又出现在我身后，盯着我的代码。&lt;/p>
&lt;p>“快慢指针找前驱，O(N) 时间，O(1) 空间，逻辑没毛病。” 他抿了一口茶，慢条斯理地说道，“但是小白啊，你的边界条件处理得不够优雅。你看，为了处理只有一个节点的情况，你硬生生在函数开头打了一个&lt;code>if&lt;/code>补丁！这在架构设计里叫什么？这叫‘补丁式编程’！万一哪天需求变了，变成删倒数第二个节点，你是不是还得再打一排补丁？”&lt;/p>
&lt;p>我愣在原地：“那……怎么改进？”&lt;/p>
&lt;p>“&lt;strong>虚拟头节点（Dummy Node）&lt;/strong>。” 老勇者微微一笑，“Dummy Node，链表题里的万能站位符。”&lt;/p>
&lt;h4 id="虚拟节点消特判">虚拟节点消特判
&lt;/h4>&lt;p>“在链表最前面加一个哑节点 &lt;code>dummy&lt;/code>，不参与战斗，只负责站位。让 &lt;code>slow&lt;/code> 从 &lt;code>dummy&lt;/code> 出发，&lt;code>fast&lt;/code> 从真正的 &lt;code>head&lt;/code> 出发，同时走。这样一来，&lt;code>slow&lt;/code> 天然比 &lt;code>fast&lt;/code> 慢了一拍——不需要手动抢跑。而且不管要删的是不是真正的头节点，&lt;code>slow&lt;/code> 永远指着被删节点的前驱，连 &lt;code>if not head.next&lt;/code> 的特判都省了。最后返回 &lt;code>dummy.next&lt;/code>，完美。”&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">deleteMiddle&lt;/span>(self, head: Optional[ListNode]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> Optional[ListNode]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 引入虚拟头节点，消灭边界特判&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dummy &lt;span style="color:#f92672">=&lt;/span> ListNode(&lt;span style="color:#ae81ff">0&lt;/span>, head)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># slow从dummy出发，fast从head出发&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> slow, fast &lt;span style="color:#f92672">=&lt;/span> dummy, head
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> fast &lt;span style="color:#f92672">and&lt;/span> fast&lt;span style="color:#f92672">.&lt;/span>next:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> slow &lt;span style="color:#f92672">=&lt;/span> slow&lt;span style="color:#f92672">.&lt;/span>next
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fast &lt;span style="color:#f92672">=&lt;/span> fast&lt;span style="color:#f92672">.&lt;/span>next&lt;span style="color:#f92672">.&lt;/span>next
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> slow&lt;span style="color:#f92672">.&lt;/span>next &lt;span style="color:#f92672">=&lt;/span> slow&lt;span style="color:#f92672">.&lt;/span>next&lt;span style="color:#f92672">.&lt;/span>next
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> dummy&lt;span style="color:#f92672">.&lt;/span>next
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我思路瞬间清爽了，虚拟头结点就像一个站在队伍最前面的替补队员，不参与比赛，但保证排头有人。&lt;code>slow&lt;/code> 和 &lt;code>fast&lt;/code> 从不同起点同时出发，天然拉开了一拍的差距。原本那个让人头疼的‘头节点可能被删’或者‘单节点越界’的边界问题，全部被 &lt;code>dummy&lt;/code> 给消化掉——代码从八行缩到六行，特判抹除，逻辑一马平川。&lt;/p>
&lt;p>“&lt;strong>虚拟头结点是链表题的万能润滑剂&lt;/strong>，” 老勇者拍了拍我的肩膀，“遇到删除操作，先想想能不能用 &lt;code>dummy&lt;/code> 把边界条件统一掉。记住一句话：能少写一行特判，就少埋一个 Bug。”&lt;/p>
&lt;hr>
&lt;h4 id="两套解法记心间村长大坑无尽头">两套解法记心间，村长大坑无尽头
&lt;/h4>&lt;p>村长那边，项链中间那个闪烁红光的环“咔嚓”一声碎裂消失，剩下两半自动接合，整条链子散发出稳定的蓝色光芒。对称之盾的属性面板刷新了——防御力后面多了一行金色小字：“已激活：链路平衡加成”。&lt;/p>
&lt;p>老头激动得抱着机器原地蹦跶，“拿到了！平衡加成！我就知道删掉中间那个环管用！”&lt;/p>
&lt;p>我靠在椅背上，端起茶杯。平心而论，“快指针抢跑 + 前驱定位” 的解法虽然多打了一个 &lt;code>if&lt;/code> 补丁，但逻辑清晰、稳如老狗，每一步都清清楚楚；而老勇者的 Dummy Node 技巧也确实把原本需要特殊处理的边界条件化于无形，代码更短，逻辑更纯粹。我把两种写法并排收进魔法书，记了一笔：快慢指针找前驱，虚拟节点消特判。&lt;/p>
&lt;p>看着村长心满意足地溜达出门的背影，我忍不住在心里嘀咕：村长这条魔法项链的副本连打了两天，每天解锁一个新花样。不知道明天锻造台会不会又整出什么链表操作的新皮肤——也许是指定位置插入？也许是每隔 k 个删一个？照这个趋势下去，迟早有一天他会拿着一条带环的链表跑来问我：“小白啊，你看这链子它怎么自己咬住尾巴了？”&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode2130 快慢指针拆项链，递归回溯借东风</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-14/</link><pubDate>Sun, 14 Jun 2026 12:37:13 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-14/</guid><description>&lt;h2 id="快慢指针拆项链递归回溯借东风">🎮快慢指针拆项链，递归回溯借东风
&lt;/h2>&lt;p>今天我正蹲在锅炉边享受难得的清静，熟悉的Steam Deck风扇声又由远及近地碾了过来。村长捧着电脑冲了进来，屏幕上闪烁着一条由圆环串联而成的项链，每个环上标着数字，还散发着淡蓝色的魔法光芒。&lt;/p>
&lt;p>&amp;ldquo;小白！游戏里掉落了一条神秘的项链！&amp;ldquo;村长的眼睛比项链还亮，&amp;ldquo;你看，这链子一共 &lt;code>n&lt;/code> 个环，&lt;code>n&lt;/code> 还是偶数！游戏提示说蕴含什么对称的力量——从两头往中间数，第 &lt;code>i&lt;/code> 个环和第 &lt;code>n-1-i&lt;/code> 个环叫‘孪生环’！每对孪生环放在一起会产生能量共鸣，共鸣值就是两个环上的数字之和！需要找出最强共鸣值提交给锻造台，才能解锁传说中的对称之盾！&amp;rdquo;&lt;/p>
&lt;p>我接过电脑，屏幕上那条项链排成一条直线，圆环之间用箭头连接——这不就是链表吗？偶数个节点，首尾对称配对，求最大孪生和。“村长，您直接把它塞进一个数组里，然后用个 &lt;code>for&lt;/code> 循环，左边一个指针右边一个指针往中间夹击不就行了？”&lt;/p>
&lt;p>村长一听，胡子都翘起来了：“糊涂！数组是什么？那是连续内存！这链子这么长，万一中间断了，你还得重新分配一大块连续的内存空间来存它！我们村的魔法锅炉用的就是碎片化内存管理，最忌讳动不动就开大数组！最好是 O(1) 空间复杂度，原地解决！”&lt;/p>
&lt;p>我叹了口气，行吧，那只能对链表本身进行物理改造了。&lt;/p>
&lt;hr>
&lt;h4 id="斩链刀反转术">斩链刀·反转术
&lt;/h4>&lt;p>梳理下思路：快慢指针找中点，反转后半段，然后前后同时遍历配对——正好是 O(n) 时间加 O(1) 空间。&lt;/p>
&lt;p>我抄起键盘开干：快慢指针是老搭档了，&lt;code>fast&lt;/code> 每次走两步，&lt;code>slow&lt;/code> 每次走一步，当 &lt;code>fast&lt;/code> 跑到终点时，&lt;code>slow&lt;/code> 正好卡在中点。然后把后半段链表反转，让尾节点变成新头。最后前半段从原头出发，后半段从新头出发，一边走一边求和，&lt;code>max&lt;/code> 一路追最大值。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Solution&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">middleNode&lt;/span>(self, head: Optional[ListNode]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> Optional[ListNode]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> slow &lt;span style="color:#f92672">=&lt;/span> fast &lt;span style="color:#f92672">=&lt;/span> head
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> fast &lt;span style="color:#f92672">and&lt;/span> fast&lt;span style="color:#f92672">.&lt;/span>next:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> slow &lt;span style="color:#f92672">=&lt;/span> slow&lt;span style="color:#f92672">.&lt;/span>next
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fast &lt;span style="color:#f92672">=&lt;/span> fast&lt;span style="color:#f92672">.&lt;/span>next&lt;span style="color:#f92672">.&lt;/span>next
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> slow
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">reverseList&lt;/span>(self, head: Optional[ListNode]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> Optional[ListNode]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pre, cur &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span>, head
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> cur:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> nxt &lt;span style="color:#f92672">=&lt;/span> cur&lt;span style="color:#f92672">.&lt;/span>next
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cur&lt;span style="color:#f92672">.&lt;/span>next &lt;span style="color:#f92672">=&lt;/span> pre
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pre &lt;span style="color:#f92672">=&lt;/span> cur
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cur &lt;span style="color:#f92672">=&lt;/span> nxt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> pre
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">pairSum&lt;/span>(self, head: Optional[ListNode]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mid_node &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>middleNode(head)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> second_head &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>reverseList(mid_node)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> second_head:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> max(ans, head&lt;span style="color:#f92672">.&lt;/span>val &lt;span style="color:#f92672">+&lt;/span> second_head&lt;span style="color:#f92672">.&lt;/span>val)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> head &lt;span style="color:#f92672">=&lt;/span> head&lt;span style="color:#f92672">.&lt;/span>next
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> second_head &lt;span style="color:#f92672">=&lt;/span> second_head&lt;span style="color:#f92672">.&lt;/span>next
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>提交，AC。“村长，这什么孪生环啊，分明是把链表反转加双指针的经典面试题穿上了魔法项链的皮肤！连‘对称之盾’这种道具名都起得跟算法术语似的。”我一边吐槽，一边我把计算出的最大孪生和报给村长：“是 10086！”&lt;/p>
&lt;p>村长激动地一拍大腿：“好！我就知道这条项链大有玄机！你听听，10086——这数字多耳熟，我上个月充话费打的就是这个号！我得赶紧提交共鸣值，对称之盾在锻造台上已经冒金光了！”说罢，他便捧着电脑倒腾了起来。&lt;/p>
&lt;hr>
&lt;h4 id="回溯剑递归道">回溯剑·递归道
&lt;/h4>&lt;p>“快慢指针加反转链表，O(n)时间，O(1)空间，写得不错。” 熟悉的声音从身后传来。&lt;/p>
&lt;p>老勇者不知何时倚在了门框上，手里端着一杯枸杞茶，瞅着屏幕。他接着抿了一口茶，“但你有没有想过，你为了算个最大值，把人家好好的链表给拆了？这叫破坏性读取，懂不懂？”&lt;/p>
&lt;p>我愣在原地：“可是村长说要 O(1) 空间……”&lt;/p>
&lt;p>“O(1) 空间只是手段，不是目的。”老勇者放下茶杯，指尖在键盘上飞舞：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Solution&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">pairSum&lt;/span>(self, head: Optional[ListNode]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> left &lt;span style="color:#f92672">=&lt;/span> head
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">dfs&lt;/span>(node):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> node:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 先递归到底，node走到链表尾部&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dfs(node&lt;span style="color:#f92672">.&lt;/span>next)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 回溯时，node 从尾向头走，left 从头向尾走，两者天然形成孪生配对&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">nonlocal&lt;/span> left, ans
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> max(ans, left&lt;span style="color:#f92672">.&lt;/span>val &lt;span style="color:#f92672">+&lt;/span> node&lt;span style="color:#f92672">.&lt;/span>val)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> left &lt;span style="color:#f92672">=&lt;/span> left&lt;span style="color:#f92672">.&lt;/span>next
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dfs(head)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我盯着那段代码，瞳孔地震：“等等……这是……”&lt;/p>
&lt;p>“没错。”老勇者微微一笑，“&lt;strong>递归回溯&lt;/strong>。函数调用栈本身就是一条隐形的链表。利用调用栈天然的 LIFO 特性，当你一路递归到底再回溯上来时，&lt;code>node&lt;/code> 是从尾部往头部走的，而我的 &lt;code>left&lt;/code> 是从头部往尾部走的。两者在回溯过程中自然相遇——这不就是最完美的孪生配对吗？”&lt;/p>
&lt;p>他顿了顿，悠悠地吐出一句：“不需要找中点，不需要反转链表，不需要手动维护三个指针。代码只有十几行，逻辑优雅得像首诗。”&lt;/p>
&lt;hr>
&lt;h4 id="确定vs优雅之辩">确定vs优雅之辩
&lt;/h4>&lt;p>村长凑过来，盯着老勇者的那段代码，眉头拧成了麻花：“递归？链表用递归？不怕爆栈？你这 &lt;code>dfs(node.next)&lt;/code> 一层套一层，万一这链子有十万个节点，一半也有五万层，到时候一个 &lt;code>StackOverflowError&lt;/code>，连个报错信息都来不及打就炸了！这叫优雅？这这叫给评测机递刀子，给生产环境埋炸弹！“&lt;/p>
&lt;p>他气的胡子一翘一翘的：“我告诉你，真正的工程化思维是什么？是&lt;strong>确定性&lt;/strong>！是能跑在生产环境里不炸的稳定性！不是让你在这儿写什么‘回溯的诗’！诗能当饭吃吗？诗能让服务器不宕机吗？不能！”&lt;/p>
&lt;p>面对村长唾沫横飞的“工程化”说教，老勇者连眼皮都没抬一下。他轻轻抿了口茶：“老村长啊，你只盯着爆栈的定时炸弹，却没看见迭代反转的副作用。小白那套快慢指针加反转链表的做法，原地把人家后半段链表的 &lt;code>next&lt;/code> 全拧反了。这在算法设计里叫什么？这叫‘破坏性副作用’！“&lt;/p>
&lt;p>老勇者的声音不大，却字字诛心：”如果在生产环境里，另一个线程正在遍历这条链表——比如统计它的长度——小白这一刀切下去，那个线程当场就得给你抛出一个 &lt;code>NullPointerException&lt;/code>。递归虽然费栈，但它不修改原数据，是纯函数式的读操作，没有破坏性副作用。你管这叫定时炸弹，我倒觉得这是读写分离的架构美学。”&lt;/p>
&lt;p>村长一听，胡子又翘了起来：“你跟我谈架构美学？我告诉你，在服务器机房里，内存就是命！你那一层层递归调用栈——五万层栈帧，每帧存局部变量和返回地址，空间开销是O(n)，比小白那O(1)的迭代反转多了一整个数量级！题目数据量小的时候你还能装优雅，数据量一上来，操作系统直接甩个 &lt;code>SIGSEGV&lt;/code> 送你回老家！”&lt;/p>
&lt;p>老勇者也不恼，只是慢条斯理地吹了吹茶杯里的热气：“迭代反转O(1)额外空间不假，但那是在原地修改的前提下——它把原链表拆了重装。如果这道题要求保留原链表不变，你那一套就得先拷贝再反转，空间一样O(n)。递归的回溯本质上是在调用栈上隐式地维护了一个‘从尾到头’的遍历顺序，不需要显式修改任何指针。你说它费栈，我说它省心。哪套方案更优，取决于约束条件，不是一棍子打死的事。”&lt;/p>
&lt;p>两人你一言我一语，一个坚持“爆栈风险不可控”，一个反驳“并发环境下数据完整性更重要”，唾沫星子横飞，简直比代码评审会上架构师和SRE吵架还激烈。&lt;/p>
&lt;p>我呆呆地坐在角落里，看着这两位大佬争得面红耳赤。半晌，我弱弱地举起手，试图打破这剑拔弩张的气氛：“那个……两位前辈……”&lt;/p>
&lt;p>两人同时转过头：“说！！”&lt;/p>
&lt;p>我咽了口唾沫，小声说道：“其实……这题如果用 Python 写的话，直接把链表转成列表，然后双指针从两头往中间夹击求最大值……不仅不用反转链表，也不用递归爆栈，而且运行速度比你们俩都快……”&lt;/p>
&lt;p>空气突然安静了。&lt;/p>
&lt;p>村长和老勇者对视了一眼，同时冷哼一声，异口同声地骂道：“闭嘴！！”&lt;/p>
&lt;p>说完，两人一甩袖子，各自背着手气呼呼地走了出去，只留我一个人在风中凌乱。&lt;/p>
&lt;p>所以……我这算不算是凭一己之力，终结了一场史诗级的技术辩论？&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3838 绝对防御！手动Hash与模运算的土豆级零信任协议</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-13/</link><pubDate>Sat, 13 Jun 2026 22:13:24 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-13/</guid><description>&lt;h2 id="绝对防御手动hash与模运算的土豆级零信任协议">🎮绝对防御！手动Hash与模运算的土豆级零信任协议
&lt;/h2>&lt;p>今天一大早，村长神神秘秘地塞给我一本皱巴巴的手册，封面上歪歪扭扭写着《v2.6.13版字母权重表》，旁边还盖了个萝卜章。手册里夹着一张纸条，上面是几串密文单词，每个单词旁边画着鬼画符一样的标记。&lt;/p>
&lt;p>“小白，”村长压低声音，眼神里透着三分警惕七分得意，“我最近搞了套加密算法，能把所有任务指令都编码成奇怪的字符串。你看这张表，a对应几斤几两，b对应几斤几两……全是我亲手标的。纸条上那几串密文，你给我实时翻译下看看。”&lt;/p>
&lt;p>我接过那张手绘的权重表，嘴角抽搐：“村长，这都2026年了，您还在用手动哈希？连个JWT都不用？咱村不是刚通了5G吗，隔壁勇者团都用上OAuth2.0了。”&lt;/p>
&lt;p>村长捋了捋胡子，一脸高深莫测：“年轻人，这叫复古硬核加密。万一哪天基站被炸了，云计算挂了，咱村的秘密指令还得靠这张纸传。再说了，我这算法密钥只有咱俩知道——绝无第三方持有，零信任架构懂不懂？少废话，赶紧算！”&lt;/p>
&lt;p>我翻开手册，开头就写了三条让人血压飙升的规则：&lt;/p>
&lt;p>第一条，每个单词的“密码值”= 单词里每个字母在权重表中对应值的总和。&lt;/p>
&lt;p>我心中暗想，说得跟什么上古魔法似的，小学二年级的加法运算也敢叫加密。瞥了眼村长，他正自顾自地捋着胡子，仿佛这真的是什么惊天动地的黑科技。&lt;/p>
&lt;p>第二条，把总和对 26 取模，得到一个 0~25 的数字。&lt;/p>
&lt;p>我叹了口气，经典 &lt;code>% 26&lt;/code>，取模压缩。村长您是不是觉得26个字母刚好能塞进一个byte的低5位？内存优化强迫症是吧？&lt;/p>
&lt;p>第三条，倒序映射： 0 → &amp;lsquo;z&amp;rsquo;, 1 → &amp;lsquo;y&amp;rsquo;, &amp;hellip;, 25 → &amp;lsquo;a&amp;rsquo;。&lt;/p>
&lt;p>我盯着这行规则看了足足五秒钟，终于绷不住了：&amp;ldquo;等等！正常人不都是 0→&amp;lsquo;a&amp;rsquo; 吗？为什么这里是反的？村长你是不是把ASCII表贴反了？还是说这是为了防止我们用 &lt;code>char + 'a'&lt;/code> 一行搞定而故意加的debuff？！&amp;rdquo;&lt;/p>
&lt;p>村长在旁边嘿嘿一笑：&amp;ldquo;哦，那个啊……因为我觉得从z开始数比较有仪式感。你想啊，z是字母表的终点，从终点往起点映射，有一种‘落叶归根’的哲学意味。怎么，有意见？&amp;rdquo;&lt;/p>
&lt;p>我深吸一口气，把到嘴边的吐槽硬生生咽了回去：&amp;ldquo;没意见，您说了算。我写还不行吗。&amp;rdquo;&lt;/p>
&lt;p>既然规则已经明确到这种程度，那还有什么好犹豫的？直接模拟就完事了！&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Solution&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">mapWordWeight&lt;/span>(self, words: List[str], weights: List[int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> str:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ord_a &lt;span style="color:#f92672">=&lt;/span> ord(&lt;span style="color:#e6db74">&amp;#39;a&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ord_z &lt;span style="color:#f92672">=&lt;/span> ord(&lt;span style="color:#e6db74">&amp;#39;z&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> word &lt;span style="color:#f92672">in&lt;/span> words:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> weight &lt;span style="color:#f92672">=&lt;/span> sum(weights[ord(c) &lt;span style="color:#f92672">-&lt;/span> ord_a] &lt;span style="color:#66d9ef">for&lt;/span> c &lt;span style="color:#f92672">in&lt;/span> word) &lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#ae81ff">26&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans&lt;span style="color:#f92672">.&lt;/span>append(chr(ord_z &lt;span style="color:#f92672">-&lt;/span> weight))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#39;&amp;#39;&lt;/span>&lt;span style="color:#f92672">.&lt;/span>join(ans)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>提交，AC，一气呵成。我把翻译出来的指令条递给村长——上面赫然写着：&amp;ldquo;下午三点村口开会讨论秋季土豆种植计划。&amp;rdquo;&lt;/p>
&lt;p>村长接过指令条，满意地点点头：“好！解密成功！这套算法果然只有你能驾驭！” 说完，他背着手，迈着那种“绝世高手下山”的六亲不认步伐，慢悠悠地踱出了院子，甚至还哼起了不知名的小曲儿。&lt;/p>
&lt;p>我呆立在原地，瞅着村长那逐渐远去的背影，感觉自己的CPU温度正在飙升。这老头……搞了权重累加、取模压缩、倒序映射三层加密，连‘落叶归根’的哲学都用上了，就为了保护土豆种植计划？！他到底在防谁啊？防隔壁村的猪吗？！再说了，谁家好人会把最高机密写在一本皱巴巴的手册上啊？要是来场暴雨，或者被哪只不长眼的乌鸦当成筑巢材料叼走，咱村的零信任架构直接物理层面崩了好吗！&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3559 二进制传送门觉醒：当每个节点都能瞬间跳跃</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-12/</link><pubDate>Fri, 12 Jun 2026 15:25:32 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-12/</guid><description>&lt;h2 id="二进制传送门觉醒当每个节点都能瞬间跳跃">🎮二进制传送门觉醒：当每个节点都能瞬间跳跃
&lt;/h2>&lt;p>今天一大早，我刚推开村口锅炉房的门，就看见村长蹲在那儿，顶着两个黑眼圈，怀里抱着笔记本电脑，屏幕上&amp;rsquo;&amp;lsquo;奇数流宗师&amp;rsquo;&amp;lsquo;几个烫金大字一闪一闪的，后面跟着密密麻麻的查询列表，滚动条缩成了一个小点。墨菲定律诚不欺我——怕什么来什么。&lt;/p>
&lt;p>&amp;lsquo;&amp;lsquo;小白！&amp;lsquo;&amp;lsquo;村长看到我，像装了雷达一样从地上弹起来，&amp;lsquo;&amp;lsquo;这个成就太难了！你看，宗师成就要求处理任意两个节点之间的路径查询，不是只有根到叶子那么简单！&amp;lsquo;&amp;lsquo;他疯狂划拉屏幕，&amp;lsquo;&amp;lsquo;查询可能是从1号到5号，从3号到7号，从8号到2号……我把昨天你那套压扁成链的代码套进去，跑的我风扇狂转，Steam Deck差点起飞——结果还没跑完！&amp;rsquo;&amp;rsquo;&lt;/p>
&lt;p>我心里一沉。昨天那是从根到最深叶子，路径只有一条，深度k固定，一行快速幂秒杀。今天是&lt;strong>任意两点之间的路径&lt;/strong>——每条查询的起点终点都不一样，路径长度k各不相同。昨天的对称性trick倒是还能用——路径上有k条边，方案数还是2^{k-1}。但问题变成了：&lt;strong>怎么快速算出任意两点间的路径长度&lt;/strong>？&lt;/p>
&lt;p>我在草稿纸上开始画树。假设查询节点4和节点5，路径是4→3→5，长度是2，查询节点2和节点5，路径是2→1→3→5，长度是3。关键藏在这些路径的交汇处——它们不是一条直线冲到底的，而是先往上爬到某个公共祖先，再拐弯往下。那个&amp;rsquo;&amp;lsquo;分叉点&amp;rsquo;&amp;lsquo;就是&lt;strong>最近公共祖先，LCA&lt;/strong>。那么，对于每个查询(u, v)，路径长度 &lt;code>k = depth[u] + depth[v] - 2*depth[LCA]&lt;/code>。有了k，对称性trick照旧——方案数还是2^{k-1}。&lt;/p>
&lt;p>关键在于，&lt;strong>怎么快速找到LCA&lt;/strong>？&lt;/p>
&lt;hr>
&lt;h4 id="遇事不决先暴力">遇事不决先暴力？
&lt;/h4>&lt;p>既然LCA的定义是两个节点往上爬、第一个相遇的祖先，那最朴素的做法就是模拟这个爬树过程：从u一路爬到根，把沿途所有节点记下来；再从v一路爬到根，看哪个节点最先出现在u的路径里。&lt;/p>
&lt;p>对于查询(u, v)，分别爬树记录路径，暴力比对找第一个共同节点： &lt;code>find_path(node)&lt;/code> 函数记录从node到根节点的路径，&lt;code>find_lca_brute_force(u, v)&lt;/code>函数暴力找u和v的最近公共祖先(LCA)。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 暴力LCA解法：通过爬树找最近公共祖先 &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">assignEdgeWeights&lt;/span>(edges: List[List[int]], queries: List[List[int]]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> List[int]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MOD &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1_000_000_007&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n &lt;span style="color:#f92672">=&lt;/span> len(edges) &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g &lt;span style="color:#f92672">=&lt;/span> [[] &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> x, y &lt;span style="color:#f92672">in&lt;/span> edges:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g[x]&lt;span style="color:#f92672">.&lt;/span>append(y)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g[y]&lt;span style="color:#f92672">.&lt;/span>append(x)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 预处理父节点和深度&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parent &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (n &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#75715e"># parent[i]表示节点i的父节点&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> depth &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> (n &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#75715e"># depth[i]表示节点i的深度 &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">dfs&lt;/span>(x: int, fa: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parent[x] &lt;span style="color:#f92672">=&lt;/span> fa
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> y &lt;span style="color:#f92672">in&lt;/span> g[x]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> y &lt;span style="color:#f92672">!=&lt;/span> fa: &lt;span style="color:#75715e"># 避免回溯到父节点&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> depth[y] &lt;span style="color:#f92672">=&lt;/span> depth[x] &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dfs(y, x)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dfs(&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>) &lt;span style="color:#75715e"># 从根节点1开始DFS，根节点的父节点设为0（虚拟节点）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 记录从node到根节点的路径列表（包含node和根节点）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">find_path_to_root&lt;/span>(node: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> list:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> path &lt;span style="color:#f92672">=&lt;/span> []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> current &lt;span style="color:#f92672">=&lt;/span> node
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> current &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>: &lt;span style="color:#75715e"># 一直爬到根节点的父节点（虚拟节点0）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> path&lt;span style="color:#f92672">.&lt;/span>append(current)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> current &lt;span style="color:#f92672">=&lt;/span> parent[current]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> path
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 暴力找u和v的最近公共祖先(LCA)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">find_lca_brute_force&lt;/span>(u: int, v: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> path_u &lt;span style="color:#f92672">=&lt;/span> find_path_to_root(u)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> path_v &lt;span style="color:#f92672">=&lt;/span> find_path_to_root(v)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> path_u_set &lt;span style="color:#f92672">=&lt;/span> set(path_u) &lt;span style="color:#75715e"># 将path_u转为集合，方便快速查找 &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 从v的路径往上找，第一个在u路径中的节点就是LCA&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> node &lt;span style="color:#f92672">in&lt;/span> path_v:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> node &lt;span style="color:#f92672">in&lt;/span> path_u_set:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> node
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 理论上不会到这里，因为根节点一定是公共祖先&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 计算u到v的路径长度k = depth[u] + depth[v] - 2*depth[LCA]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">get_distance&lt;/span>(u: int, v: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> lca &lt;span style="color:#f92672">=&lt;/span> find_lca_brute_force(u, v)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> depth[u] &lt;span style="color:#f92672">+&lt;/span> depth[v] &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">*&lt;/span> depth[lca]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 计算k条边的赋值方案中，使总和为奇数的方案数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">count_odd_ways&lt;/span>(k: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> pow(&lt;span style="color:#ae81ff">2&lt;/span>, k &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, MOD) &lt;span style="color:#66d9ef">if&lt;/span> k &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 处理每个查询&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> answer &lt;span style="color:#f92672">=&lt;/span> []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> x, y &lt;span style="color:#f92672">in&lt;/span> queries:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> x &lt;span style="color:#f92672">==&lt;/span> y: &lt;span style="color:#75715e"># 同一点，路径长度为0，方案数为0 &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> answer&lt;span style="color:#f92672">.&lt;/span>append(&lt;span style="color:#ae81ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 计算路径长度&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> path_length &lt;span style="color:#f92672">=&lt;/span> get_distance(x, y)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 计算方案数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> answer&lt;span style="color:#f92672">.&lt;/span>append(count_odd_ways(path_length))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> answer
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我默默算了笔账：查询数量十万，每次暴力找LCA——&lt;code>find_path&lt;/code> 爬树最坏O(n)，&lt;code>node in path_v&lt;/code> 又是O(n)，总复杂度O(q*n)，十万乘十万，评测机不弹TLE才怪。&lt;/p>
&lt;p>正一筹莫展，后脑勺挨了轻轻一下——力道依旧精准。老勇者的声音从背后传来：&amp;ldquo;又在暴力枚举？&amp;rdquo;&lt;/p>
&lt;p>我捂着后脑勺，感觉救星到了。老勇者瞄了一眼屏幕上的查询列表，又看了一眼我的那段爬树代码，叹了口气：&amp;ldquo;你啊，总是用O(n)的方法硬刚O(log n)能解决的问题。告诉我，你知道&lt;strong>倍增法求LCA&lt;/strong>吗？&amp;rdquo;&lt;/p>
&lt;p>我心虚地缩了缩脖子：&amp;ldquo;听说过，Binary Lifting&amp;hellip;&amp;hellip;但一直没学，总觉得二进制跳来跳去的有点玄乎……&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;&lt;strong>二进制提升，求LCA的神器&lt;/strong>。&amp;ldquo;老勇者打了个响指，&amp;ldquo;准备让暴力爬树法退役吧，你该学点真正的树上跳跃姿势了！&amp;rdquo;&lt;/p>
&lt;hr>
&lt;h4 id="当lca遇上二进制提升">当LCA遇上二进制提升！
&lt;/h4>&lt;p>他在纸上画了一棵枝繁叶茂的大树，并在旁边写下几个大字——&amp;rsquo;&amp;rsquo;&lt;strong>预处理 + 快速查询 = 降维打击&lt;/strong>&amp;rsquo;&amp;rsquo;。&lt;/p>
&lt;p>&amp;ldquo;暴力法的痛点在哪？&amp;ldquo;老勇者指向树上的一个节点，&amp;ldquo;每次查询都要从节点一步步往上爬，像蜗牛一样。如果树退化成一条链，爬一次就是O(n)。十万次查询就是O(nq)，评测机不TLE才是见鬼了。&amp;rdquo;&lt;/p>
&lt;p>&amp;lsquo;&amp;lsquo;不一步一步爬？&amp;lsquo;&amp;lsquo;我感到匪夷所思。毕竟在我的认知里，树只能沿着边往上走，要么dfs，要么bfs——就像RPG里爬塔，每层楼梯都得踩一遍，怎么可能跳过中间楼层直接到顶层？&lt;/p>
&lt;p>&amp;lsquo;&amp;rsquo;&lt;strong>预处理&lt;/strong>。&amp;lsquo;&amp;lsquo;老勇者吐出三个字，&amp;lsquo;&amp;lsquo;提前给每个节点装上 &amp;lsquo;快捷传送门&amp;rsquo;——不只存它的父节点，而是存它的第1级祖先、第2级祖先、第4级祖先、第8级祖先……直到第2^16级祖先。这样查询的时候，你就可以&amp;rsquo;跳着走&amp;rsquo;，而不是&amp;rsquo;爬着走&amp;rsquo;。这就是&lt;strong>二进制提升&lt;/strong>的名字由来——用2的幂次来加速跳跃。&amp;rdquo;&lt;/p>
&lt;p>我挠头：&amp;lsquo;&amp;lsquo;为什么偏偏是2的幂次？&amp;rsquo;&amp;rsquo;&lt;/p>
&lt;p>&amp;lsquo;&amp;lsquo;因为任何整数都可以拆成二进制。&amp;lsquo;&amp;lsquo;老勇者瞅了我一眼，&amp;lsquo;&amp;lsquo;比如13 = 8 + 4 + 1 = 2^3 + 2^2 + 2^0。如果你要往上跳13步，不用一步一步爬，只需要跳三次：先跳8步，再跳4步，最后跳1步。每次跳跃都是O(1)，总共O(log n)。从蜗牛进化成袋鼠，靠的就是二进制拆分。&amp;rsquo;&amp;rsquo;&lt;/p>
&lt;p>他在纸上写下了核心定义：&lt;code>pa[x][i] = x的第2^i级祖先&lt;/code>，也就是节点x向上跳2^i步到达的祖先。&lt;/p>
&lt;p>&amp;lsquo;&amp;lsquo;哦——&amp;rsquo;&amp;rsquo; 我若有所思，&amp;lsquo;&amp;lsquo;提前处理好节点的2的幂次级祖先们？可是这要怎么算？每个节点都跳一遍不还是炸？”&lt;/p>
&lt;p>&amp;lsquo;&amp;rsquo;&lt;strong>动态规划&lt;/strong>。&amp;lsquo;&amp;lsquo;老勇者写下状态转移方程：&lt;code>pa[x][i+1] = pa[pa[x][i]][i]&lt;/code>，&amp;rsquo;&amp;rsquo; x的第2^{i+1}级祖先，等于x的第2^i级祖先再往上跳2^i步。&amp;rsquo;&amp;rsquo;&lt;/p>
&lt;p>我感觉脑子有点转不过来：&amp;lsquo;&amp;lsquo;这不会跳错？套娃套得我头晕。&amp;rsquo;&amp;rsquo;&lt;/p>
&lt;p>&amp;lsquo;&amp;lsquo;不会。&amp;lsquo;&amp;lsquo;老勇者画了个示意图，&amp;lsquo;&amp;lsquo;假设x的第2级祖先是a，a的第2级祖先是b。x到a是2步，a到b也是2步，那x到b就是4步——所以b就是x的第4级祖先。这个递推的精髓在于：用已知的短距离跳跃，组合出未知的长距离跳跃。2^i级的祖先你已经在上一轮算好了，拿它再跳一次2^i，就是2^{i+1}级。倍增就是这么来的——每一步都建立在之前所有步的基础上，像盖楼一样一层一层往上搭。&amp;rsquo;&amp;rsquo;&lt;/p>
&lt;p>&amp;ldquo;DFS的时候，一边往下走一边顺手填。走到一个节点时，它的所有2的幂次祖先都已经在上层被算好了，你只需要用父节点的数据往上递推——&lt;code>pa[x][0]&lt;/code>是父节点，&lt;code>pa[x][1]&lt;/code>是爷爷，&lt;code>pa[x][2]&lt;/code>是爷爷的爷爷，以此类推。一棵树遍历完，&lt;strong>每个节点都自带一套二进制传送门&lt;/strong>，以后找LCA的时候，你就不需要再爬树了，直接让节点按二进制拆分往上层跳就行。&amp;rsquo;&amp;rsquo;&lt;/p>
&lt;p>老勇者接着写下了完整的预处理代码：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">LcaBinaryLifting&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> __init__(self, edges: List[List[int]]):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n &lt;span style="color:#f92672">=&lt;/span> len(edges) &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m &lt;span style="color:#f92672">=&lt;/span> n&lt;span style="color:#f92672">.&lt;/span>bit_length() &lt;span style="color:#75715e"># 计算需要多少层：log2(n)向上取整&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g &lt;span style="color:#f92672">=&lt;/span> [[] &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> x, y &lt;span style="color:#f92672">in&lt;/span> edges:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> x &lt;span style="color:#f92672">-=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 节点编号从0开始（内部处理更方便）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> y &lt;span style="color:#f92672">-=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g[x]&lt;span style="color:#f92672">.&lt;/span>append(y)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g[y]&lt;span style="color:#f92672">.&lt;/span>append(x)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># depth[i]: 节点i的深度（根节点深度为0）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> depth &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> n
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># pa[i][j]: 节点i的第2^j级祖先（-1表示不存在）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pa &lt;span style="color:#f92672">=&lt;/span> [[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> m &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">dfs&lt;/span>(x: int, fa: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pa[x][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> fa &lt;span style="color:#75715e"># 第2^0=1级祖先就是父节点&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> y &lt;span style="color:#f92672">in&lt;/span> g[x]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> y &lt;span style="color:#f92672">!=&lt;/span> fa:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> depth[y] &lt;span style="color:#f92672">=&lt;/span> depth[x] &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dfs(y, x)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dfs(&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#75715e"># 从根节点0开始DFS，根节点的父节点设为-1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 动态规划计算更高级的祖先，状态转移 pa[x][i+1] = pa[pa[x][i]][i]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(m &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> x &lt;span style="color:#f92672">in&lt;/span> range(n):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> (p &lt;span style="color:#f92672">:=&lt;/span> pa[x][i]) &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>: &lt;span style="color:#75715e"># 如果2^i级祖先存在&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pa[x][i &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> pa[p][i] &lt;span style="color:#75715e"># 计算2^(i+1)级祖先&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>depth &lt;span style="color:#f92672">=&lt;/span> depth
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>pa &lt;span style="color:#f92672">=&lt;/span> pa
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&amp;ldquo;你看，&amp;ldquo;老勇者指着代码，&amp;ldquo;第一步，建图。第二步，DFS预处理每个节点的深度和直接父节点（pa[x] [0]）。第三步，动态规划计算更高级的祖先。外层循环 log n次，内层循环n次，总共n log n。&amp;rdquo;&lt;/p>
&lt;p>他打了个比方：&amp;ldquo;预处理一次性完成，之后每次查询都能直接享受红利——就好比副本开荒前先把全图传送点踩一遍，开荒的时候直接飞过去，不用每次都从新手村跑图。&amp;rdquo;&lt;/p>
&lt;p>我盯着代码看了一会，一拍大腿：&amp;ldquo;也就是说，预处理好了以后，找LCA就不需要一步一步爬树了，而是&lt;strong>二进制跳跃&lt;/strong>！因为LCA肯定是两个节点都能跳到的地方——先让深度大的那个跳到和另一个同一高度，然后两个一起往上蹦，只要跳完之后两人不在同一个节点，就说明还没跳到公共祖先，继续往上跳！&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;是这个道理。&amp;rdquo; 老勇者竖起四根手指，&amp;ldquo;听好了，找LCA分四步——&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;第一步，&lt;strong>对齐深度&lt;/strong>。确保x在y上方（depth[x] ≤ depth[y]），如果x更深就交换。然后把y往上跳，跳到和x同一深度。怎么跳？把深度差拆成二进制，按位跳——差13层就先跳8层再跳4层再跳1层。&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;第二步，&lt;strong>来个判断&lt;/strong>——如果跳完之后y == x，说明x本身就是y的祖先，直接返回x。这种情况比如x是y的爷爷，把y提到x的高度之后，发现两人站在同一个节点上，那LCA就是x。&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;第三步，&lt;strong>同时倍增跳跃&lt;/strong>。让x和y从大到小尝试往上跳——从最大的步长（比如2^16）开始试。如果跳完之后x和y的祖先还不一样，说明LCA还在上面，那就跳；如果一样了，说明已经跳到LCA或LCA以上了，就不跳。循环走完，x和y会停在LCA的紧下方——也就是LCA的下一层。&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;第四步，&lt;strong>返回它们的父节点&lt;code>pa[x][0]&lt;/code>，就是LCA&lt;/strong>。&amp;rdquo;&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">LcaBinaryLifting&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> __init__(self, edges: List[List[int]]):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">......&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>depth &lt;span style="color:#f92672">=&lt;/span> depth
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>pa &lt;span style="color:#f92672">=&lt;/span> pa
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">get_kth_ancestor&lt;/span>(self, node: int, k: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(k&lt;span style="color:#f92672">.&lt;/span>bit_length()): &lt;span style="color:#75715e"># 遍历k的每一位二进制位&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> k &lt;span style="color:#f92672">&amp;gt;&amp;gt;&lt;/span> i &lt;span style="color:#f92672">&amp;amp;&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>: &lt;span style="color:#75715e"># k 二进制从低到高第 i 位是 1；&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> node &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>pa[node][i] &lt;span style="color:#75715e"># 往上跳2^i步&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> node
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">get_lca&lt;/span>(self, x: int, y: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>depth[x] &lt;span style="color:#f92672">&amp;gt;&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>depth[y]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> x, y &lt;span style="color:#f92672">=&lt;/span> y, x &lt;span style="color:#75715e"># 确保x的深度不大于y&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 1. 使y和x在同一深度&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> y &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>get_kth_ancestor(y, self&lt;span style="color:#f92672">.&lt;/span>depth[y] &lt;span style="color:#f92672">-&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>depth[x])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 2. 检查是否重合&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> y &lt;span style="color:#f92672">==&lt;/span> x:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> x &lt;span style="color:#75715e"># x本身就是y的祖先&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 3. 倍增跳跃：从最大的步长开始尝试&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(len(self&lt;span style="color:#f92672">.&lt;/span>pa[x]) &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> px, py &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>pa[x][i], self&lt;span style="color:#f92672">.&lt;/span>pa[y][i]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> px &lt;span style="color:#f92672">!=&lt;/span> py:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> x, y &lt;span style="color:#f92672">=&lt;/span> px, py &lt;span style="color:#75715e"># 同时往上跳 2**i 步&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 4. 此时x和y的父节点就是LCA&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>pa[x][&lt;span style="color:#ae81ff">0&lt;/span>]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我盯着那段&lt;code>for i in range(len(self.pa[x]) - 1, -1, -1)&lt;/code>的代码，有点困惑：&amp;ldquo;为什么要从大到小跳？&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;&lt;strong>因为要尽快接近LCA，但又不能跳过。&lt;/strong>&amp;ldquo;老勇者打了个比方，&amp;ldquo;就像用砝码称重——先试最大的砝码，太重的就撤掉，刚好就留着，这样精度最高。从小到大跳的话，你跳完1步、2步、4步……到后面可能会跳过LCA，还得退回来，退回来又不知道退多少。&lt;strong>从大到小是贪心，每次跳最大可能的步数，但保证不跳过LCA&lt;/strong>——因为跳过的话 &lt;code>pa[x][i] == pa[y][i]&lt;/code>，说明这步太大了，LCA在下面，就不跳。这样就能保证每次都能最大程度地接近LCA，最终刚好卡在LCA正下方，再往上一步就是LCA。&amp;rdquo;&lt;/p>
&lt;p>我反应过来了：&amp;ldquo;所以最后返回&lt;code>pa[x][0]&lt;/code>——x的父节点——就是LCA？预处理O(n log n)，查询O(log n)，这就是二进制提升找LCA的威力？&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;没错。&amp;ldquo;老勇者点点头，话锋一转，&amp;ldquo;来，LCA有了，路径长度怎么算？你之前推导过。&amp;rdquo;&lt;/p>
&lt;p>我愣了一下，脑子里迅速回放开始写的公式：&amp;ldquo;k = depth[u] + depth[v] - 2*depth[LCA]！&amp;ldquo;抄起键盘补上：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">LcaBinaryLifting&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">......&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">get_dis&lt;/span>(self, x: int, y: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>depth[x] &lt;span style="color:#f92672">+&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>depth[y] &lt;span style="color:#f92672">-&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>depth[self&lt;span style="color:#f92672">.&lt;/span>get_lca(x, y)] &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>老勇者满意地点点头：&amp;ldquo;LCA拿到，套公式算出k，再套昨天的对称性结论——方案数 = 2^{k-1}，这题就解决了。&amp;rdquo;&lt;/p>
&lt;p>他顺手补上主函数，一行列表推导式，对每个查询(x, y)，如果x == y，答案是0；否则答案是2^{路径长度减一}：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 预处理2的幂（用于快速计算方案数）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>MOD &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1_000_000_007&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>POW2 &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">10&lt;/span> &lt;span style="color:#f92672">**&lt;/span> &lt;span style="color:#ae81ff">5&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>POW2[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, len(POW2)):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> POW2[i] &lt;span style="color:#f92672">=&lt;/span> POW2[i &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">%&lt;/span> MOD
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Solution&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">assignEdgeWeights&lt;/span>(self, edges: List[List[int]], queries: List[List[int]]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> List[int]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g &lt;span style="color:#f92672">=&lt;/span> LcaBinaryLifting(edges) &lt;span style="color:#75715e"># 初始化LCA结构&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> POW2[g&lt;span style="color:#f92672">.&lt;/span>get_dis(x &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, y &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#66d9ef">if&lt;/span> x &lt;span style="color:#f92672">!=&lt;/span> y &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> x, y &lt;span style="color:#f92672">in&lt;/span> queries
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我盯着那段 POW2 数组：&amp;ldquo;为什么不直接用 pow(2, k-1, MOD)？Python内置的快速幂不是挺快吗？&amp;rdquo;&lt;/p>
&lt;p>老勇者瞪了我一眼，恨铁不成钢地说：&amp;ldquo;还是预处理！pow 再快也是快速幂，十万次查询，每次O(log k)，就是十万乘十七，一百七十万次运算。但如果提前把2的幂打好表——&lt;code>POW2[i] = 2^i mod MOD&lt;/code>——预处理O(n)一次性搞定，之后每次查表O(1)。&lt;strong>空间换时间，经典到不能再经典的套路&lt;/strong>。&amp;rdquo;&lt;/p>
&lt;p>我这才醒悟过来，&amp;ldquo;哦——就像游戏里提前算好所有技能伤害表，战斗时直接查，不用现场按计算器！&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;总算回过神了你。&amp;ldquo;老勇者点点头，&amp;ldquo;不过记住，预处理的数组大小要掐准数据范围。题目n ≤ 10^5，POW2开10^5就够。开小了查询越界，开大了浪费内存——这就是程序员的&amp;rsquo;度&amp;rsquo;。&amp;rdquo;&lt;/p>
&lt;hr>
&lt;h4 id="终章">终章
&lt;/h4>&lt;p>我把整套代码拼起来，提交。屏幕上的查询列表飞速滚动，一个一个&lt;code>2^{k-1}&lt;/code>弹出来，快得像是开了倍速——村长的Steam Deck传来一声清脆的 &amp;lsquo;叮&amp;rsquo;——&amp;lsquo;&amp;lsquo;奇数流宗师&amp;rsquo;&amp;lsquo;成就图标从灰色变成金色，再从金色变成带粒子特效的彩虹色，老头激动得差点把电脑举过头顶，&amp;ldquo;拿到了！宗师！双层成就！我就知道找你是对的！&amp;rdquo;&lt;/p>
&lt;p>我靠在椅背上，盯着那串 &lt;code>LcaBinaryLifting&lt;/code> 的代码，想要把它刻进脑袋里——毕竟学会了树上跳跃的神功，我可不想再一步一步蜗牛爬了。老勇者这招“二进制提升”，听起来像黑客，用起来像开挂，真香。&lt;/p>
&lt;p>村长抱着电脑乐颠颠地回去了，Steam Deck的风扇声从远处传来，不知道他又在肝什么新成就。我合上魔法书，往锅炉里添了根柴。今天的树上轻功够我消化几天了——下次再见到老勇者，我一定要把倍增表倒背如流。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3558：对称性的觉醒：当奇数和偶数平分天下</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-11/</link><pubDate>Thu, 11 Jun 2026 13:00:38 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-11/</guid><description>&lt;h2 id="-对称性的觉醒当奇数和偶数平分天下">🎮 对称性的觉醒：当奇数和偶数平分天下
&lt;/h2>&lt;p>听说村长最近沉迷于一款叫《技能树大亨》的RPG游戏——我怀疑这老头把省下来的钱全充了Steam余额，账号时长怕不是比我刷题还多。今天一大早，我正蹲在锅炉边添柴，电脑屏幕突然弹出老头的远程协助请求。点下接受，村长的游戏画面直接投屏过来——他的人物正卡在技能树界面，鼠标在一棵枝繁叶茂的树状图上疯狂画圈，技能节点之间的连线旁还标注着&amp;quot;普通升级: 1点&amp;quot;和&amp;quot;强化升级: 2点&amp;quot;。&lt;/p>
&lt;p>&amp;ldquo;小白！我的隐藏成就卡住了！&amp;ldquo;村长的声音从屏幕那头炸出来，急得像是副本门口等着拉人的团长，&amp;ldquo;从根技能1号出发，每条连线可以选普通升级花1点，或者强化升级花2点。隐藏成就叫&amp;rsquo;奇数流大师&amp;rsquo;，要求算出有多少种加点法能使到达最深那个终极技能的总消耗是奇数！”&lt;/p>
&lt;p>我盯着屏幕，扫到成就描述末尾一行不起眼的小字——&amp;ldquo;忽略从根到最深节点路径外的所有边&amp;rdquo;。这不就把一棵树压缩成一条链吗？只有从根到最深节点的那条路径算数，其他枝丫全是气氛组，约等于RPG里站桩喊固定台词的NPC。问题瞬缩成两件事：第一，最深那层在哪儿——也就是最大深度k是多少；第二，这k条边上各赋1或2，总和为奇数的组合数是多少。&lt;/p>
&lt;hr>
&lt;h4 id="第一步找深度是dfs的肌肉记忆">第一步：找深度是DFS的肌肉记忆
&lt;/h4>&lt;p>题目给的是无向树——边[1,2]表示1和2互通，虽然根定在1，但遍历必须双向通行，否则DFS 会困在叶子节点，剩下的分支全遍历不到。所以建邻接表时必须每条边两头都加——1的邻居里有2，2的邻居里也有1，双向车道全登记，DFS才能想去哪去哪。&lt;/p>
&lt;p>但双向建图有一个经典坑——不记父节点的话，DFS会在父子之间来回横跳，像喝醉的勇者在两个房间之间来回读条，直到栈溢出教我做人。所以递归时带个fa参数当“防回退机制”——告诉DFS“你刚从你爸那儿过来，别再回去了”&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>MOD &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1_000_000_007&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>n &lt;span style="color:#f92672">=&lt;/span> len(edges) &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>g &lt;span style="color:#f92672">=&lt;/span> [[] &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(n &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> x, y &lt;span style="color:#f92672">in&lt;/span> edges:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g[x]&lt;span style="color:#f92672">.&lt;/span>append(y)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g[y]&lt;span style="color:#f92672">.&lt;/span>append(x)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">dfs&lt;/span>(x: int, fa: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> d &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> y &lt;span style="color:#f92672">in&lt;/span> g[x]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> y &lt;span style="color:#f92672">!=&lt;/span> fa: &lt;span style="color:#75715e"># 不递归到父节点，防止无限循环&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> d &lt;span style="color:#f92672">=&lt;/span> max(d, dfs(y, x) &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> d
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>k &lt;span style="color:#f92672">=&lt;/span> dfs(&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>双向建图 + fa过滤 = 无向图当有向树遍历。叶子节点没有非父邻居，直接返回0；自底向上逐层加一，max取最大深度。这套组合拳打过无数次的DFS模板——就像RPG里的基础连招。&lt;/p>
&lt;p>深度k到手。&lt;/p>
&lt;hr>
&lt;h4 id="第二步奇偶性的对称美学">第二步：奇偶性的对称美学
&lt;/h4>&lt;p>接下来是核心——k条边，每条1或2，总和为奇数的方案数。&lt;/p>
&lt;p>1是奇数，2是偶数，总和为奇数等价于路径上有奇数个1。枚举C(k,1) + C(k,3) + C(k,5) + &amp;hellip;？我数学早就连本带利还给老师了，组合数求和公式长啥样都忘了，更别说还要取模。&lt;/p>
&lt;p>等等——换个思路：&lt;strong>如果前k-1条边的和是偶数，最后一条只能选1把总和扳成奇数；前k-1条边的和是奇数，最后一条必须选2把总和保持奇数。&lt;/strong>&lt;/p>
&lt;p>也就是说，前k-1条边可以自由发挥，最后一条边只能被迫营业——有且仅有一种选择能救场。所以总方案数就是前k-1条边的任意组合数——&lt;strong>前k-1条每条自由选1或2，总共2^{k-1}种&lt;/strong>。&lt;/p>
&lt;p>再往深层想的话，这可以说是奇偶对称性的必然结果：&lt;strong>对于任意k≥1，在所有2^k种赋值中，权重和为奇数和偶数的方案数永远平分天下，各占一半&lt;/strong>。&lt;/p>
&lt;p>这不是什么巧合，是奇偶性天生的对称：把最后一条边从1翻成2，奇偶就翻转，所有方案两两配对，恰好一半奇一半偶。所以答案=&lt;strong>2^k / 2 = 2^{k-1}&lt;/strong>。不用枚举，不用求和，一行对称性直接秒杀。&lt;/p>
&lt;pre tabindex="0">&lt;code>return pow(2, k - 1, MOD)
&lt;/code>&lt;/pre>&lt;p>&lt;code>pow(base, exp, mod)&lt;/code>快速幂，底层用二分指数，O(log k)搞定，属实是标准库里的隐藏神器。&lt;/p>
&lt;hr>
&lt;h4 id="收工">收工？
&lt;/h4>&lt;p>我敲下回车，屏幕那头村长的隐藏成就图标“叮”地一声亮了，金色光芒闪得他差点把电脑抱起来转圈。“拿到了！奇数流大师！我就知道找你是对的！” 村长的声音兴奋得像刚抽到SSR。&lt;/p>
&lt;p>我刚想谦虚两句，突然看到游戏界面上弹出一个新的成就预告——&amp;ldquo;奇数流宗师：应对任意两个技能节点之间的路径，计算奇偶方案数，解锁终极加点方案&amp;rdquo;。&lt;/p>
&lt;p>我心里咯噔一下。任意两个节点之间的路径？那可不是今天这种“从根到最深叶子”的单条快车道了——查询可能从任意节点出发，到任意节点结束，路径长度各不相同，有些说不定还会往上走到公共祖先再拐下去。奇偶对称性的核心没变，路径上有k条边，方案数还是2^{k-1}，但每条查询的k都不一样，怎么快速找出任意两点之间的距离？&lt;/p>
&lt;p>我感觉自己的后背开始冒汗。&lt;/p>
&lt;p>村长还在屏幕那头兴奋地嚷嚷：&amp;ldquo;小白，你看这个&amp;rsquo;奇数流宗师&amp;rsquo;成就，好像有点意思啊！要不你现在帮我看看……&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;今、今天就算了吧！&amp;ldquo;我慌忙打断他，&amp;ldquo;我得去……去添柴！锅炉要熄了！&amp;rdquo;&lt;/p>
&lt;p>说完，我果断切断了远程连接，瘫在椅子上长长地舒了一口气。&lt;/p>
&lt;p>明天的事，明天再说吧。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3691从堆到树：一道题的两个平行宇宙</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-10/</link><pubDate>Wed, 10 Jun 2026 16:10:20 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-10/</guid><description>&lt;h2 id="从堆到树一道题的两个平行宇宙">🎮从堆到树：一道题的两个平行宇宙
&lt;/h2>&lt;p>今天的任务卷轴——3691. 最大子数组总值 II——规则赫然写着：选恰好 k 个&lt;strong>不同的非空子数组，可以重叠，同一个子数组不能被选择超过一次&lt;/strong>。求最大极差之和。&lt;/p>
&lt;p>我差点笑出声。这不就是老勇者昨天拿红笔在我羊皮纸上改出来的那道“如果加上约束”吗？连标点符号都没改！直接上昨天那套代码——隧道模型 + 贪心堆 + 稀疏表。稀疏表预处理 O(n log n)，每条隧道初始最深段入堆，候选池恒定 n 个，每次弹堆顶累加极差、同一隧道缩一格压回，k 次堆操作 O(k log n)。提交，AC，一气呵成。&lt;/p>
&lt;p>我往椅背上一靠，长长地舒了一口气。今天这副本过得这么快，说白了还得感谢老勇者昨天提前给我开的小灶，跟考前泄题似的。我严重怀疑这老头是策划部的内鬼。我端起茶杯，想着今天终于不用担心被弹脑瓜崩——毕竟答案是他自己教的，他总不能弹自己吧？&lt;/p>
&lt;p>茶杯刚端到嘴边，茶水表面映出一个熟悉的身影。我手一抖，滚烫的茶水差点浇在裤裆上。老勇者正站在我身后，低头看着我的屏幕，表情介于&amp;quot;孺子可教&amp;quot;和&amp;quot;你还差得远&amp;quot;之间。&lt;/p>
&lt;p>&amp;ldquo;大、大师……&amp;ldquo;我结结巴巴地说道，&amp;ldquo;你怎么又来了？！这题不就是昨天你红笔改的那道吗，没加新约束，原封不动！我用的是你教的隧道模型加堆加稀疏表，一个变量都没改！&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;别紧张。&amp;ldquo;老勇者笑了笑，在我肩上轻轻拍了拍——终于不是弹脑瓜崩，&amp;ldquo;昨天教你的，今天能直接用上，说明你记住了，理解了&amp;rsquo;固定一端、贪心另一端&amp;rsquo;的精髓。我来不是为了弹你，是来问你一个问题。&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;问题？&amp;ldquo;我心里咯噔一下，毕竟老勇者的&amp;quot;问题&amp;quot;通常意味着&amp;quot;你又掉坑里了&amp;quot;或者&amp;quot;还有更优解&amp;rdquo;。&lt;/p>
&lt;p>老勇者指了指我的代码：&amp;ldquo;隧道模型加堆，k 次堆操作 O(k log n)，k 到十万的时候千万级操作量，常数拉满。如果哪天 k 飙到 10^6、10^7，你这个堆还撑得住吗？”&lt;/p>
&lt;p>我倒吸一口凉气。说实话，我压根没想过这个问题。能过就行，还要什么自行车？但老勇者这么一说，我突然意识到：这解法复杂度跟 k 线性相关，k 越大越慢。现在 k ≤ 10^5 能过，但如果 k 到了 10^6、10^7 呢？如果这是一道交互题，要求在线查询呢？&lt;/p>
&lt;p>&amp;ldquo;真正的勇士，&amp;ldquo;老勇者缓缓开口，&amp;ldquo;敢于直面惨淡的复杂度，敢于正视淋漓的 TLE。如果有一种方法，复杂度跟 k 彻底解耦，能在 O(n log n) 的时间内解决，不管 k 有多大都稳如泰山——你想不想学？&amp;rdquo;&lt;/p>
&lt;p>我手里的茶杯&amp;quot;啪&amp;quot;地掉在地上：&amp;ldquo;O(n log n)？跟 k 无关？这都违反信息论了吧？！&amp;rdquo;&lt;/p>
&lt;p>老勇者狡黠一笑，&amp;rdquo;&lt;strong>二分答案&lt;/strong>，上古时代用来处理 Top-K 问题的禁忌魔法。它的核心思想简单得反直觉：&lt;strong>不直接找那 k 个子数组，而是先找到第 k 大的极差值到底是多少，然后分类统计&lt;/strong>。&lt;/p>
&lt;p>&amp;ldquo;等等，你是说……我不需要知道前 k 名分别是谁，只需要知道第 k 名的分数是多少？” 我感觉自己的世界观在崩塌。&lt;/p>
&lt;p>“对。”老勇者打了个响指，空中应声浮现出一排数字，“就像你不需要把全班成绩单从头排到尾，只需要知道第十名的分数——然后比它高的全录，刚好压线的挑几个凑够人数，剩下的全刷。这种思路叫&lt;strong>二分答案判定&lt;/strong>，把选择问题降维成判定问题。在很多 Top-K 场景里，都是降维打击的利器。”&lt;/p>
&lt;hr>
&lt;h4 id="第一重封印二分答案先找阈值再分类统计">第一重封印：二分答案——先找阈值，再分类统计
&lt;/h4>&lt;p>老勇者在卷轴背面画出一条数轴，从 &lt;code>0&lt;/code> 标到 &lt;code>max(nums) - min(nums)&lt;/code>。&lt;/p>
&lt;p>&amp;ldquo;想象你在玩一个猜数字游戏。我告诉你：存在一个阈值 &lt;code>low_d&lt;/code>，所有极差严格大于 &lt;code>low_d&lt;/code> 的子数组，数量凑不够 k 个；但如果把等于 &lt;code>low_d&lt;/code> 的也加上，数量就够 k 了——甚至超过 k。这个 &lt;code>low_d&lt;/code> 是什么？&amp;rdquo;&lt;/p>
&lt;p>我想了想：&amp;ldquo;这不就是第 k 大的极差值吗？排名刚好卡在第 k 位的那个人！&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;没错。&amp;ldquo;老勇者点点头，手指在数轴上轻轻一划，&amp;ldquo;如果我们能快速算出&amp;rsquo;有多少个子数组的极差 ≥ X&amp;rsquo;，就能用二分查找精准定位这个 &lt;code>low_d&lt;/code>。找到之后，问题就拆成三块：极差严格大于 &lt;code>low_d&lt;/code> 的全选；极差刚好等于 &lt;code>low_d&lt;/code> 的，挑几个补满 k 个；极差小于 &lt;code>low_d&lt;/code> 的直接pass。最后用选中的子数组去求和。&amp;rdquo;&lt;/p>
&lt;p>我眼睛一亮：&amp;ldquo;这不就高考录取吗！先划分数线，过线的全录，压线的按志愿顺序挑，没达线的回家种地。把 Top-K 选择问题降维成一个判定问题——&amp;lsquo;这个分数线划得合不合理？&amp;rsquo;——然后二分答案！&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;是这个道理。&amp;ldquo;老勇者笑了笑，话锋陡然一转，&amp;ldquo;不过，现在你只是知道了解法蓝图。蓝图里最关键的那块砖还没烧——怎么高效判定？给你一个阈值 X，你&lt;strong>怎么快速统计出极差 ≥ X 的子数组到底有多少个&lt;/strong>？&amp;rdquo;&lt;/p>
&lt;p>我嘴巴闭得紧紧的，心里反复默念——绝对不能提 O(n²) 暴力枚举，求生欲告诉我，沉默是金。&lt;/p>
&lt;p>他瞅了瞅我：&amp;ldquo;这就引出了第二重封印——滑动窗口加单调队列。想学的话，把你脑子里那个 O(n²) 的念头掐掉，我要往里面塞点真正高效的东西。&amp;rdquo;&lt;/p>
&lt;hr>
&lt;h4 id="第二重封印滑动窗口--单调队列像塔防游戏一样收缩防线">第二重封印：滑动窗口 + 单调队列——像塔防游戏一样收缩防线
&lt;/h4>&lt;p>老勇者在纸上画了一个窗口 &lt;code>[left, i]&lt;/code>，左右两端各站着一个守卫，一个盯最大值，一个盯最小值。&lt;/p>
&lt;p>&amp;ldquo;想象你在玩一个塔防游戏。右端点 i 是你不断往前推的兵线，每次加入一个新元素，你要维护窗口内的最大值和最小值，确保极差始终小于 X。一旦极差飙到 ≥ X，说明战线崩了，就得把左端点 left 往右挪，收缩防线，直到极差重新掉回安全线以下。&amp;rdquo;&lt;/p>
&lt;p>他写下一段代码，我凑了过去，看到两个双端队列 &lt;code>min_q&lt;/code> 和 &lt;code>max_q&lt;/code>，一个单调递增存最小值候选，一个单调递减存最大值候选。每次 i 右移，新元素入队，过期元素被窗口淘汰。关键在最后一句：&lt;code>cnt += left&lt;/code>——对于当前右端点 i，左端点在 &lt;code>[0, left-1]&lt;/code> 范围内的所有子数组，极差都 ≥ X，所以直接累加 left 就是新增的数量。一旦 cnt 攒到 k，就说明这个阈值太低，数量超标了，直接打回。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">check&lt;/span>(low_d: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> bool:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> low_d &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 实际检查的是极差 &amp;gt;= low_d+1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> min_q &lt;span style="color:#f92672">=&lt;/span> deque() &lt;span style="color:#75715e"># 单调递增队列，维护最小值的下标&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max_q &lt;span style="color:#f92672">=&lt;/span> deque() &lt;span style="color:#75715e"># 单调递减队列，维护最大值的下标&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt &lt;span style="color:#f92672">=&lt;/span> left &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i, x &lt;span style="color:#f92672">in&lt;/span> enumerate(nums):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 1. 右边入队：维护单调性&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> min_q &lt;span style="color:#f92672">and&lt;/span> x &lt;span style="color:#f92672">&amp;lt;=&lt;/span> nums[min_q[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>]]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> min_q&lt;span style="color:#f92672">.&lt;/span>pop()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> min_q&lt;span style="color:#f92672">.&lt;/span>append(i)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> max_q &lt;span style="color:#f92672">and&lt;/span> x &lt;span style="color:#f92672">&amp;gt;=&lt;/span> nums[max_q[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>]]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max_q&lt;span style="color:#f92672">.&lt;/span>pop()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max_q&lt;span style="color:#f92672">.&lt;/span>append(i)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 2. 左边出队：收缩窗口直到极差 &amp;lt; low_d+1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> nums[max_q[&lt;span style="color:#ae81ff">0&lt;/span>]] &lt;span style="color:#f92672">-&lt;/span> nums[min_q[&lt;span style="color:#ae81ff">0&lt;/span>]] &lt;span style="color:#f92672">&amp;gt;=&lt;/span> low_d:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> left &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> min_q[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">&amp;lt;&lt;/span> left: &lt;span style="color:#75715e"># 队首不在窗口中&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> min_q&lt;span style="color:#f92672">.&lt;/span>popleft()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> max_q[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">&amp;lt;&lt;/span> left: &lt;span style="color:#75715e"># 队首不在窗口中&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max_q&lt;span style="color:#f92672">.&lt;/span>popleft()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt &lt;span style="color:#f92672">+=&lt;/span> left &lt;span style="color:#75715e"># 对于右端点i，有left个左端点使得极差 &amp;gt;= low_d+1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> cnt &lt;span style="color:#f92672">&amp;gt;=&lt;/span> k:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">False&lt;/span> &lt;span style="color:#75715e"># 数量够了，阈值太低&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span> &lt;span style="color:#75715e"># 数量不够，阈值太高&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>low_d &lt;span style="color:#f92672">=&lt;/span> bisect_left(range(max(nums) &lt;span style="color:#f92672">-&lt;/span> min(nums)), &lt;span style="color:#66d9ef">True&lt;/span>, key&lt;span style="color:#f92672">=&lt;/span>check)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我盯着那段单调队列的代码，感觉似曾相识：&amp;ldquo;等等，两个双端队列一左一右夹着窗口滑过去，最大值最小值实时追踪，这不就是 1438 题&amp;rsquo;绝对差不超过限制的最长连续子数组&amp;rsquo;的经典套路吗？那道题求的是窗口能撑到多长，这道题求的是子数组数量，就差在 &lt;code>cnt += left&lt;/code> 这一行！&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;没错。&amp;ldquo;老勇者点点头，&amp;ldquo;对于当前右端点 i，左端点在 &lt;code>[0, left-1]&lt;/code> 范围内的所有子数组，极差全都 ≥ &lt;code>low_d&lt;/code>。所以直接累加 left，数量就出来了。判定函数的返回值也反过来用：返回 True 说明数量不够，阈值太高了，得往下降；返回 False 说明数量超了，阈值太低了，得往上抬。二分夹逼到最后，第一个让 check 返回 True 的 &lt;code>low_d&lt;/code>，就是第 k 大的极差值。&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;但——&amp;ldquo;他话锋一转，眼神变得危险起来，&amp;ldquo;滑动窗口只能告诉你&amp;rsquo;有多少个&amp;rsquo;子数组过线了，至于每个过线子数组的极差具体是多少、加起来总和多大，它一概不管，就像个只数人头不登记分数的门卫。&amp;rdquo;&lt;/p>
&lt;p>我感觉后背开始冒汗，&amp;ldquo;既然滑动窗口只能计数不能求和，那要求极差之和，难道要把每个子数组的最大最小值都查出来？这不又退回 O(n²) 了？&amp;rdquo;&lt;/p>
&lt;p>&amp;lsquo;&amp;lsquo;所以才需要第三重封印。&amp;ldquo;老勇者在纸上写下三个大字——单调栈，&amp;ldquo;判定靠滑动窗口，求和靠单调栈加线段树——前者是哨兵站岗，只管数人头；后者是工程兵架桥，把每一段极差的具体数值算清楚。&amp;rdquo;&lt;/p>
&lt;hr>
&lt;h4 id="第三重封印单调栈给每个元素划地盘">第三重封印：单调栈——给每个元素划地盘
&lt;/h4>&lt;p>&amp;ldquo;先解决第一步——怎么快速知道每个子数组的最小值和最大值由谁当。&amp;ldquo;老勇者叩了叩桌子，&amp;ldquo;对于每个位置 i，你怎么快速找到它左边第一个 ≤ nums[i] 的位置？&amp;rdquo;&lt;/p>
&lt;p>我想了想：&amp;lsquo;&amp;lsquo;用一个单调递增栈，从左往右扫，栈顶就是左边第一个比当前元素小的位置？&amp;rsquo;&amp;rsquo;&lt;/p>
&lt;p>&amp;lsquo;&amp;lsquo;对。&amp;lsquo;&amp;lsquo;老勇者提笔唰唰写下几行代码：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>n &lt;span style="color:#f92672">=&lt;/span> len(nums)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>left_less_eq &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> n &lt;span style="color:#75715e"># 左边第一个 &amp;lt;= nums[i] 的位置&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>left_great_eq &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> n &lt;span style="color:#75715e"># 左边第一个 &amp;gt;= nums[i] 的位置&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>st1 &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#75715e"># 哨兵&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>st2 &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i, x &lt;span style="color:#f92672">in&lt;/span> enumerate(nums):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> len(st1) &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">and&lt;/span> nums[st1[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>]] &lt;span style="color:#f92672">&amp;gt;&lt;/span> x:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> st1&lt;span style="color:#f92672">.&lt;/span>pop()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> left_less_eq[i] &lt;span style="color:#f92672">=&lt;/span> st1[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> st1&lt;span style="color:#f92672">.&lt;/span>append(i)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> len(st2) &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">and&lt;/span> nums[st2[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>]] &lt;span style="color:#f92672">&amp;lt;&lt;/span> x:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> st2&lt;span style="color:#f92672">.&lt;/span>pop()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> left_great_eq[i] &lt;span style="color:#f92672">=&lt;/span> st2[&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> st2&lt;span style="color:#f92672">.&lt;/span>append(i)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&amp;ldquo;两个单调栈，一趟扫描，分别追踪左边第一个 ≤ 和第一个 &amp;gt; 的位置。&lt;code>left_less_eq[i]&lt;/code> 表示位置 i 左边第一个 ≤ nums[i] 的位置。这意味着在区间 &lt;code>(left_less_eq[i], i]&lt;/code> 内，所有元素都严格大于 nums[i]。所以，&lt;strong>以 i 为右端点、左端点落在这个区间内的所有子数组，最小值统统是 nums[i]&lt;/strong>。&amp;rsquo;&amp;rsquo;&lt;/p>
&lt;p>我惊呼道：&amp;ldquo;那 &lt;code>left_greater_eq[i]&lt;/code> 就是左边第一个 ≥ nums[i] 的位置，左端点落在 &lt;code>(left_greater_eq[i], i]&lt;/code> 内的所有子数组，最大值都是&lt;code> nums[i]&lt;/code>！两个数组合起来，就把每个元素作为最小值或最大值的‘地盘’划得清清楚楚！&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;没错。相当于给每个元素发了一张地契——在它的地盘里，它就是最值。&amp;ldquo;老勇者点点头，&amp;ldquo;但光有地盘还不够。你想想，随着右端点 i 不断往右移，每个左端点 j 对应的子数组 &lt;code>[j, i]&lt;/code> 的最小值和最大值是动态变化的。新元素一加入，某些左端点对应的最值就要更新。你需要一个数据结构，能把这些动态变化的最值维护起来，还能快速查询任意左端点区间内的最值之和。&amp;rdquo;&lt;/p>
&lt;p>我感觉脑细胞已经开始集体抗议：&amp;ldquo;今天的强度过分了啊——动态维护所有左端点的最值？还区间查询求和？这活儿谁干得了？队列不行，栈也不行，总不能上树吧——&amp;rdquo;&lt;/p>
&lt;p>老勇者咳了一声，连锅炉里的火都抖了三抖，&amp;ldquo;你说对了——就是要上树。欢迎来到地狱模式的最终章：懒标记线段树。&amp;rdquo;&lt;/p>
&lt;hr>
&lt;h4 id="第四重封印懒标记线段树动态维护所有左端点的极值">第四重封印：懒标记线段树——动态维护所有左端点的极值
&lt;/h4>&lt;p>老勇者在纸上画出一棵倒挂的二叉树，每个节点上挂着四面小旗，分别写着 &lt;code>sum_min&lt;/code>、&lt;code>sum_max&lt;/code>、&lt;code>l_min&lt;/code>、&lt;code>l_max&lt;/code>。&lt;/p>
&lt;p>&amp;ldquo;线段树的下标 j 代表&amp;rsquo;&lt;strong>以 j 为左端点，当前 i 为右端点&lt;/strong>&amp;lsquo;的子数组 &lt;code>[j, i]&lt;/code>。每个节点管一片连续的左端点区间，维护四个值——&lt;code>sum_min&lt;/code> 是这个区间内所有左端点对应的当前子数组的&lt;strong>最小值之和&lt;/strong>，&lt;code>sum_max&lt;/code> 是&lt;strong>最大值之和&lt;/strong>。&lt;code>l_min&lt;/code> 和 &lt;code>l_max&lt;/code> 是&lt;strong>懒标记&lt;/strong>，代表这个区间内所有左端点被统一刷成的&amp;rsquo;共同最小值&amp;rsquo;和&amp;rsquo;共同最大值&amp;rsquo;。&amp;rdquo;&lt;/p>
&lt;p>我感觉自己的 SAN 值正在狂掉：&amp;ldquo;等等，为什么存&amp;rsquo;和&amp;rsquo;？还有那个&amp;rsquo;懒标记&amp;rsquo;是什么鬼？听起来像我的拖延症一样。&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;因为你要的不是单个子数组的极差，而是&amp;rsquo;&lt;strong>所有极差 ≥ low_d 的子数组的极差之和&lt;/strong>&amp;rsquo;。&amp;ldquo;老勇者耐心地解释，&amp;ldquo;如果你只存单个最值，查区间 &lt;code>[0, l]&lt;/code> 的极差之和时，你得把 l+1 个左端点挨个捞出来，一个个减，一个个加——O(n)，求和又炸了。但每个节点提前把管辖区的 &lt;code>sum_max&lt;/code> 和 &lt;code>sum_min&lt;/code> 存好，一次区间查询直接拎出两个总和，&lt;strong>极差之和 = sum_max - sum_min&lt;/strong>，O(log n) 完事。&amp;rdquo;&lt;/p>
&lt;p>我恍然大悟：&amp;ldquo;所以线段树不是在存单个值，是在存&amp;rsquo;辖区总和&amp;rsquo;！每个节点提前把自己管的那段区间的账算好了，上面要查的时候直接报总数，不用一个一个数人头！&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;正是。这就是线段树的精髓——把 O(n) 的区间求和压成 O(log n) 的分块预结算。&amp;ldquo;老勇者话锋一转，&amp;ldquo;但光有查询还不够，你还得能更新。每次 i 往右移一位，新元素 &lt;code>nums[i]&lt;/code> 加入，哪些左端点对应的子数组最值会变？&amp;rdquo;&lt;/p>
&lt;p>我顺着他的目光看向之前写的单调栈代码：&amp;ldquo;左端点落在&lt;code> (left_less_eq[i], i]&lt;/code> 内的，最小值统统更新为 &lt;code>nums[i]&lt;/code>；左端点落在&lt;code> (left_greater_eq[i], i]&lt;/code> 内的，最大值统统更新为 &lt;code>nums[i]&lt;/code>。这就是&lt;strong>两次区间赋值&lt;/strong>——每来一个新元素，就要把两片连续区间的所有左端点的最值全刷成同一个数。&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;对。区间赋值，暴力改是 O(n)，来一个元素改一片，总复杂度直接飙回 O(n²)。这时候&lt;strong>懒标记&lt;/strong>就登场了。&amp;ldquo;老勇者指了指节点上那两面写有 &lt;code>l_min&lt;/code> 和 &lt;code>l_max&lt;/code> 的小旗，&amp;ldquo;以刷最小值为例——如果当前节点完全被覆盖，直接把它的 &lt;code>l_min&lt;/code> 更新成 &lt;code>nums[i]&lt;/code>，对应的 &lt;code>sum_min&lt;/code> 就是 &lt;code>l_min × 区间长度&lt;/code>，然后拍拍屁股走人，&lt;strong>不用往下递归到每个叶子&lt;/strong>。它的子孙们先欠着，等下次查询或更新路过的时候再把这笔账还下去。这样一次区间赋值也是 O(log n)，而不是 O(n)。&amp;rdquo;&lt;/p>
&lt;p>我目瞪口呆，&amp;ldquo;所以懒标记不是偷懒，是把区间赋值的活儿打包延期执行——每次只改大区间的总账，明细先欠着，等要查明细的时候再往下分摊！这不就是我平时写代码时的 &amp;lsquo;TODO: 稍后优化&amp;rsquo; 吗？&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;你这说的……虽然不太恰当，但意外地准确。&amp;ldquo;老勇者嘴角抽搐了一下，&amp;ldquo;懒标记的核心思想就是&lt;strong>延迟传播&lt;/strong>（Lazy Propagation）。更新时只改当前节点的标记和总和，不往下递归；查询或再次更新时，如果需要访问子节点，才把标记下传（spread）。这样保证每次操作都是 O(log n)。&amp;rdquo;&lt;/p>
&lt;p>他提笔写下了一颗完整的懒标记线段树，看起来就像魔法阵一样：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Node&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># val = [sum_min, sum_max, l_min, l_max]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># todo = [todo_min, todo_max]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> __slots__ &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;val&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;todo&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">LazySegmentTree&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 懒标记初始值&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _TODO_INIT &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> __init__(self, n: int):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 线段树维护一个长为 n 的数组（下标从 0 到 n-1）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_n &lt;span style="color:#f92672">=&lt;/span> n
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_tree &lt;span style="color:#f92672">=&lt;/span> [Node() &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">&amp;lt;&amp;lt;&lt;/span> (n &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>bit_length())]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_build(&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>, n &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 合并两个 val&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">_merge_val&lt;/span>(self, a: List[int], b: List[int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> List[int]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> [a[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">+&lt;/span> b[&lt;span style="color:#ae81ff">0&lt;/span>], a[&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">+&lt;/span> b[&lt;span style="color:#ae81ff">1&lt;/span>], a[&lt;span style="color:#ae81ff">2&lt;/span>], a[&lt;span style="color:#ae81ff">3&lt;/span>]]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 把懒标记作用到 node 子树（本例为区间赋值）&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">_apply&lt;/span>(self, node: int, l: int, r: int, todo) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cur &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>_tree[node]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 计算 tree[node] 区间的整体变化&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> todo_min, todo_max &lt;span style="color:#f92672">=&lt;/span> todo
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> todo_min &lt;span style="color:#f92672">&amp;gt;=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cur&lt;span style="color:#f92672">.&lt;/span>val[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> todo_min &lt;span style="color:#f92672">*&lt;/span> (r &lt;span style="color:#f92672">-&lt;/span> l &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#75715e"># 区间最小值之和 = 最小值 × 区间长度&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cur&lt;span style="color:#f92672">.&lt;/span>val[&lt;span style="color:#ae81ff">2&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> todo_min &lt;span style="color:#75715e"># 记录共同最小值&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cur&lt;span style="color:#f92672">.&lt;/span>todo[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> todo_min &lt;span style="color:#75715e"># 设置懒标记&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> todo_max &lt;span style="color:#f92672">&amp;gt;=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cur&lt;span style="color:#f92672">.&lt;/span>val[&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> todo_max &lt;span style="color:#f92672">*&lt;/span> (r &lt;span style="color:#f92672">-&lt;/span> l &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#75715e"># 区间最大值之和&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cur&lt;span style="color:#f92672">.&lt;/span>val[&lt;span style="color:#ae81ff">3&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> todo_max
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cur&lt;span style="color:#f92672">.&lt;/span>todo[&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> todo_max
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 把当前节点的懒标记下传给左右儿子&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">_spread&lt;/span>(self, node: int, l: int, r: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> todo &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>_tree[node]&lt;span style="color:#f92672">.&lt;/span>todo
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> todo &lt;span style="color:#f92672">==&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>_TODO_INIT: &lt;span style="color:#75715e"># 没有需要下传的信息&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m &lt;span style="color:#f92672">=&lt;/span> (l &lt;span style="color:#f92672">+&lt;/span> r) &lt;span style="color:#f92672">//&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_apply(node &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>, l, m, todo) &lt;span style="color:#75715e"># 下传给左儿子&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_apply(node &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, r, todo) &lt;span style="color:#75715e"># 下传给右儿子&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> todo[:] &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>_TODO_INIT[:] &lt;span style="color:#75715e"># 清空当前节点的懒标记&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 合并左右儿子的 val 到当前节点的 val&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">_maintain&lt;/span>(self, node: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_tree[node]&lt;span style="color:#f92672">.&lt;/span>val &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>_merge_val(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_tree[node &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>]&lt;span style="color:#f92672">.&lt;/span>val,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_tree[node &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>]&lt;span style="color:#f92672">.&lt;/span>val
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 初始化线段树&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 时间复杂度 O(n)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">_build&lt;/span>(self, node: int, l: int, r: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_tree[node]&lt;span style="color:#f92672">.&lt;/span>val &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">4&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_tree[node]&lt;span style="color:#f92672">.&lt;/span>todo &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>_TODO_INIT[:]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> l &lt;span style="color:#f92672">==&lt;/span> r: &lt;span style="color:#75715e"># 叶子&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m &lt;span style="color:#f92672">=&lt;/span> (l &lt;span style="color:#f92672">+&lt;/span> r) &lt;span style="color:#f92672">//&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_build(node &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>, l, m) &lt;span style="color:#75715e"># 初始化左子树&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_build(node &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, r) &lt;span style="color:#75715e"># 初始化右子树&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_maintain(node)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">_update&lt;/span>(self, node: int, l: int, r: int, ql: int, qr: int, f: Tuple[int, int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> ql &lt;span style="color:#f92672">&amp;lt;=&lt;/span> l &lt;span style="color:#f92672">and&lt;/span> r &lt;span style="color:#f92672">&amp;lt;=&lt;/span> qr: &lt;span style="color:#75715e"># 当前子树完全在 [ql, qr] 内&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_apply(node, l, r, f)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_spread(node, l, r)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m &lt;span style="color:#f92672">=&lt;/span> (l &lt;span style="color:#f92672">+&lt;/span> r) &lt;span style="color:#f92672">//&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> ql &lt;span style="color:#f92672">&amp;lt;=&lt;/span> m: &lt;span style="color:#75715e"># 更新左子树&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_update(node &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>, l, m, ql, qr, f)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> qr &lt;span style="color:#f92672">&amp;gt;&lt;/span> m: &lt;span style="color:#75715e"># 更新右子树&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_update(node &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, r, ql, qr, f)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_maintain(node)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">_query&lt;/span>(self, node: int, l: int, r: int, ql: int, qr: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> List[int]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> ql &lt;span style="color:#f92672">&amp;lt;=&lt;/span> l &lt;span style="color:#f92672">and&lt;/span> r &lt;span style="color:#f92672">&amp;lt;=&lt;/span> qr: &lt;span style="color:#75715e"># 当前子树完全在 [ql, qr] 内&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>_tree[node]&lt;span style="color:#f92672">.&lt;/span>val
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_spread(node, l, r)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m &lt;span style="color:#f92672">=&lt;/span> (l &lt;span style="color:#f92672">+&lt;/span> r) &lt;span style="color:#f92672">//&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> qr &lt;span style="color:#f92672">&amp;lt;=&lt;/span> m: &lt;span style="color:#75715e"># [ql, qr] 在左子树&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>_query(node &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>, l, m, ql, qr)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> ql &lt;span style="color:#f92672">&amp;gt;&lt;/span> m: &lt;span style="color:#75715e"># [ql, qr] 在右子树&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>_query(node &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, r, ql, qr)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> l_res &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>_query(node &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>, l, m, ql, qr)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> r_res &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>_query(node &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, r, ql, qr)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>_merge_val(l_res, r_res)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">_find_last&lt;/span>(self, node: int, l: int, r: int, ql: int, qr: int, f: Callable[[List[int]], int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> l &lt;span style="color:#f92672">&amp;gt;&lt;/span> qr &lt;span style="color:#f92672">or&lt;/span> r &lt;span style="color:#f92672">&amp;lt;&lt;/span> ql &lt;span style="color:#f92672">or&lt;/span> &lt;span style="color:#f92672">not&lt;/span> f(self&lt;span style="color:#f92672">.&lt;/span>_tree[node]&lt;span style="color:#f92672">.&lt;/span>val):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> l &lt;span style="color:#f92672">==&lt;/span> r:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> l
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_spread(node, l, r)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m &lt;span style="color:#f92672">=&lt;/span> (l &lt;span style="color:#f92672">+&lt;/span> r) &lt;span style="color:#f92672">//&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> idx &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>_find_last(node &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, m &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, r, ql, qr, f) &lt;span style="color:#75715e"># 优先查右子树&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> idx &lt;span style="color:#f92672">&amp;lt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> idx &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>_find_last(node &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>, l, m, ql, qr, f) &lt;span style="color:#75715e"># 右子树没有再查左子树&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> idx
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 用 f 更新 [ql, qr] 中的每个 a[i]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 0 &amp;lt;= ql &amp;lt;= qr &amp;lt;= n-1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 时间复杂度 O(log n)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">update&lt;/span>(self, ql: int, qr: int, f: Tuple[int, int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>_update(&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>, self&lt;span style="color:#f92672">.&lt;/span>_n &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, ql, qr, f)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 返回用 _merge_val 合并所有 a[i] 的计算结果，其中 i 在闭区间 [ql, qr] 中&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 0 &amp;lt;= ql &amp;lt;= qr &amp;lt;= n-1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 时间复杂度 O(log n)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">query&lt;/span>(self, ql: int, qr: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> List[int]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>_query(&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>, self&lt;span style="color:#f92672">.&lt;/span>_n &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, ql, qr)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 返回 [ql, qr] 内最后一个满足 f 的下标&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 0 &amp;lt;= ql &amp;lt;= qr &amp;lt;= n-1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 时间复杂度 O(log n)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">find_last&lt;/span>(self, ql: int, qr: int, f: Callable[[List[int]], int]) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>_find_last(&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>, self&lt;span style="color:#f92672">.&lt;/span>_n &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, ql, qr, f)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&amp;ldquo;注意看 &lt;code>_ apply&lt;/code> 方法。&amp;ldquo;老勇者指着代码，&amp;quot;&lt;code>todo_min &amp;gt;= 0&lt;/code> 表示要把这个区间的最小值设为 &lt;code>todo_min&lt;/code>。那么 &lt;code>sum_min = todo_min × 区间长度&lt;/code>，同时设置 &lt;code>l_min = todo_min&lt;/code> 和懒标记 &lt;code>todo[0] = todo_min&lt;/code>。&lt;code>_spread&lt;/code> 方法负责把懒标记下传给子节点，然后清空当前节点的标记。&amp;rdquo;&lt;/p>
&lt;p>我感觉脑子有点转不过来：&amp;ldquo;所以下传的时候，是把父节点的懒标记应用到子节点上，然后父节点的标记清零？这就像是老板把任务分配给下属，然后自己装作什么都没发生过？&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;差不多。&amp;ldquo;老勇者点点头，&amp;ldquo;但要注意，下传之前要检查懒标记是否是初始值（这里是 &lt;code>[-1, -1]&lt;/code>）。如果是初始值，说明没有待下传的任务，直接返回。&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;还有 &lt;code>_merge_val&lt;/code> 方法，合并左右儿子的信息时，&lt;code>sum_min&lt;/code> 和 &lt;code>sum_max&lt;/code> 直接相加，因为要统计总和；&lt;code>l_min&lt;/code> 和 &lt;code>l_max&lt;/code> 取左儿子的值——注意：这里有个前提——只有在区间被统一赋值时，&lt;code>l_min&lt;/code>/&lt;code>l_max&lt;/code> 才有意义，合并时实际上只用叶子节点的真实值。&amp;rdquo;&lt;/p>
&lt;p>我盯着那段 &lt;code>find_last&lt;/code> 的代码，感觉脑子快要烧了：&amp;ldquo;这个 &lt;code>find_last&lt;/code> 为什么优先查右子树？&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;因为要找&amp;rsquo;最后一个&amp;rsquo;满足条件的位置。&amp;ldquo;老勇者解释道，&amp;ldquo;线段树的中序遍历是从左到右，所以右子树的下标更大。优先查右子树，如果右子树里有满足条件的，就直接返回；如果没有，再查左子树。这样保证找到的是最右边的那个。另外，注意看它开头的判断：&lt;code>if l &amp;gt; qr or r &amp;lt; ql or not f(self._tree[node].val) &lt;/code>。如果当前节点的 val 都不满足&lt;code>条件 f&lt;/code>，那它的子节点肯定也不满足，直接剪枝返回 -1。这是线段树查询的经典优化。&amp;rdquo;&lt;/p>
&lt;p>接着，他写下了主逻辑：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Lazy 线段树&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>t &lt;span style="color:#f92672">=&lt;/span> LazySegmentTree(n)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cnt &lt;span style="color:#f92672">=&lt;/span> s &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i, x &lt;span style="color:#f92672">in&lt;/span> enumerate(nums):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> t&lt;span style="color:#f92672">.&lt;/span>update(left_less_eq[i] &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, i, (x, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>)) &lt;span style="color:#75715e"># 更新最小值&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> t&lt;span style="color:#f92672">.&lt;/span>update(left_great_eq[i] &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, i, (&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, x)) &lt;span style="color:#75715e"># 更新最大值&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 找到最后一个使得 最大值-最小值 &amp;gt;= low_d 的左端点&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> l &lt;span style="color:#f92672">=&lt;/span> t&lt;span style="color:#f92672">.&lt;/span>find_last(&lt;span style="color:#ae81ff">0&lt;/span>, i, &lt;span style="color:#66d9ef">lambda&lt;/span> v: v[&lt;span style="color:#ae81ff">3&lt;/span>] &lt;span style="color:#f92672">-&lt;/span> v[&lt;span style="color:#ae81ff">2&lt;/span>] &lt;span style="color:#f92672">&amp;gt;=&lt;/span> low_d)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> l &lt;span style="color:#f92672">&amp;gt;=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cnt &lt;span style="color:#f92672">+=&lt;/span> l &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 有 l+1 个子数组满足条件&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> d &lt;span style="color:#f92672">=&lt;/span> t&lt;span style="color:#f92672">.&lt;/span>query(&lt;span style="color:#ae81ff">0&lt;/span>, l) &lt;span style="color:#75715e"># 查询这些子数组的 sum_min 和 sum_max&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> s &lt;span style="color:#f92672">+=&lt;/span> d[&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">-&lt;/span> d[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#75715e"># 极差之和 = sum_max - sum_min&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> s &lt;span style="color:#f92672">-&lt;/span> (cnt &lt;span style="color:#f92672">-&lt;/span> k) &lt;span style="color:#f92672">*&lt;/span> low_d &lt;span style="color:#75715e"># 减掉多算的&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&amp;ldquo;这就是全部流程。&amp;ldquo;老勇者一边写一边解释，&amp;ldquo;每次 i 往右移一位，先用单调栈确定地盘，再用线段树做两次区间赋值——把 &lt;code>(left_less_eq[i], i]&lt;/code> 的最小值刷成 &lt;code>nums[i]&lt;/code>，把&lt;code>(left_greater_eq[i], i]&lt;/code> 的最大值刷成 &lt;code>nums[i]&lt;/code>。更新完之后，在线段树上用 &lt;code>find_last&lt;/code> 在区间 &lt;code>[0, i]&lt;/code> 内从右往左找到最后一个满足&lt;strong>最大值 - 最小值 ≥ low_d&lt;/strong>的左端点 l。&amp;rdquo;&lt;/p>
&lt;p>他顿了顿，补了一句：&amp;quot;&lt;code>find_last&lt;/code> 就是线段树上做一次二分——从右往左找最后一个满足条件的叶子位置。因为极差随左端点左移单调不减，区间越长，极差越大，所以找到最后一个过线的左端点 l，&lt;code>[0, l]&lt;/code> 内的就全过线了。直接区间查询 &lt;code>[0, l]&lt;/code> 的 &lt;code>sum_max - sum_min&lt;/code>，累加进答案 s，数量 cnt 累加 l+1。最后，二分阶段确定的 &lt;code>low_d&lt;/code> 是第 k 大极差，实际统计的过线数量 cnt 可能超过 k，多出来的那些极差都等于 &lt;code>low_d&lt;/code>，答案减去 &lt;code>(cnt - k) * low_d&lt;/code> 收工。&amp;rdquo;&lt;/p>
&lt;hr>
&lt;h4 id="终章封印解除">终章：封印解除
&lt;/h4>&lt;p>我瘫在椅子上，盯着那张画满二叉树、懒标记和单调栈的草稿纸，感觉自己的脑细胞像被矿脉采掘过一样，空了又满，满了又空。&lt;/p>
&lt;p>&amp;ldquo;让我理一理……&amp;rdquo; 我扶着额头，把刚才那一整套流程在脑子里重新串了一遍：&amp;rdquo;&lt;/p>
&lt;ol>
&lt;li>&lt;strong>二分答案&lt;/strong>：定位第 k 大的极差值 &lt;code>low_d&lt;/code>，把选择问题降维成判定问题——我不需要知道具体是哪 k 个子数组，只需要知道第 k 大的极差是多少&lt;/li>
&lt;li>&lt;strong>滑动窗口 + 单调队列&lt;/strong>：在 check 函数里统计极差 ≥ X 的子数组数量，用双端队列维护窗口的最大最小值，O(n) 完成判定&lt;/li>
&lt;li>&lt;strong>单调栈预处理&lt;/strong>：给每个元素划好地盘，确定每个元素作为最小值/最大值的影响范围——left_less_eq 和 left_great_eq 就是它们的地契&lt;/li>
&lt;li>&lt;strong>懒标记线段树&lt;/strong>：动态维护每个左端点对应的子数组的极值，快速统计过线子数组的极差之和——区间赋值用懒标记 O(log n)，区间查询直接报总数 O(log n)&amp;rdquo;&lt;/li>
&lt;/ol>
&lt;p>老勇者欣慰地点点头，像是在看一个终于学会走路的孩子：&amp;ldquo;现在你来算算复杂度——&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;二分答案 &lt;code>O(log(max_val))&lt;/code>，每次 check &lt;code>O(n)&lt;/code>，单调栈 &lt;code>O(n)&lt;/code>，线段树 &lt;code>O(n log n)&lt;/code>。总复杂度 &lt;code>O(n log n + n log(max_val))&lt;/code>，跟 &lt;code>k&lt;/code> 无关！&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;这是真正的优化。&amp;ldquo;老勇者缓缓地站起身，火光映射下，他的身影就像超人一样，&amp;ldquo;昨天的堆 + 稀疏表，复杂度跟 k 线性相关；今天的二分答案 + 线段树，完全摆脱了 k 的束缚。当 k 很大时，这就是降维打击。&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;不过——&amp;rdquo; 他的话音严肃了起来，&amp;ldquo;今天的解法虽然复杂度更优，但代码量是昨天的三倍，调试难度也是指数级上升。在实际面试中，除非面试官明确要求&amp;rsquo;优化掉 k&amp;rsquo;，否则昨天的堆版本就足够了。别为了炫技把自己坑了。&amp;rdquo;&lt;/p>
&lt;p>我目送老勇者的背影消失在村口，然后低头看了看草稿纸上那密密麻麻的四重封印——二分答案、滑动窗口、单调栈、线段树，每一个单独拎出来都能撑起一道中等难度的题，今天它们被串成一条流水线，就为了从一个十万长度的数组里精准捞出前 k 大极差。&lt;/p>
&lt;p>炫技？我连把这套代码完整写出来不报错都够呛，还炫技。脑细胞都快集体罢工了好吧。&lt;/p>
&lt;p>我小心翼翼地把画满老勇者笔迹的草稿纸收进我的魔法书里，想起老勇者的话，在旁边加了一小行：能用一行流解决的问题，别上 DP；能用堆解决的问题，别上线段树。但如果约束真的逼到了那一步——也别怂，四重封印该解就解，大不了多薅些头发。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3689 贪心策略的两种命运：可重复选vs不可重复选</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-09/</link><pubDate>Tue, 09 Jun 2026 17:16:07 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-09/</guid><description>&lt;h2 id="-贪心策略的两种命运可重复选vs不可重复选">🎮 贪心策略的两种命运：可重复选vs不可重复选
&lt;/h2>&lt;blockquote>
&lt;p>能摆的时候别卷，该卷的时候别摆&lt;/p>&lt;/blockquote>
&lt;p>村长又来信了。这次竟然是让村里的老黄牛慢悠悠驮过来的——我怀疑村长把信鸽的口粮预算都给砍了，这老头在省钱的路上已经走火入魔。我展开纸条一看：“ 小白，勇士团下周开荒新副本，出发前要做战力波动评估！规则如下：给一个数组 &lt;code>nums&lt;/code> 和一个整数 &lt;code>k&lt;/code>，从中选&lt;strong>恰好 &lt;code>k&lt;/code> 个非空子数组。子数组可以重叠，同一个子数组可以被选择超过一次&lt;/strong>。每个子数组的值 = 最大值 - 最小值，求最大可能的总值。”&lt;/p>
&lt;p>我盯着“可以重叠”和“可以被选择超过一次”这两行字，手里的拨火棍差点掉进锅炉里。这不就是明摆着让我薅羊毛吗？整个数组就是天然的极差冠军——既然同一段能刷 k 遍，那就把极差冠军往死里薅——就像在游戏里找到经验值最高的怪，然后挂机刷 k 次，连走位都省了。&lt;/p>
&lt;pre tabindex="0">&lt;code>return (max(nums) - min(nums)) * k
&lt;/code>&lt;/pre>&lt;p>一行流，三十秒 AC，我甚至有空给自己泡了杯茶。茶杯端到嘴边，我忽然顿住了——不对劲。非常不对劲。&lt;/p>
&lt;p>我见过太多副本了——加一个约束，难度翻倍；砍一个约束，难度减半。但这题居然专门强调“子数组可以重叠”“同一个子数组可以反复选”——也就是说，“子数组之间不能重叠”和“每个子数组只能选一次”这两个最要命的约束全被砍了，直接退化成一个极值问题，难度标签形同虚设。这不是放水，这是泄洪啊。&lt;/p>
&lt;p>事出反常必有妖。我正盯着屏幕发呆，后脑勺&amp;quot;啪&amp;quot;地挨了一下。不重不轻，力道精准得像调试过阈值的触发器。果然又是老勇者，手指还保持着弹脑瓜崩的姿势，“AC 了还发呆？不像你啊——”&lt;/p>
&lt;p>“喂，大师，下次咱换个出场方式行吗？” 我揉着后脑勺嘟囔道，“这道题简单到诡异啊。” 我把题目指给他看，“‘可以重叠’、‘可以反复选’——约束全砍了，一行代码就搞定，策划是不是被盗号了？”&lt;/p>
&lt;p>老勇者笑了：“策划没被盗号，他这是在告诉你——贪心的极致就是摆烂。既然能无限次选择最优解，就别整那些花里胡哨的，直接把最优解复制 k 份。你想想，如果连摆烂的机会都不给你，直接上硬核约束，你连这个一行流的心得都悟不到，直接被困难版锤进地里。”&lt;/p>
&lt;p>“……所以，策划用心良苦就是为了教我读题？”&lt;/p>
&lt;p>”能看穿什么时候该摆烂，也是一种本事。不过——&amp;quot;&lt;/p>
&lt;p>老勇者话锋一转，眼神突然变得危险起来，&amp;ldquo;如果加上约束，阁下又将如何应对？&amp;rdquo;&lt;/p>
&lt;p>他伸手拿起那张皱巴巴的羊皮纸，掏出随身携带的红笔——那个瞬间我甚至怀疑他是不是专门等着改我的题目——在纸上画了画，然后又递回给我。我低头一看：&lt;/p>
&lt;p>“选择恰好 k 个&lt;strong>不同&lt;/strong>的非空子数组。子数组可以重叠，同一个子数组&lt;strong>不可以&lt;/strong>被选择超过一次。”&lt;/p>
&lt;p>我倒吸一口凉气，手里的茶杯差点跟着羊皮纸一块儿掉进锅炉里。“不同的”非空子数组，“同一个子数组不能被选择超过一次”——约束一加，难度飞天。刚才一行流抓最肥的羊反复薅，现在每只羊只让薅一次，这不得挑出前k个最肥的羊？&lt;/p>
&lt;p>我开始盘算：直接上暴力把所有可能的子数组全列出来，算极差，排序，取前 k 大。但长度为 n 的数组，子数组数量 n(n+1)/2，O(n²) 的候选集，n 大了直接炸，评测机怕是要给我唱一首《超时咏》。&lt;/p>
&lt;p>“等等——题目只改了‘不能重复选同一段’，没禁止段之间重叠！！” 我拿着笔在纸上戳了戳，“既然可以重叠，那我不需要担心选了一段之后堵死其他段的问题。整个数组的极差max - min是冠军，只能用一次。把它用掉之后，第二大极差要么是抠掉最大值后剩下的区间，要么是抠掉最小值后剩下的区间——把最值抠掉，极差才可能变小。然后，季军就是从亚军的残骸里继续抠最值——”&lt;/p>
&lt;p>我一拍脑门，“这不就是一个贪心扩展的过程吗？搞个大根堆维护候选池，每次挑极差最大的区间用掉，然后把它‘切开’——抠掉最大值或最小值之后，剩下的子区间重新扔回候选池里。重复 k 次，累加极差，搞定。大根堆！这可是高级数据结构，这把稳了。”&lt;/p>
&lt;p>“方向是对的。”老勇者点点头，然后毫不留情地浇了一盆冷水，“但你的刀法有问题——候选池里的区间数量会指数爆炸，你这不叫堆，叫炸弹。想象一下，每次切一下产生两个新区间，k 次就是 2^k 个候选，你的堆会变成二叉树的坟场。&amp;quot;&lt;/p>
&lt;p>我刚刚燃烧起来的小宇宙瞬间被浇灭，整个人像被触发了 NullPointerException 一样瘫在椅子上。大根堆都救不了我，那还有什么能救？&lt;/p>
&lt;p>老勇者撇了我一眼，拿起笔在草稿纸上画了一条长线，标上刻度 0 到 n。“换个角度。别再跟切分死磕了。任意一个子数组都由两个东西决定——左端点 l 和右端点 r。固定 l，极差随 r 怎么变？”&lt;/p>
&lt;p>我想了想：“r 越往右，区间越长，最大值只会变大或不变，最小值只会变小或不变——所以极差只增不减？”&lt;/p>
&lt;p>“对。“老勇者在纸上画了几条平行的隧道，每条从不同起点出发，箭头齐刷刷指向右边，“”这就像玩一个挖矿游戏——固定起点，隧道越长，挖到的宝藏价值差越大。“&lt;/p>
&lt;p>”初始地图上有 n 个起点，你站在每个起点 i，把每条隧道都从起点一路挖到最深处 [i, n)，把这 n 个候选段全扔进一个最大堆里。弹出堆顶——当前极差最大的区间——累加价值差进答案，然后把当前隧道在深度上缩一格，也就是把右端点从 r 减到 r-1，再压回堆里作为新的候选。如此重复 k 次即可。&amp;quot;&lt;/p>
&lt;p>我盯着那排箭头：“等等——所以不是切碎区间？而是固定左端点，只缩短右端点？让堆里永远只有 n 个候选？！”&lt;/p>
&lt;p>“对。每次弹出 [l, r)，压回 [l, r-1)。候选数恒定 n 个，不会指数爆炸。而且对于固定 l，极差随 r 单调不减，每次取堆顶一定是全局最优，贪心正确性有保证——这就是&amp;rsquo;隧道模型&amp;rsquo;，只在一个方向缩，不往两个方向切。&amp;quot;&lt;/p>
&lt;p>“但每次缩一格要重新扫一遍极差不还是 O(n)，这是把爆炸从堆里挪到了查询里吗？&amp;ldquo;我皱着眉头，感觉这个方案还是有致命伤。&lt;/p>
&lt;p>老勇者嘴角勾起一抹神秘的微笑：&amp;ldquo;所以你需要一件神器——能让任意区间的极差查询在 O(1) 时间内完成的神器。&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;O(1)？！&amp;ldquo;我差点从椅子上跳起来，&amp;ldquo;难道不用遍历就能知道最大值和最小值？这违反了信息论吧？在每个位置都装个预知未来的水晶球？&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;差不多。&amp;ldquo;老勇者真的从怀里掏出了一块……石板？上面刻满了密密麻麻的符文，&amp;ldquo;这叫稀疏表（Sparse Table），上古时代用来快速查询区间最值的魔法阵。核心思想很简单——提前把所有可能用到的区间的最值都算好存起来，用的时候直接查表。&amp;rdquo;&lt;/p>
&lt;p>他抬手在空中划出一串发光的代码，稀疏表像闪亮的星星一样悬在半空：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">op&lt;/span>(a, b):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> (min(a[&lt;span style="color:#ae81ff">0&lt;/span>], b[&lt;span style="color:#ae81ff">0&lt;/span>]), max(a[&lt;span style="color:#ae81ff">1&lt;/span>], b[&lt;span style="color:#ae81ff">1&lt;/span>]))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">ST&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> __init__(self, a):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n &lt;span style="color:#f92672">=&lt;/span> len(a)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> w &lt;span style="color:#f92672">=&lt;/span> n&lt;span style="color:#f92672">.&lt;/span>bit_length()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> st &lt;span style="color:#f92672">=&lt;/span> [[&lt;span style="color:#66d9ef">None&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> n &lt;span style="color:#66d9ef">for&lt;/span> _ &lt;span style="color:#f92672">in&lt;/span> range(w)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> st[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> [(x, x) &lt;span style="color:#66d9ef">for&lt;/span> x &lt;span style="color:#f92672">in&lt;/span> a]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, w):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(n &lt;span style="color:#f92672">-&lt;/span> (&lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">&amp;lt;&amp;lt;&lt;/span> i) &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> st[i][j] &lt;span style="color:#f92672">=&lt;/span> op(st[i&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>][j], st[i&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>][j &lt;span style="color:#f92672">+&lt;/span> (&lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">&amp;lt;&amp;lt;&lt;/span> (i&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>))])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>st &lt;span style="color:#f92672">=&lt;/span> st
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 查询 [l, r) 左闭右开，返回极差&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">query&lt;/span>(self, l, r):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> k &lt;span style="color:#f92672">=&lt;/span> (r &lt;span style="color:#f92672">-&lt;/span> l)&lt;span style="color:#f92672">.&lt;/span>bit_length() &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mn, mx &lt;span style="color:#f92672">=&lt;/span> op(self&lt;span style="color:#f92672">.&lt;/span>st[k][l], self&lt;span style="color:#f92672">.&lt;/span>st[k][r &lt;span style="color:#f92672">-&lt;/span> (&lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">&amp;lt;&amp;lt;&lt;/span> k)])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> mx &lt;span style="color:#f92672">-&lt;/span> mn
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&amp;ldquo;等等等等，&amp;ldquo;我被那一堆位运算晃得眼晕，&amp;ldquo;这玩意儿怎么看都像黑魔法……1 &amp;laquo; i 是什么鬼？&amp;rdquo;&lt;/p>
&lt;p>老勇者叹了口气，拿起笔在石板上画了个示意图：&amp;ldquo;别被位运算吓到。我给你翻译成人话——&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;第一层魔法：预处理所有 2 的幂次长度的区间。你看 st[0] 存的是长度为 1 的区间（就是单个元素），st[1] 存的是长度为 2 的区间，st[2] 存的是长度为 4 的区间，以此类推。1 &amp;laquo; i 就是 2^i，左移一位相当于乘以 2。&amp;rdquo;&lt;/p>
&lt;p>他在纸上画了一排格子：&lt;/p>
&lt;blockquote>
&lt;p>原始数组: [1, 3, 2, 5, 4]&lt;/p>
&lt;p>st&lt;a class="link" href="%e9%95%bf%e5%ba%a61" > 0 &lt;/a>: [(1,1), (3,3), (2,2), (5,5), (4,4)]
st&lt;a class="link" href="%e9%95%bf%e5%ba%a62" >1&lt;/a>: [(1,3), (2,3), (2,5), (4,5)]
st&lt;a class="link" href="%e9%95%bf%e5%ba%a64" >2&lt;/a>: [(1,5), (2,5)]&lt;/p>&lt;/blockquote>
&lt;p>&amp;quot; st[ i ] [ j ] 表示从位置 j 开始、长度为 2^i 的区间的 (最小值, 最大值)。关键在于——长区间可以由两个短区间拼出来。比如，长度为 4 的区间 [j, j+4) 可以拆成两个长度为 2 的区间 [j, j+2) 和 [j+2, j+4)，取它们的 min 和 max 就行。这就是 op 函数的作用——合并两个区间的极值。&amp;rdquo;&lt;/p>
&lt;p>我盯着那个递推式 st[ i ] [ j ] = op(st[ i-1 ] [ j ] , st[ i-1 ] [ j + (1 &amp;laquo; (i-1)) ])：&amp;ldquo;所以这是先把短的算好了，长的直接拼？&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;没错。ST 表预处理的复杂度是 O(n log n)，因为最多有 log n 层，每层最多 n 个元素。&amp;rdquo; 老勇者指着 query 方法，&amp;ldquo;真正的魔法在这里——怎么在 O(1) 时间内查询任意长度的区间？&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;假设我要查 [l, r)，长度是 len = r - l。我们需要找到不超过 len 的最大的 2 的幂次。比如 len=5，最大的幂次是 4（也就是 2^2），然后用两个长度为 4 的区间覆盖 [l, r)——一个从左边开始 [l, l+4)，一个从右边结束 [r-4, r)。这两个区间一定有重叠，但没关系，我们要的是最大值和最小值，重叠部分不影响结果。&amp;rdquo;&lt;/p>
&lt;p>他在纸上画了个覆盖示意图：&lt;/p>
&lt;blockquote>
&lt;p>数组: [1, 3, 2, 5, 4, 6, 3]
|&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;| ← [l, l+4) 长度4
|&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;| ← [r-4, r) 长度4
|&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;| ← [l, r) 长度5&lt;/p>&lt;/blockquote>
&lt;p>&amp;ldquo;k = (r - l).bit_length() - 1 就是在找这个最大的幂次。op(self.st[ k ] [ l ], self.st[ k ] [ r - (1 &amp;laquo; k) ]) 就是把两个区间拼起来，返回 (最小值, 最大值)，相减就是极差。&amp;rdquo;&lt;/p>
&lt;p>我感觉脑子里有什么东西突然连通了：&amp;ldquo;所以稀疏表就是提前背好了所有标准长度（1, 2, 4, 8, 16&amp;hellip;）的区间攻略，遇到任何长度的查询，就用两个标准区间拼出来？这确实是 O(1)，因为只需要查两次表再加一次合并！&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;对。不过要注意，稀疏表只能处理静态数组的区间查询，不能修改元素。如果要支持修改，就得用线段树或者树状数组。但这道题不需要修改，所以稀疏表是最优解。&amp;rdquo;&lt;/p>
&lt;p>老勇者接着写下了主逻辑：初始化堆的时候，把每个起点 i 对应的最长隧道 [i, n) 的极差塞进去。因为隧道长度的缩减只会导致极差变小或不变，初始堆已经满足大根堆性质，连 heapify 都省了。循环 k 次，每次弹出堆顶，累加极差，然后把同一隧道缩一格 [l, r-1) 压回去。如果堆顶的极差已经等于 0，说明剩下的都是极差为 0 的区间，可以直接收工。&lt;/p>
&lt;pre tabindex="0">&lt;code>def maxTotalValue(self, nums, k):
n = len(nums)
st = ST(nums)
# 最大堆: (极差, l, r)，初始每条隧道挖到最深处
h = [(st.query(i, n), i, n) for i in range(n)]
ans = 0
for _ in range(k):
d, l, r = h[0]
if d == 0: # 极差为 0，收工
break
ans += d
# 弹出堆顶，同一隧道缩一格再压回去
heapreplace_max(h, (st.query(l, r-1), l, r-1))
return ans
&lt;/code>&lt;/pre>&lt;p>我盯着这段代码，感觉像在看一个动态的挖矿图。堆里始终只有 n 个候选——每条矿脉一个。每次取走堆顶收益最大的矿段，同一条矿脉往浅层退一格，重新排队。稀疏表像一台地质探测仪，O(1) 就能查任意矿段的价值差。ST 表预处理O(n log n)，查询区间最值 O(1)。堆操作一次 O(log n)，k 次 O(k log n)。总复杂度 O(n log n + k log n)，稳稳扛住十万级。&lt;/p>
&lt;p>&amp;ldquo;隧道模型 + 贪心堆 + 稀疏表，&amp;ldquo;我靠在椅背上，把这三个词在嘴里嚼了一遍，&amp;ldquo;固定一端，单调扩展，堆维护全局最优。极差查询交给 ST 表。这比我那套切分方案强太多了——那是中间开花，指数爆炸；这是单端收缩，候选数恒定。同一个贪心思路，区别这么大。&amp;rdquo;&lt;/p>
&lt;p>老勇者拍了拍我的肩膀，转身向门口走去：&amp;ldquo;固定一端、贪心另一端，这种&amp;rsquo;隧道模型&amp;rsquo;在很多区间选 k 大的题里都能用。算法优化往往就是换个角度看问题，消除不必要的计算。记住，当你发现候选集要爆炸的时候，问问自己：能不能固定一个维度，只让另一个维度动？&amp;rdquo;&lt;/p>
&lt;p>走到门口，他顿了顿，回头补了一句，眼神里闪烁着某种兴奋的光芒：&amp;ldquo;下次要是再加个约束——&amp;lsquo;子数组不能重叠&amp;rsquo;的变种题，记得叫我——那时候，才是真正的战斗开始。动态规划的地狱之门，会为你敞开。&amp;rdquo;&lt;/p>
&lt;p>我打了个寒颤，默默保存了今天的代码，顺手给文件加了个备份——万一哪天真的要用到呢？毕竟，在这个充满约束的世界里，唯一不变的就是变化本身。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode2161 装备分拣：一行流的诱惑与分桶的正道</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-08/</link><pubDate>Mon, 08 Jun 2026 12:47:26 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-08/</guid><description>&lt;h2 id="-装备分拣一行流的诱惑与分桶的正道">🎮 装备分拣：一行流的诱惑与分桶的正道
&lt;/h2>&lt;p>“小白！”村长一脚踹开大门，怀里抱着一堆闪闪发光的装备，“昨晚勇士团连夜刷副本，爆了一地装备！你看这战力值——&lt;code>[9, 12, 5, 10, 14, 3, 10]&lt;/code>。现在要以战力 10 为基准分三批：比 10 弱的给新手村，等于 10 的放仓库备用，比 10 强的给精英团。而且同一批装备必须按掉落顺序排列——这样好查记录，知道是哪只 BOSS 爆的！”&lt;/p>
&lt;p>我接过装备清单扫了一眼，这不就是稳定三路划分吗？快排里的 partition 一个原地交换下去，相同元素的先后顺序直接乱套——这里要求组内保序，那就不能原地搞了，得上辅助空间。三个桶，一趟遍历：比 pivot 小的进左桶，相等的进中桶，大的进右桶。从左往右依次遍历，每个桶天然保持原数组的先后顺序。&lt;/p>
&lt;p>正准备掏分桶代码，我脑子突然一抽：等等，Python 的 &lt;code>sort&lt;/code> 不是稳定排序吗？Timsort 亲口承诺“相同键值保序”，那我直接用 &lt;code>key&lt;/code> 函数把小于 pivot 的映射成 -1，等于的映射成 0，大于的映射成 1，然后排一遍不就行了？一行流，帅就完事。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>nums&lt;span style="color:#f92672">.&lt;/span>sort(key&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">lambda&lt;/span> x: (x &lt;span style="color:#f92672">&amp;gt;&lt;/span> pivot) &lt;span style="color:#f92672">-&lt;/span> (x &lt;span style="color:#f92672">&amp;lt;&lt;/span> pivot))
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>跑样例，输出完美。提交，AC 弹出。我往椅背上一靠，翘起二郎腿，心里已经开始给自己加戏——一行代码搞定装备分拣，村长直呼内行，精英团连夜给我加 Buff，仓库管理员感动得要把女儿许配给我。&lt;/p>
&lt;p>正美着，后脑勺“啪”地挨了一下。&lt;/p>
&lt;p>回头一看，老勇者叼着棒棒糖站在我身后，手指还保持着弹脑瓜崩的姿势。“你觉得这就算写完了？”&lt;/p>
&lt;p>“AC 了啊！”我捂着后脑勺，指着屏幕上的绿色大勾。&lt;/p>
&lt;p>“AC 不代表没毛病。”他点了点屏幕，“你这 &lt;code>sort&lt;/code>，时间复杂度多少？”&lt;/p>
&lt;p>“O(n log n)……”我声音小了一半，后脑勺还隐隐作痛。&lt;/p>
&lt;p>”题目让你分组，不是让你排序。分组是 O(n) 的活——看一眼战力值，扔进对应的箱子，完事。你调 &lt;code>sort&lt;/code>，等于雇了个全职排序师来当门卫。排序引擎不知道你只想分堆——它勤勤恳恳地比较、归并、插入，log n 层递归树跑满，就为了完成一个你早就知道答案的任务。而且 Timsort 底层 merge 要临时数组，额外空间也是 O(n)——你这一行流，时间复杂度输了，空间也没省。”&lt;/p>
&lt;p>他把棒棒糖往嘴角一叼：“一行流看着帅，语义不对。用排序引擎做分组，技术债，利息 O(n log n)。哪天副本掉了一百万件装备，Boss 都刷新两轮了你的代码还在排序，勇士团站门口干等，回头差评全写你名字。”&lt;/p>
&lt;p>我脸上的得意全部转移到了脚趾上。我默默地把那一行流注释掉，老老实实掏出了分桶三数组的写法：&lt;/p>
&lt;p>遍历整个装备列表，比&lt;code>pivot&lt;/code>弱的扔进 &lt;code>left&lt;/code> 箱发往新手村；相等的扔进 &lt;code>mid&lt;/code> 箱存仓库；比&lt;code>pivot&lt;/code>强的扔进 &lt;code>right&lt;/code> 箱送精英团。因为是从左往右按掉落顺序依次遍历的，每个箱子里的装备天然保持了原来的先后顺序，最后把三个箱子的装备按顺序拼接，分拣完毕：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">pivotArray&lt;/span>(self, nums: List[int], pivot: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> List[int]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> left, mid, right &lt;span style="color:#f92672">=&lt;/span> [], [], []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> x &lt;span style="color:#f92672">in&lt;/span> nums:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> x &lt;span style="color:#f92672">&amp;lt;&lt;/span> pivot:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> left&lt;span style="color:#f92672">.&lt;/span>append(x)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> x &lt;span style="color:#f92672">==&lt;/span> pivot:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mid&lt;span style="color:#f92672">.&lt;/span>append(x)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> right&lt;span style="color:#f92672">.&lt;/span>append(x)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> left &lt;span style="color:#f92672">+&lt;/span> mid &lt;span style="color:#f92672">+&lt;/span> right
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>O(n) 时间，O(n) 额外空间，语义清晰，逻辑直白。村长看了眼分好的三箱装备，满意地点点头，抱着箱子去仓库贴标签了。&lt;/p>
&lt;p>老勇者看着我把分桶版本提交上去，这次没弹脑瓜崩，点了点头：“记住，一行流能 AC 不代表它是最优解。&lt;code>sort&lt;/code> 是通用排序，它不知道你只想分堆。能现场分组的，就别排序。”&lt;/p>
&lt;p>我揉着后脑勺，默默记下了。但盯着那三个桶，还是忍不住追问了一句：“空间复杂度有没有优化空间？”&lt;/p>
&lt;p>老勇者瞥了我一眼：“你愿意拿时间换，就可以。”&lt;/p>
&lt;p>“原地交换加插入排序，能压到 O(1) 空间——每遇到一个元素，先判断它该去哪边，再用位移操作把它挪到对应位置，保证组内顺序不变。但每次挪位置都是 O(n)，总复杂度 O(n²)。O(1) 空间换 O(n²) 时间，等于徒手搬一百趟装备省一个空间戒指，血亏。”&lt;/p>
&lt;p>他把棒棒糖往嘴里一叼，拍了拍我肩膀：“编程跟打副本一个道理——理解机制比硬刚更重要。稳定分区必须付出代价，要么付空间，要么付时间。n ≤ 10⁵ 的数据量，O(n) 空间换 O(n) 时间是血赚。省空间的念头，留着对付那些不要求稳定性的题——那才是原地交换大展拳脚的地方。”&lt;/p>
&lt;p>窗外，勇士团扛着新到货的装备箱浩浩荡荡走过，村长跟在后面一路小跑，手里还抱着我分好的三摞清单。我把魔法书翻到今天这一页，在旁边批了三行：能分组的，就别排序。稳定分区，空间换时间是正道。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode2196：树根猎人：从一堆父子关系里揪出老祖宗</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-07/</link><pubDate>Sun, 07 Jun 2026 22:40:48 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-07/</guid><description>&lt;h2 id="-树根猎人从一堆父子关系里揪出老祖宗">🎮 树根猎人：从一堆父子关系里揪出老祖宗
&lt;/h2>&lt;p>一只灰扑扑的信鸽扑棱着翅膀从窗口栽了进来，精准地砸中了我刚泡好的方便面。我解开它腿上的字条一看——是村长的笔迹：&amp;ldquo;邻村要建棵树，你帮忙看看。&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;建树？&amp;ldquo;我一个刷算法的，什么时候接了土木工程的活儿？这委托费够不够赔我这碗红烧牛肉面！翻到字条背面，还有一整页三元组：&lt;code>[parent, child, isLeft]&lt;/code>。我恍然大悟，此树非彼树。邻村要建的是一棵家谱二叉树，但现在只有一堆零散的父子关系——不成树，也不知道根在哪。村长要我根据这些描述把树拼好，并找出根节点。&lt;/p>
&lt;p>&amp;ldquo;清官难断家务事，&amp;ldquo;我把字条往桌上一拍，露出职业性的微笑，&amp;ldquo;但算法能解父子局。&amp;rdquo;&lt;/p>
&lt;p>说白了，就是从一堆“谁是谁他爹”的八卦信息里重建完整的家族谱系。我理了理思路，这事分两步走：&lt;/p>
&lt;p>&lt;strong>第一步：补全亲属关系网&lt;/strong>&lt;br>
对着家谱名单，把每个人的直系亲属关系一条条补全——把每个节点造出来，顺便把父子指针挂上。这就像是给村里的每个人办身份证，然后记录他们的亲子关系。&lt;/p>
&lt;p>&lt;strong>第二步：找出老祖宗&lt;/strong>&lt;br>
那个只当爹、不当娃的根节点，就是我们要找的目标。在任一个三元组的 &lt;code>child&lt;/code> 位置上出现的都是娃，而只有&lt;strong>老祖宗&lt;/strong>——那个从来没当过子女的人——永远不会出现在 &lt;code>child&lt;/code> 位置上。所以策略很简单：把所有出现过的 &lt;code>child&lt;/code> 收集到一个集合里，再遍历所有节点，哪个不在 &lt;code>child&lt;/code> 集合里，哪个就是根。&lt;/p>
&lt;p>“这不就是玩‘谁是卧底’吗？”我自言自语，“所有人都有爹，就他没有——那他就是老祖宗。”&lt;/p>
&lt;p>我搓搓手，开始落代码，先准备两样东西：&lt;/p>
&lt;ol>
&lt;li>一个哈希表 &lt;code>nodes&lt;/code>：以节点值为 key，存节点对象。这就好比是村里的户口本，每个人都在这里登记。&lt;/li>
&lt;li>一个集合 &lt;code>children&lt;/code>：专门登记所有当过娃的节点，用来筛根。这就是我们的&amp;quot;有爹人士名单&amp;rdquo;。&lt;/li>
&lt;/ol>
&lt;pre tabindex="0">&lt;code>nodes = {}
children = set()
&lt;/code>&lt;/pre>&lt;p>接着，遍历 &lt;code>descriptions&lt;/code> 里的每一条 &lt;code>[parent, child, isLeft]&lt;/code>。对于 &lt;code>parent&lt;/code> 和 &lt;code>child&lt;/code>，先去 &lt;code>nodes&lt;/code> 里查一下：没有就当场 &lt;code>new&lt;/code> 一个 &lt;code>TreeNode&lt;/code> 塞进去。然后，根据 &lt;code>isLeft&lt;/code> 把 &lt;code>child&lt;/code> 挂到 &lt;code>parent&lt;/code> 左边或右边，父子关系定下来。最后把 &lt;code>child&lt;/code> 扔进 &lt;code>children&lt;/code> 集合，留个案底，证明这孩子有爹。&lt;/p>
&lt;pre tabindex="0">&lt;code>for parent, child, is_left in descriptions:
if parent not in nodes:
nodes[parent] = TreeNode(parent)
if child not in nodes:
nodes[child] = TreeNode(child)
if is_left:
nodes[parent].left = nodes[child]
else:
nodes[parent].right = nodes[child]
children.add(child)
&lt;/code>&lt;/pre>&lt;p>遍历跑完，整棵树的骨架就全部搭好了。接下来找根——那个唯一没出现在 &lt;code>children&lt;/code> 集合里的人：&lt;/p>
&lt;pre tabindex="0">&lt;code>for x, node in nodes.items():
if x not in children:
return node
&lt;/code>&lt;/pre>&lt;p>一趟主循环把建节点、挂关系、登记子女全部搞定，最后再来一趟小循环抓老祖。总复杂度 O(n)，线性扫描，丝滑流畅。&lt;/p>
&lt;p>村长很快就过来要结果了。他戴着老花镜，眯着眼扫了下代码，点了点头：&amp;ldquo;哈希表挂节点这种基操就不夸你了——能想到把找根的线索嵌在建节点的同一个循环里，边建边登记，最后差集一把梭，说明你开始有‘数据处理流水线’的意识了。&amp;rdquo;&lt;/p>
&lt;p>他把字条翻了个面，确认背面没有隐藏附加条件，满意地往怀里一揣：&amp;ldquo;行了，邻村还等着这棵树回去祭祖呢。对了，他们说要感谢你，送了你一袋土豆。&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;……土豆？&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;嗯，说是自家种的，纯天然无污染。&amp;rdquo;&lt;/p>
&lt;p>我看了看桌上那碗被信鸽毁掉的方便面，又想了想这趟委托的收入，叹了口气。算了，至少不是 Bug。&lt;/p>
&lt;p>送走村长后，我拆开那袋土豆，发现下面还压着一张小纸条：&lt;/p>
&lt;p>&lt;em>&amp;ldquo;多谢相助。另：如果你愿意帮我们优化一下农田灌溉系统的最短路径算法，报酬是十袋土豆。&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>我一把抓起纸条，搓成一团，扔了出去。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode2574：空间复杂度O(1)？村长说这是节俭美德！</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-06/</link><pubDate>Sat, 06 Jun 2026 12:36:25 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-06/</guid><description>&lt;p>🎮 空间复杂度O(1)？村长说这是节俭美德！&lt;/p>
&lt;p>“小白，村里要搞个村运会！”村长塞给我一张皱巴巴的纸，上面歪歪扭扭画着一排数字：&lt;code>[10, 4, 8, 3]&lt;/code>，“你看，这是咱村拔河比赛的对阵表！”&lt;/p>
&lt;p>我定睛一看，“这……不就是个数组吗？”&lt;/p>
&lt;p>“肤浅！”村长指着纸上那排数字，表情像在解说世界杯决赛，“这叫‘平衡指数’！对于每个人，我要知道他左边所有人的体重之和，与右边所有人的体重之和，差距有多大！这样我才能调整阵容，让两边势均力敌！”&lt;/p>
&lt;p>我挠挠头：“&amp;hellip;&amp;hellip;求每个位置的 &lt;code>|leftSum - rightSum|&lt;/code> ？”&lt;/p>
&lt;p>“对！”村长猛拍大腿，“就是这个意思！赶紧帮我算，下午就要贴对阵表了！”&lt;/p>
&lt;p>我把对阵表往桌上一摊，思路直白得像村口那条水泥路——先从左往右扫一遍，记下每个位置左边所有人的体重之和；再从右往左扫一遍，记下右边所有人的体重之和；最后逐个位置算差值取绝对值：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">leftRightDifference&lt;/span>(nums):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> n &lt;span style="color:#f92672">=&lt;/span> len(nums)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> left_sum &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> n
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> right_sum &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> n
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 从左到右，计算每个位置的左侧总和&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(&lt;span style="color:#ae81ff">1&lt;/span>, n):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> left_sum[i] &lt;span style="color:#f92672">=&lt;/span> left_sum[i &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">+&lt;/span> nums[i &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 从右到左，计算每个位置的右侧总和&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(n &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> right_sum[i] &lt;span style="color:#f92672">=&lt;/span> right_sum[i &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">+&lt;/span> nums[i &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 计算差值的绝对值&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(n):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> answer[i] &lt;span style="color:#f92672">=&lt;/span> abs(left_sum[i] &lt;span style="color:#f92672">-&lt;/span> right_sum[i])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> answer
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>村长凑过来看了一眼，表情有点复杂，就像 code review 时发现了一段能跑但让人血压升高的代码。&lt;/p>
&lt;p>“小白啊，”他拍了拍桌上那张被我用掉的草稿纸，语气里透着一股“我不是心疼钱我是心疼资源”的味道，“拔河比赛有预赛、复赛、决赛三轮，每轮每个队伍你都给我开三张表——&lt;code>left_sum&lt;/code> 、&lt;code>right_sum&lt;/code> 、&lt;code>answer&lt;/code> ——三轮下来草纸堆得比锅炉房还高。咱村的传统你忘了？！”&lt;/p>
&lt;p>我愣了一下。好家伙，三个数组一字排开，每个数组都要从头填到尾，内存占用都快赶上一支拔河队的完整档案了。而且整个过程走完，这些数组就成了再也没人翻看的旧档案——村长心疼钱，我还心疼那多余的内存分配呢。&lt;/p>
&lt;p>“节省……”我咬着笔杆，盯着那三个数组开始琢磨。&lt;code>left_sum&lt;/code> 好办——用一个 &lt;code>left&lt;/code> 变量一路滚到底就行。从最左边往右走，走哪儿加哪儿，根本不用开数组，一个变量搞定。但 &lt;code>right_sum&lt;/code> 怎么办？右边队员的体重和，在从左往右走的时候是未知的——还没走到右边呢，怎么知道右边那群人加起来多重？除非我先算一遍总和。&lt;/p>
&lt;p>总和！我一个激灵，在草稿纸上划拉起来：设全队总重 &lt;code>total = sum(nums)&lt;/code>，对于位置 i，左边的体重和是 &lt;code>left&lt;/code>，当前这个人自己的体重是 &lt;code>nums[i]&lt;/code>，那右边的和&lt;code>right&lt;/code>不就是 &lt;code>total - left - nums[i]&lt;/code> 吗？&lt;/p>
&lt;ul>
&lt;li>&lt;code>answer[i] = |left - right|&lt;/code>&lt;/li>
&lt;li>&lt;code>right = total - left - nums[i]&lt;/code>&lt;/li>
&lt;li>&lt;code>answer[i] = |left - (total - left - nums[i])| = |2 * left + nums[i] - total|&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>我整理了下思路：&lt;code>left_sum&lt;/code> 用一个滚动的变量 &lt;code>left&lt;/code> 取代，&lt;code>right_sum&lt;/code> 用 &lt;code>total - left - nums[i]&lt;/code> 表达式消解，&lt;code>answer&lt;/code> 干脆直接写回原数组 &lt;code>nums&lt;/code>，连额外空间都省了。代码从三循环三数组，缩成一趟遍历原地修改：&lt;/p>
&lt;pre tabindex="0">&lt;code>def leftRightDifference(nums):
total = sum(nums) # 全队总体重
left = 0 # 左边已经算过的人总体重，初始为 0
for i, num in enumerate(nums):
nums[i] = abs(left * 2 + num - total) # 计算差值，直接写回原数组
left += num # 把当前人算到左边的队伍里
return nums
&lt;/code>&lt;/pre>&lt;p>时间复杂度 O(n)，空间复杂度 O(1)（不算答案数组的话），是真正的&amp;quot;原地算法&amp;quot;！&lt;/p>
&lt;p>“三张表变零张，好啊！连草纸钱都省了！这才是咱们村的作风！” 村长高兴地直拍大腿，那兴奋劲儿比他自己拔赢了比赛还夸张。&lt;/p>
&lt;p>老头把优化后的对阵表往怀里一揣，乐呵呵地走了。临出门又回头喊了一嗓子，“省下来的钱，回头给你加个鸡腿！我先去排兵布阵，下午预赛，记得来操场看拔河！”&lt;/p>
&lt;p>门关上，我把拨火棍往锅炉边一靠，打开魔法书给 2574 也留了行记录：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>2574 - 左右元素和的差值&lt;/strong>
解法：total 推右边和，数学去冗余。一趟遍历原地修改，O(n) 时间，O(1) 额外空间
心得：右边和不是独立信息，是派生值。用老勇者的话说——能现场算的，就别存。&lt;/p>&lt;/blockquote>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3753：游乐园峰谷过山车II</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-05/</link><pubDate>Fri, 05 Jun 2026 11:12:16 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-05/</guid><description>&lt;h2 id="-游乐园峰谷过山车-ii当数据量膨胀到万亿级">🎮 游乐园峰谷过山车 II：当数据量膨胀到万亿级
&lt;/h2>&lt;p>“来了。”&lt;/p>
&lt;p>我靠窗坐着，魔法书摊在膝上，手边一杯茶冒着热气。窗外传来熟悉的脚步声——不是那种出大事的急匆匆跑法，而是不紧不慢、带着“ 我要来吓唬你了但我其实很期待看你反应 ” 的节奏。上次游乐园试运营、正式服连着来的时候，也是这个步调。如果把村长的每次出场录下来做傅里叶变换，峰值频率大概在“焦虑”和“幸灾乐祸”之间。&lt;/p>
&lt;p>门被一脚踹开。“小白！游乐园又来任务了！”他手里挥着一封烫金信封，脸上的表情比上次更兴奋。&lt;/p>
&lt;p>我接过信封，没急着拆。“村长，让我猜猜——波动值 II，规则一个字没改，数据量加了一串零。对吗？”&lt;/p>
&lt;p>村长愣了一下，显然没料到我预判了他的预判。我把信封拆开，果然：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>3753 - 范围内总波动值 II&lt;/strong>
规则：同 3751
数据范围：1 ≤ num1 ≤ num2 ≤ 10¹⁵&lt;/p>&lt;/blockquote>
&lt;p>10¹⁵，十五位数。上次 3751 是 10⁵，六位数，暴力法直接遍历，几十万次操作，眨眼就跑完了。这次如果用同样的暴力对付 10¹⁵，遍历次数得用“万亿”做单位——从几十万到万亿，差了整整十个数量级。上次几十万次耗时不到 0.1 秒，这次暴力能跑到地老天荒。&lt;/p>
&lt;p>但我没有慌，甚至有点小得意。因为昨天的我在老勇者敲烟斗的节奏里，已经把数位 DP 的核心思想刻入脑海了。不光学会了“怎么搜”，还搞懂了“怎么记”——限制状态和计算状态分家，波动值从行李升级成战报。而那两个版本的代码，昨天就躺进我的魔法书里了，一字一句，墨迹都还没褪色。&lt;/p>
&lt;p>“村长，”我把茶杯放下，翻开魔法书到那两页代码，“你看，昨天老勇者教了我两种数位 DP 写法。第一种‘行李版’，把 &lt;code>waviness&lt;/code> 放在参数里一路扛下去——最符合直觉，填完所有位直接返回累加好的成绩单。第二种‘战报版’，把 &lt;code>waviness&lt;/code> 踢出参数表，变成返回值的一部分，每个子树统一上报 &lt;code>(子树波动值总和, 子树合法数字个数)&lt;/code>。”&lt;/p>
&lt;p>我用手指点了点代码里的核心差异，继续解释：“说句公道话，今天这数据量，两种其实都能跑。10¹⁵ 是十五位数，状态总数也就几万个，行李版的缓存虽然会漏掉一些命中，但状态空间本身不大，硬跑也能过——顶多比战报版多喘两口气，不至于升天。”&lt;/p>
&lt;p>“但你猜我选哪个？”我把魔法书往他面前推了推，指着第二种写法旁边昨天重重划下的那行下划线——&lt;strong>波动值是战报，不是行李&lt;/strong>。“行李版今天能过，但如果数据量换到 10¹⁸ 呢？换到 10¹⁰⁰ 呢？行李扛一路，缓存命中率随数据量一路下滑；战报往上交，数据涨到天上去也没关系。既然昨天已经想明白了，今天就没必要退回去走老路。”&lt;/p>
&lt;p>我把战报版代码原封不动贴进提交框，一个字符都没改，点了提交。绿色大勾弹出来的时候，村长正在喝我给他倒的茶。他看了看屏幕上的 Accepted，又看了看耗时——表情从“我要看你翻车”变成了“你怎么又不翻车”，嘴唇抖了三抖，最后只憋出一句：“就这？”&lt;/p>
&lt;p>“就这。”我把魔法书翻到昨天写的那条批注——“波动值是战报，不是行李”——在下面补了一行新记录：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>3753 - 范围内总波动值 II&lt;/strong>
解法：同 3751 数位 DP 战报版，O(位数 × 状态数)，代码零改动
数据范围：num2 ≤ 10¹⁵，位数从 6 涨到 16，状态数几乎没变
心得体会：行李版今天也能过，但既然学会了看战报，就没必要再扛行李。好的状态设计，不惧数据膨胀——昨天多想十分钟，今天省下十个数量级，连代码都不用改一行。&lt;/p>&lt;/blockquote>
&lt;p>村长看了看我魔法书上的批注，默默喝完了杯里的茶。他站起来走到门口，顿了顿，没回头：“下次要是再来个 III，范围扩到 10¹⁰⁰——你还这么稳？”&lt;/p>
&lt;p>“理论上，”我重新拿起拨火棍，往锅炉里又添了根柴，“位数再涨十倍，状态数也就多个零。只要问题本质没变，战报永远管用。”&lt;/p>
&lt;p>村长哼了一声，推开门的瞬间，风把他半句话吹了进来：“……你小子，学了点东西。”&lt;/p>
&lt;p>门关上。我靠在椅背上，火苗在锅炉里欢快地蹿着。魔法书摊在膝上，两行批注一上一下——3751 教会我怎么写数位 DP，3753 教会我一个更重要的道理——最好的代码不是跑得最快的，而是数据量涨一百亿倍也不用改一行的。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3751：游乐园峰谷过山车</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-04/</link><pubDate>Thu, 04 Jun 2026 16:50:01 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-04/</guid><description>&lt;h2 id="-游乐园峰谷过山车数位dp的修行">🎮 游乐园峰谷过山车：数位DP的修行
&lt;/h2>&lt;h4 id="第一章村长的新任务">第一章：村长的新任务
&lt;/h4>&lt;p>&amp;ldquo;小白！别睡了！有新任务了！&amp;rdquo; 村长一脚踹开我家大门，手里挥舞着一份皱巴巴的羊皮卷轴。我一个激灵从床上弹起来，下意识就去摸魔法书——这开场白我太熟了，每次村长用这个句式，后面铁定跟着一道新题。&lt;/p>
&lt;p>“还记得算法乐园那个没设 base case 的‘递归过山车’吗？”他把卷轴拍在我桌上，“乐园那边连夜开了事故复盘会，紧急关停了项目，总算把老爷爷从轨道上救下来了。他们痛定思痛，在旁边新建了个项目，叫‘数位过山车’。”&lt;/p>
&lt;p>我拿起卷轴，村长继续解释：“ 这次项目上线前要做安全评估，再不敢直接莽了。新过山车的轨道由数字构成——每个数字就是一段铁轨，每一位是铁轨上的一个连接点。如果某一段连接点比两边都高，就是‘峰’；比两边都低，就是‘谷’。峰和谷太多意味着轨道起伏太剧烈，车开上去乘客容易被动升天，重蹈爷爷的覆辙。所以他们需要计算**‘波动值’**——也就是峰和谷的总数——来评估轨道安全性，决定安全带是用两根还是五根。”&lt;/p>
&lt;p>我瞅了眼卷轴，上面写着波动值的定义：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>峰&lt;/strong>：某一位数字严格大于左右两边——像山峰一样凸起来，在上面一览众山小。
&lt;strong>谷&lt;/strong>：某一位数字严格小于左右两边——像山谷一样凹下去，在里头抬头望星空。
首尾两位不能算峰或谷——边界保护，毕竟第一个和最后一个连邻居都凑不齐。
少于3位的数字波动值为0——太短了没法波动&lt;/p>&lt;/blockquote>
&lt;p>“你的任务，” 村长用指关节敲了敲卷轴，“是计算区间 [num1, num2] 内所有数字的波动值之和。乐园那边要拿着这个数据去申请保险。”&lt;/p>
&lt;p>我盯着卷轴看了三秒，突然笑出声：“这不就是遍历每个数字，拿字符串滑窗检查一下峰和谷嘛。&lt;code>zip(s, s[1:], s[2:])&lt;/code> 三连滑窗，峰谷一眼扫完，比在游戏里开扫描模式还快。村长你是不是又在放水？”&lt;/p>
&lt;p>村长冷笑一声。那种笑容我见过——上次他给我推游乐园正式服 5×10⁴ 数据的时候，也是这副表情，像一个提前看过剧透的副本 Boss。“数据范围 num2 ≤ 10⁵，暴力当然能过。但我先给你打个预防针——说不定哪天范围就扩到 10¹⁸ 了呢？”&lt;/p>
&lt;p>说完，村长便甩袖而去。我透过窗户望了一眼算法乐园的方向，那座“数位过山车”的轨道已经在晨光中闪闪发亮——10⁵ 嘛，先暴力 AC 了再说。至于 10¹⁸……到时候再想呗，未来的我肯定有办法。实在不行，不还有老勇者嘛。&lt;/p>
&lt;hr>
&lt;h4 id="第二章暴力枚举三连-zip-横扫千军">第二章：暴力枚举——三连 zip 横扫千军
&lt;/h4>&lt;p>直接暴力：从 num1 一路遍历到 num2，每个数字转成字符串，然后掏出 Python 的 &lt;code>zip(s, s[1:], s[2:])&lt;/code> 三连滑窗，把连续三位 a、b、c 一次性拎出来。如果 &lt;code>a &amp;gt; b &amp;lt; c&lt;/code>，说明 b 是个谷，轨道在这里砸了一个坑；如果 &lt;code>a &amp;lt; b &amp;gt; c&lt;/code>，说明 b 是个峰，轨道在这里顶出一个包。每发现一个峰或谷，计数器加一。遍历完所有数字，返回总数完事。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">totalWaviness&lt;/span>(num1: int, num2: int) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> int:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(num1, num2 &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>): &lt;span style="color:#75715e"># 遍历区间内每个数字&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> s &lt;span style="color:#f92672">=&lt;/span> str(i) &lt;span style="color:#75715e"># 转成字符串方便逐位访问&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> a, b, c &lt;span style="color:#f92672">in&lt;/span> zip(s, s[&lt;span style="color:#ae81ff">1&lt;/span>:], s[&lt;span style="color:#ae81ff">2&lt;/span>:]): &lt;span style="color:#75715e"># 滑动窗口取连续三位&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> a &lt;span style="color:#f92672">&amp;gt;&lt;/span> b &lt;span style="color:#f92672">&amp;lt;&lt;/span> c: &lt;span style="color:#75715e"># 谷&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> a &lt;span style="color:#f92672">&amp;lt;&lt;/span> b &lt;span style="color:#f92672">&amp;gt;&lt;/span> c: &lt;span style="color:#75715e"># 峰&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> res
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>写完一跑，秒出AC。&lt;code>zip(s, s[1:], s[2:])&lt;/code> 这个三连滑窗真是比德芙还丝滑——&lt;code>s&lt;/code> 是原字符串，&lt;code>s[1:]&lt;/code> 是从第二位开始的字符串，&lt;code>s[2:]&lt;/code> 是从第三位开始的字符串，三个错位排列的字符串往 &lt;code>zip&lt;/code> 里一塞，直接吐出所有连续三位的组合。时间复杂度 O(N·L)，N 是区间长度，L 是数字平均位数。10⁵ 个数字，每个最多六七位，总操作量撑死几十万次。&lt;/p>
&lt;hr>
&lt;h4 id="第三章小白的-数位dp-困境">第三章：小白的 数位DP 困境
&lt;/h4>&lt;p>提交完暴力代码，我靠在椅背上准备摸鱼，脑子里却自动回放起村长临走时的那个冷笑——“如果哪天范围扩到 10¹⁸&amp;hellip;&amp;hellip;”，我越想越不是滋味。村长的嘴，骗人的鬼，但村长的预言，比 stack trace 还准。上次他说“以后会有大数据版本”，隔天正式服就砸脸上了。这次他特意打个预防针，八成是在给我提前剧透。&lt;/p>
&lt;p>10¹⁸ 意味着什么？意味着如果我还用暴力遍历，评测机怕是要甩我个TLE。我必须想一个不依赖区间长度的解法。&lt;/p>
&lt;p>我在魔法书里翻来翻去，目光落在一页标注着“数位 DP”的章节上。那是老勇者上次去邻村之前塞给我的，说是“有空看看，以后用得上”。我当时翻了前两页，看到满眼的 &lt;code>dfs(i, is_limit, is_num)&lt;/code> 就觉得脑壳疼，随手一合当枕头用了。现在想想，真是书到用时方恨少，码到跑时方觉糙。&lt;/p>
&lt;p>“数位 DP……”我喃喃自语，“ 核心思想应该是按位搜索——把数字一位一位填出来，在填的过程中统计波动值。这样就不用枚举每一个完整的数字，而是枚举每一位可能的取值。相当于把十亿个数字压缩成几十个状态，用记忆化搜索代替暴力遍历。”&lt;/p>
&lt;p>思路大概是——从最高位开始，逐位尝试 0 到 9——每填一个新的数字 d，我需要知道它和前一位 &lt;code>last_digit&lt;/code> 的关系——变大了、变小了，还是持平。但峰和谷的判断需要三位数字。要判断前一位是不是峰，我得同时知道前一位的数字、更前一位的数字、以及当前位 d。也就是说，当我填下 d 的时候，才能回头判定前一位是不是峰或谷。“那我该记录什么状态？”我拿笔在草稿纸上乱画。&lt;/p>
&lt;p>首先肯定要有 &lt;code>i&lt;/code>，表示当前填到第几位，这是数位 DP 的标配，用来判断递归什么时候到头。然后得有 &lt;code>last_digit&lt;/code>，记录上一位填的是什么数字，不然没法算当前位是升还是降。还得搞个 &lt;code>last_cmp&lt;/code>，记录上一位和更前一位的比较趋势——上升记 1，下降记 -1，持平记 0。这样当上一位与当前位的新趋势 &lt;code>c&lt;/code> 算出来之后，如果 &lt;code>c * last_cmp &amp;lt; 0&lt;/code>，就说明趋势反转了，前一位要么是峰要么是谷，波动值加一。&lt;/p>
&lt;p>“等等，” 我咬着笔杆皱起眉头，“还有上下界的问题。我只能在 num1 到 num2 之间搜，不能搜出范围外。需要 &lt;code>limit_low&lt;/code> 和 &lt;code>limit_high&lt;/code> 两个布尔值，分别标记当前是否要受下界和上界的约束。另外，如果 num1 和 num2 位数不同，比如num1=1，num2=100，数位对齐后数字 5 实际上是 ‘005’，前两个零不能参与峰谷判断，得想办法标记前面是否已经填过非零数字，没开始之前趋势 c 直接设成 0。”&lt;/p>
&lt;p>我盯着纸上列出的状态参数——&lt;code>i&lt;/code>、&lt;code>last_cmp&lt;/code>、&lt;code>last_digit&lt;/code>、&lt;code>limit_low&lt;/code>、&lt;code>limit_high&lt;/code>——光参数就已经五个了，还没算上前导零的判断。但更让我卡住的还不是参数多，而是另一个根本性的问题。&lt;/p>
&lt;p>“返回值怎么设计？” 这是我最困惑的地方。之前学数位 DP 的时候，最常见的套路是“统计满足条件的数字个数”——dfs 返回从当前状态开始能构造出多少个合法数字，最后调用 &lt;code>dfs(0, true, true)&lt;/code> 就是答案。还有一种是“统计权值和”——比如求所有数字的数位和之和，返回值是从当前状态开始所有数字的某个属性累加值。&lt;/p>
&lt;p>但这道题既不是数个数，也不是简单的权值和。我要的是“波动值之和”——先算每个数字的波动值，再把所有数字的波动值加起来。波动值本身是一个数字内部峰和谷的计数，它在填数字的过程中动态累加，不是一个静态的属性。那么问题来了，我到底应该把波动值当成状态参数 &lt;code>waviness&lt;/code> 一路传下去，到了底层直接返回？还是应该把它当成返回值的一部分，让每层递归把后续所有数字的波动值之和汇总上来？&lt;/p>
&lt;p>我越想越乱，草稿纸上画满了状态转移图，又全划掉了。我盯着天花板上被锅炉熏出的水渍，第一次觉得数位 DP 比递归过山车还难。递归过山车好歹只有一个问题——没设 base case；而数位 DP 有一百个问题，每一个都在问我：“你这个状态设计对了吗？”参数怎么分、返回值怎么定、记忆化怎么开、前导零怎么处理，全搅在一起，像极了一个没有封装的单体应用——牵一发而动全身。&lt;/p>
&lt;p>正在我纠结要不要干脆放弃治疗、等正式服上线再说的时候，门口传来一阵慢悠悠的脚步声，伴随着打火机点烟斗的“咔嗒”声——老勇者从邻村回来了。&lt;/p>
&lt;p>“怎么，数位 DP 把你难住了？”他倚在门框上，慢悠悠地吐出一口烟圈。&lt;/p>
&lt;hr>
&lt;h4 id="第四章老勇者的-数位-dp-锦囊">第四章：老勇者的 数位 DP 锦囊
&lt;/h4>&lt;p>老勇者没急着进屋，先在门口磕了磕烟斗里的灰，然后慢悠悠地踱到我桌前，低头扫了一眼我那画满又划掉的草稿纸。他看了足足五秒钟，表情就像在 code review 里看到了一坨没有注释的祖传代码。&lt;/p>
&lt;p>“状态设计全写在纸上了，但你没把它们分清楚。”他把烟斗往桌角一敲，指着纸上那几个被我圈了又圈的参数，“数位 DP 的参数分两类——一类叫‘限制状态’，管的是‘你能填什么’；一类叫‘计算状态’，管的是‘你填完之后发生了什么’。你把它们全搅在一起，自然觉得脑子里的栈要溢出了。”&lt;/p>
&lt;p>他在纸上画了一道线，左边写“限制”，右边写“计算”：&lt;/p>
&lt;p>“限制状态有两个：&lt;code>limit_low&lt;/code> 和 &lt;code>limit_high&lt;/code>。它们只做一件事——卡死当前位能填的数字范围。如果 &lt;code>limit_high&lt;/code> 为 true，说明前面每一位都死死贴着上界，当前位绝对不能超过 &lt;code>high_s[i]&lt;/code>；一旦某一位填了一个比上界小的数字，后面的 &lt;code>limit_high&lt;/code> 就永久变成 false，所有位直接解放，0 到 9 随便填。&lt;code>limit_low&lt;/code> 同理，只不过方向相反——一旦某一位填了一个比下界大的数字，后面就再也不用看下界的脸色了。这两个参数就是数字世界里的边界门卫。”&lt;/p>
&lt;p>“计算状态也有两个：&lt;code>last_digit&lt;/code> 和 &lt;code>last_cmp&lt;/code>。它们只做一件事——判断峰和谷。&lt;code>last_digit&lt;/code> 是上一位的数字，&lt;code>last_cmp&lt;/code> 是上一位和更前一位的趋势关系，1 是上升，-1 是下降，0 是持平。当你填下当前位 d 时，先算出新的趋势—— &lt;code>c = (d &amp;gt; last_digit) - (d &amp;lt; last_digit)&lt;/code>，一个表达式同时处理大于、小于、等于三种情况，然后看一眼 &lt;code>c * last_cmp &lt;/code>：峰和谷本质都是方向改变，在数学上就是乘积为负，也就是说如果&lt;code>&amp;lt; 0&lt;/code>，说明趋势反转，上一位要么是峰要么是谷，波动值加一。这两个参数完全不关心上下界，只关心数字之间的相对大小。”&lt;/p>
&lt;p>“至于前导零——用 &lt;code>last_cmp == 0&lt;/code> 就能兼顾。还没填有效数字之前，趋势保持为零，&lt;code>c * last_cmp&lt;/code> 永远是 0，永远不会触发峰谷判断。把前导零的三个虚拟零拆开看，它们之间全是持平，趋势变化为零，自然不会有峰谷误判。”&lt;/p>
&lt;p>他在两个区域之间又画了一根线，在计算状态那边补了一个参数：“至于波动值 &lt;code>waviness&lt;/code>，它既不是限制也不是计算，它是你最终要交的答案本身。这个东西的处理方式，有两种流派：一种把它当行李一路扛下去，一种把它当战报一层层报上来。看好了，我两个都写给你。”&lt;/p>
&lt;p>他在纸上写下第一种写法：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>low_s &lt;span style="color:#f92672">=&lt;/span> list(map(int, str(num1)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>high_s &lt;span style="color:#f92672">=&lt;/span> list(map(int, str(num2)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>n &lt;span style="color:#f92672">=&lt;/span> len(high_s)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>diff_lh &lt;span style="color:#f92672">=&lt;/span> n &lt;span style="color:#f92672">-&lt;/span> len(low_s)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">@cache&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">dfs&lt;/span>(i, waviness, last_cmp, last_digit, limit_low, limit_high):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> i &lt;span style="color:#f92672">==&lt;/span> n:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> waviness
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> lo &lt;span style="color:#f92672">=&lt;/span> low_s[i &lt;span style="color:#f92672">-&lt;/span> diff_lh] &lt;span style="color:#66d9ef">if&lt;/span> limit_low &lt;span style="color:#f92672">and&lt;/span> i &lt;span style="color:#f92672">&amp;gt;=&lt;/span> diff_lh &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> hi &lt;span style="color:#f92672">=&lt;/span> high_s[i] &lt;span style="color:#66d9ef">if&lt;/span> limit_high &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#ae81ff">9&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> is_num &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#f92672">not&lt;/span> limit_low &lt;span style="color:#f92672">or&lt;/span> i &lt;span style="color:#f92672">&amp;gt;=&lt;/span> diff_lh
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> d &lt;span style="color:#f92672">in&lt;/span> range(lo, hi &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> c &lt;span style="color:#f92672">=&lt;/span> (d &lt;span style="color:#f92672">&amp;gt;&lt;/span> last_digit) &lt;span style="color:#f92672">-&lt;/span> (d &lt;span style="color:#f92672">&amp;lt;&lt;/span> last_digit) &lt;span style="color:#66d9ef">if&lt;/span> is_num &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> w &lt;span style="color:#f92672">=&lt;/span> waviness
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> c &lt;span style="color:#f92672">*&lt;/span> last_cmp &lt;span style="color:#f92672">&amp;lt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> w &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res &lt;span style="color:#f92672">+=&lt;/span> dfs(i &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, w, c, d,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> limit_low &lt;span style="color:#f92672">and&lt;/span> d &lt;span style="color:#f92672">==&lt;/span> lo,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> limit_high &lt;span style="color:#f92672">and&lt;/span> d &lt;span style="color:#f92672">==&lt;/span> hi)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> res
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> dfs(&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#66d9ef">True&lt;/span>, &lt;span style="color:#66d9ef">True&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>“这个版本最符合直觉——波动值放在参数里，顺着递归一路带到底。每发现一个峰或谷，就 &lt;code>w += 1&lt;/code>，最后叶子节点把累加好的结果交上去。就像你拎着一个行李袋，每经过一个峰或谷就往袋子里扔一颗石子，到了终点直接把袋子倒出来数。状态设计也一目了然：&lt;code>limit_low&lt;/code> 和 &lt;code>limit_high&lt;/code> 是门卫，卡取值范围；&lt;code>last_digit&lt;/code> 和 &lt;code>last_cmp&lt;/code> 是轨道设计师，管峰谷判断；&lt;code>waviness&lt;/code> 是最终成绩单，一路传到底。”&lt;/p>
&lt;p>“但是，”他把烟斗往桌角一敲，“这个版本藏着一个坑——&lt;code>@cache&lt;/code> 会把所有参数都当成缓存 key，包括 &lt;code>waviness&lt;/code>——问题来了：到达同一个 &lt;code>(i, last_cmp, last_digit, limit_low, limit_high)&lt;/code> 状态时，&lt;code>waviness&lt;/code> 可能完全不一样，因为 &lt;code>waviness&lt;/code> 不同，缓存会直接把它们当成了两个不同的状态，各算各的。等于你给每一条搜索路径都建了一个独立的缓存项，记忆化名存实亡，退化成了带 &lt;code>@cache&lt;/code> 装饰器的暴力回溯。”&lt;/p>
&lt;p>我盯着代码里那个被 &lt;code>@cache&lt;/code> 包裹的 &lt;code>dfs&lt;/code>，参数表里 &lt;code>waviness&lt;/code> 赫然在列——每次峰谷累加，它就变一次，等于每条搜索路径都有一个独一无二的缓存 key——“那怎么改？”我追问。&lt;/p>
&lt;p>“把 &lt;code>waviness&lt;/code> 从参数里踢出去，让它变成返回值的一部分。行李别自己扛了，让每个子树统一上报战报。”他在纸上写下第二种写法：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>low_s &lt;span style="color:#f92672">=&lt;/span> list(map(int, str(num1)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>high_s &lt;span style="color:#f92672">=&lt;/span> list(map(int, str(num2)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>n &lt;span style="color:#f92672">=&lt;/span> len(high_s)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>diff_lh &lt;span style="color:#f92672">=&lt;/span> n &lt;span style="color:#f92672">-&lt;/span> len(low_s)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># dfs 返回两个数：子树波动值总和，子树合法数字个数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">@cache&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">dfs&lt;/span>(i, last_cmp, last_digit, limit_low, limit_high):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> i &lt;span style="color:#f92672">==&lt;/span> n:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e"># 能递归到终点的都是合法数字&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> lo &lt;span style="color:#f92672">=&lt;/span> low_s[i &lt;span style="color:#f92672">-&lt;/span> diff_lh] &lt;span style="color:#66d9ef">if&lt;/span> limit_low &lt;span style="color:#f92672">and&lt;/span> i &lt;span style="color:#f92672">&amp;gt;=&lt;/span> diff_lh &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> hi &lt;span style="color:#f92672">=&lt;/span> high_s[i] &lt;span style="color:#66d9ef">if&lt;/span> limit_high &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#ae81ff">9&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> waviness_sum &lt;span style="color:#f92672">=&lt;/span> num_cnt &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> is_num &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#f92672">not&lt;/span> limit_low &lt;span style="color:#f92672">or&lt;/span> i &lt;span style="color:#f92672">&amp;gt;&lt;/span> diff_lh &lt;span style="color:#75715e"># 前面是否填过数字&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> d &lt;span style="color:#f92672">in&lt;/span> range(lo, hi &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 当前填的数不是最高位，c 才有意义&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> c &lt;span style="color:#f92672">=&lt;/span> (d &lt;span style="color:#f92672">&amp;gt;&lt;/span> last_digit) &lt;span style="color:#f92672">-&lt;/span> (d &lt;span style="color:#f92672">&amp;lt;&lt;/span> last_digit) &lt;span style="color:#66d9ef">if&lt;/span> is_num &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sub_waviness_sum, sub_num_cnt &lt;span style="color:#f92672">=&lt;/span> dfs(i &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>, c, d,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> limit_low &lt;span style="color:#f92672">and&lt;/span> d &lt;span style="color:#f92672">==&lt;/span> lo,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> limit_high &lt;span style="color:#f92672">and&lt;/span> d &lt;span style="color:#f92672">==&lt;/span> hi)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> waviness_sum &lt;span style="color:#f92672">+=&lt;/span> sub_waviness_sum &lt;span style="color:#75715e"># 累加子树的波动值&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> num_cnt &lt;span style="color:#f92672">+=&lt;/span> sub_num_cnt &lt;span style="color:#75715e"># 累加子树的合法数字个数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> c &lt;span style="color:#f92672">*&lt;/span> last_cmp &lt;span style="color:#f92672">&amp;lt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>: &lt;span style="color:#75715e"># 形成了一个峰或谷&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> waviness_sum &lt;span style="color:#f92672">+=&lt;/span> sub_num_cnt &lt;span style="color:#75715e"># 这个峰谷会出现在 sub_num_cnt 个数字中&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> waviness_sum, num_cnt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">return&lt;/span> dfs(&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#66d9ef">True&lt;/span>, &lt;span style="color:#66d9ef">True&lt;/span>)[&lt;span style="color:#ae81ff">0&lt;/span>]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>“注意看，这个版本的返回值是一个元组——&lt;code>(子树波动值总和, 子树合法数字个数)&lt;/code>。每个节点不从上面接收任何波动值，只向上汇报两件事：我这棵子树里所有完整数字的波动值加起来是多少，以及我这棵子树里一共有多少个合法数字。“ 他指着核心逻辑，继续解释道：“当发现当前位形成峰或谷时——也就是 &lt;code>c * last_cmp &amp;lt; 0&lt;/code> 时，不是 &lt;code>+1&lt;/code>，而是 &lt;code>+ sub_num_cnt&lt;/code>——因为当前位形成的这个峰或谷，不是只影响一个数字，而是会影响从这条路走到底的&lt;strong>每一个&lt;/strong>合法数字。相当于你这个峰或谷不能只给自己加一分，而是要给从你这儿毕业的所有合法数字各加一分。”&lt;/p>
&lt;p>他顿了顿，举了个例子：“假设处理数字范围 [100, 999]，当递归到第2位时：
第一种写法：路径 1→2→? 和 1→3→? 会因为 &lt;code>waviness&lt;/code> 不同而分别计算，即使它们在第2位之后的子树结构完全相同，缓存也命中不了，各算各的。
第二种解法：对于相同的&lt;code> (i=2, last_cmp, last_digit, limit_low, limit_high)&lt;/code> 状态，子树返回的 &lt;code>(波动值总和, 数字个数)&lt;/code> 永远一模一样——因为波动值不再是你自己扛的行李，而是子树统一上交的战报。不管之前的历史路径是什么，只要这五个参数相同，缓存直接命中，子树不用重新算。如果当前位恰好形成了峰或谷，直接 &lt;code>+ sub_num_cnt&lt;/code>，把贡献批量加上去，一步到位。”&lt;/p>
&lt;p>我盯着两个版本来回看了好几遍，恍然大悟：“ 所以第一种是把波动值当‘个人行李’，每个搜索路径自己拎着走，但拎的东西不一样就没法共享存储柜。第二种是把波动值当‘团队战报’，每个子树统一汇总上报，相同的搜索状态无论从哪条路进来，返回的战报完全一样，共用储物柜。波动值从‘自己扛’变成了‘向上报’，&lt;code>waviness&lt;/code> 从参数里消失，&lt;code>@cache&lt;/code> 缓存命中率直接拉满。”&lt;/p>
&lt;p>老勇者满意地点点头，把烟斗往嘴里一叼：“ 把波动值当行李扛下去，看似符合直觉，但每个搜索路径的行李内容都不一样，相同状态永远无法共享缓存——这是典型的“有后效性”，历史路径影响了当前决策，记忆化形同虚设。而把波动值当战报报上来，让每个子树返回一个与历史无关的元组 &lt;code>(子树波动值总和, 子树合法数字个数)&lt;/code>，当前位的峰谷贡献通过 &lt;code>+ sub_num_cnt&lt;/code> 批量结算，五个参数锁死缓存 key，无后效性完美满足——这才是 DP 该有的样子。”&lt;/p>
&lt;p>他站起身朝门口走去，快到门边时回头补了一句，“数位 DP 的精髓，不是怎么搜，而是怎么设计状态和返回值，让记忆化从装饰品变成核武器。波动值是行李还是战报，决定了你的缓存命中率是趋近于零还是接近一百。”&lt;/p>
&lt;hr>
&lt;h4 id="第五章数位-dp-复盘从行李到战报的进化">第五章：数位 DP 复盘——从行李到战报的进化
&lt;/h4>&lt;p>锅炉里的火苗噼啪响了两声，像极了我脑袋开窍的声音。&lt;/p>
&lt;p>回看这一整天的折腾，从暴力滑窗到数位 DP，我踩的坑差不多能写一本短篇小说，名字都想好了，叫《数位 DP 从入门到放弃再到真香》。状态列不全，不知道谁该进参数、谁该进返回值，像个出门旅行把所有家当全攥在手里却找不到行李箱的焦虑旅客，最后还被缓存命中率敲打了一番，数位 DP 的门槛从来不在“能不能搜”，而在“搜完之后能不能记住”，而能不能记住，取决于状态设计有没有做到无后效性。&lt;/p>
&lt;p>我把魔法书翻到“数位 DP”那一章，把两个版本工工整整地贴上去：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>3751 - 范围内总波动值 I&lt;/strong>
暴力解法：&lt;code>zip&lt;/code> 三连滑窗，O(N·L)，数据 10⁵ 时秒出
数位 DP 解法：记忆化搜索，O(位数 × 状态数)
核心收获：波动值是战报，不是行李。限制状态和计算状态分开，记忆化才能真正发威。&lt;code>c * last_cmp &amp;lt; 0&lt;/code> 一行统一峰谷判断。峰谷贡献不是 &lt;code>+1&lt;/code>，是 &lt;code>+ sub_num_cnt&lt;/code>。&lt;/p>&lt;/blockquote>
&lt;p>我在“波动值是战报，不是行李”下面又重重划了道下划线。靠在椅背上，我透过窗户望了一眼算法乐园的方向，天边那座数位过山车的轨道在夕阳下闪闪发亮。&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3635：游乐园正式服的挑战</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-03/</link><pubDate>Wed, 03 Jun 2026 10:39:17 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-03/</guid><description>&lt;h2 id="-游乐园正式服的挑战当昨天的代码遇上今天的数据量">🎮 游乐园正式服的挑战：当昨天的代码遇上今天的数据量
&lt;/h2>&lt;p>上回说到，我和老勇者从“算法乐园”逃回村里，及时救活了村里的魔法锅炉。今天，我蹲在炉子旁边添柴边打盹，火焰舔着锅底，浓汤咕嘟咕嘟冒泡，节奏稳得像台跑了十年没崩过的服务器。昨天的游乐园副本虽然惊险，但好歹白嫖了两份战报，O(n+m) 的剪枝优化躺在魔法书里，想想还有点小得意。&lt;/p>
&lt;p>正迷糊着，一阵急促的脚步声由远及近。我睁眼一看，村长又来了。这回他手里没有门票，而是举着一封烫金信封：“小白，算法乐园那边来消息了——说上次的游园体验是‘试运营版本’，现在正式版上线了，叫 ‘3635 - 最早完成陆地和水上游乐设施的时间 II’。规则一模一样，但是——”&lt;/p>
&lt;p>他把信封翻开，只见上面写着醒目的数据范围：“陆地项目 n ≤ 5×10⁴，水上项目 m ≤ 5×10⁴，时间上限 10⁵。”&lt;/p>
&lt;p>我瞌睡当场醒了一半。五万乘五万，那可是 2.5×10⁹ 的组合量。拿暴力双循环去莽，别说 AC，评测机都得先给我发一封投诉邮件。但我没有慌。准确地说，是慌了两秒之后，脑子里突然闪过一道光——等等，这题昨天不是刚做过吗？规则一模一样，那昨天在长椅上推出来的优化思路……&lt;/p>
&lt;p>我猛地把魔法书翻到昨天画过山车标记的那一页。几行代码安安静静躺在那里，旁边还黏着一小坨棉花糖的糖霜痕迹，散发着淡淡的甜味：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">solve&lt;/span>(firstStart, firstDur, secondStart, secondDur):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> min_finish &lt;span style="color:#f92672">=&lt;/span> min(start &lt;span style="color:#f92672">+&lt;/span> dur &lt;span style="color:#66d9ef">for&lt;/span> start, dur &lt;span style="color:#f92672">in&lt;/span> zip(firstStart, firstDur))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> min(max(start, min_finish) &lt;span style="color:#f92672">+&lt;/span> dur &lt;span style="color:#66d9ef">for&lt;/span> start, dur &lt;span style="color:#f92672">in&lt;/span> zip(secondStart, secondDur))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">earliestFullTime&lt;/span>(landStart, landDur, waterStart, waterDur):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> min(solve(landStart, landDur, waterStart, waterDur),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> solve(waterStart, waterDur, landStart, landDur))
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我脑子里的灯泡亮得跟锅炉里的火光似的。昨天老勇者怎么说的来着？——“第一类设施里，只有最早结束的那个有意义，其他全是冗余信息。” 这句话跟数据量有半毛钱关系吗？没有。不管陆地项目是 100 个还是 50000 个，从陆地区出来的最早时间永远只取决于 &lt;code>min(start + duration)&lt;/code>。你有一百个过山车还是五万个过山车，你只需要知道哪个能让你最早下来，剩下的四万九千九百九十九个全是冗余数据，剪枝一刀全砍掉。&lt;/p>
&lt;p>关键在于&lt;code>solve&lt;/code> 函数里那两个 &lt;code>zip&lt;/code> 遍历。它们肩并肩站着，谁也不嵌套谁——第一个找 &lt;code>min_finish&lt;/code>，第二个在水上项目里挑最优。n 和 m 从 100 膨胀到 5×10⁴，每个设施仍然只被摸一次，总共十万次操作。暴力法 2.5×10⁹，优化版 10⁵——差了整整四个数量级！这就好比别人在手动数麦粒，而你开了一台联合收割机——场地越大，差距越离谱。&lt;/p>
&lt;p>代码一个字符都不用改。我原封不动贴上去，提交——秒 AC。耗时跟昨天跑 100 个项目的体感几乎一样，屏幕上弹出一个绿色的大勾，快得像锅炉里迸出的火星。村长在旁边看得一愣一愣的：“这就……做完了？那可是五万乘五万——”&lt;/p>
&lt;p>“做完了，”我把魔法书往怀里一揣，拿起拨火棍继续给锅炉添柴，“昨天的我已经帮今天的我写好了。”&lt;/p>
&lt;p>锅炉里的火苗欢快地蹿着，浓汤咕嘟咕嘟冒泡。我盯着跳动的火光，忍不住笑了。昨天在游乐园长椅上啃棉花糖的时候，还以为那段剪枝代码只是“今天先优雅一把，以后再说”。谁知道“以后”来得这么快——隔了一个晚上，正式版就砸脸上了。而代码躺在我的魔法书里，连个标点符号都没改，直接应战五万数据。这种感觉就像在新手村捡了一把木剑，以为后期还要换神装，结果发现这玩意是成长型神器，等级随角色自动缩放。&lt;/p>
&lt;p>我把魔法书摊在锅炉边，在昨天的战报下面补了一行新记录：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>3635 - 最早完成陆地和水上游乐设施的时间 II&lt;/strong>&lt;br>
解法：同 3633 优化版，O(n+m)，代码零改动，昨天多想十分钟，今天就省下四个数量级。&lt;br>
心得体会：剪枝剪得好，正式版再猛也不怕。暴力不是错，但知道什么时候不暴力，才是真正的抠门大师。&lt;/p>&lt;/blockquote>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>leetcode3633：游乐园大冒险</title><link>https://icurious-d.github.io/posts/leetcode_stories/26-06-02/</link><pubDate>Tue, 02 Jun 2026 10:48:27 +0800</pubDate><guid>https://icurious-d.github.io/posts/leetcode_stories/26-06-02/</guid><description>&lt;h2 id="-游乐园大冒险从暴力枚举到剪枝优化的最早回村攻略">🎮 游乐园大冒险——从暴力枚举到剪枝优化的最早回村攻略
&lt;/h2>&lt;p>昨天帮村长省下一大笔糖果钱，今天一大早，老头就笑呵呵地塞给我两张游乐园门票：“小白，干得漂亮！这是给你的福利——新开的‘算法乐园’，陆地项目和水上项目随便挑。不过有个条件——”&lt;/p>
&lt;p>他压低声音，眼神突然犀利起来：“村里的魔法锅炉等着你回来添柴，你得给我早点回村。陆地区和水区各玩一个就撤，顺序你定，但要算出怎么安排能最早回来。记住，我要的是最早回村时间，不是玩得最爽——别玩疯了忘了正事！”&lt;/p>
&lt;p>我捏着门票，心里一半是期待一半是吐槽：这哪是福利，这分明是换了个场景继续刷题。不过转念一想，陆地项目 n 最多 100 个，水上项目 m 也最多 100 个——这数据量不就是道热身题吗？&lt;/p>
&lt;p>把门票往兜里一揣，我朝乐园大门走去。村长在后面又补了一嗓子：“回来晚了锅炉熄了，你上个月的 AC 我全给你清了！”&lt;/p>
&lt;p>“收到收到，速战速决！”我头也不回地摆了摆手。&lt;/p>
&lt;hr>
&lt;h4 id="-第一战直觉翻车贪心的诱惑和陷阱">💥 第一战：直觉翻车——贪心的诱惑和陷阱
&lt;/h4>&lt;p>进了乐园，我先绕着园区转了一圈。左半边是陆地区，过山车轨道上挂着“Stack Overflow”的牌子，旋转木马叫“While True”，碰碰车场地写着“Index Out of Bounds”——好家伙，名字起得跟 bug 清单似的。右半边是水区，激流勇进叫“广度优先”，漂流叫“梯度下降”，还有个儿童泳池叫“Hello World”。&lt;/p>
&lt;p>我掏出村长塞给我的设施清单。陆地项目 &lt;code>landStart[i]&lt;/code> 是最早开放时间，&lt;code>landDur[i]&lt;/code> 是玩一圈要多久；水上项目同理。规则就一条：两类各挑一个，顺序随意，玩完就走。求最早离开时间。&lt;/p>
&lt;p>我往旁边的长椅上一坐，脑子里开始蹦各种贪心策略。先玩开放最早的？万一那个项目时长很长，另一个早就开放了，干等着浪费时间，典型的“先到先得但后到的人堵在门口”。先玩时长最短的？开放时间可能很晚，你这边三分钟完事了，那边还得等两个小时才能进去，更不靠谱。我甚至尝试先陆地后水上，每次都挑最早结束的组合，但发现不同组合之间互相影响，开放时间和持续时间互相牵制，贪心的局部最优推不出全局最优。&lt;/p>
&lt;p>折腾了十分钟，代码里便充斥着各种 &lt;code>if-else&lt;/code> 。我猛一拍脑门：：“我在干嘛？陆地最多 100 个，水上最多 100 个，总共才一万种组合。直接暴力枚举，两种顺序各算一遍取最小，不就完了？我这是在跟空气斗智斗勇！”&lt;/p>
&lt;p>老勇者不知什么时候已经站在我身后，嘴里叼着根棒棒糖——儿童节的存货还没吃完——慢悠悠地吐了句：“数据量小的时候，暴力就是最优解。你刚才那通操作，属于典型的过早优化，面试官看了血压都上来了。”&lt;/p>
&lt;p>我脸一红，赶紧把刚才那堆乱七八糟的代码全删了，老老实实写暴力。&lt;/p>
&lt;hr>
&lt;h4 id="-第二战暴力枚举简单粗暴一拳超人">⚡ 第二战：暴力枚举——简单粗暴，一拳超人
&lt;/h4>&lt;p>思路很直白：对于每一对陆地设施 &lt;code>i&lt;/code> 和水上设施 &lt;code>j&lt;/code>，算两种游玩顺序的完成时间，取最小值。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>先陆地后水上&lt;/strong>：陆地 &lt;code>landStart[i]&lt;/code>准时开始，&lt;code>landStart[i] + landDur[i]&lt;/code>结束。水上最早&lt;code>waterStart[j]&lt;/code> 开放，但必须等我从陆地项目出来，所以实际开始 &lt;code>max(waterStart[j], 陆地结束时间)&lt;/code>，然后加上 &lt;code>waterDur[j]&lt;/code>。&lt;/li>
&lt;li>&lt;strong>先水上后陆地&lt;/strong>：同理，水上开始 &lt;code>waterStart[j]&lt;/code>，结束 &lt;code>waterStart[j] + waterDur[j]&lt;/code>；陆地开始 &lt;code>max(landStart[i], 水上结束时间)&lt;/code>，再加 &lt;code>landDur[i]&lt;/code>。&lt;/li>
&lt;/ul>
&lt;p>遍历所有组合，记录全局最小值。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">earliestCompletionTime&lt;/span>(landStart, landDur, waterStart, waterDur):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> float(&lt;span style="color:#e6db74">&amp;#39;inf&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> i &lt;span style="color:#f92672">in&lt;/span> range(len(landStart)):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> j &lt;span style="color:#f92672">in&lt;/span> range(len(waterStart)):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 先陆地后水上&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> land_end &lt;span style="color:#f92672">=&lt;/span> landStart[i] &lt;span style="color:#f92672">+&lt;/span> landDur[i]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> finish1 &lt;span style="color:#f92672">=&lt;/span> max(waterStart[j], land_end) &lt;span style="color:#f92672">+&lt;/span> waterDur[j]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 先水上后陆地&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> water_end &lt;span style="color:#f92672">=&lt;/span> waterStart[j] &lt;span style="color:#f92672">+&lt;/span> waterDur[j]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> finish2 &lt;span style="color:#f92672">=&lt;/span> max(landStart[i], water_end) &lt;span style="color:#f92672">+&lt;/span> landDur[i]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 更新最小值&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ans &lt;span style="color:#f92672">=&lt;/span> min(ans, finish1, finish2)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> ans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>写完一跑，样例秒过。我正准备截图留念，老勇者把一根棉花糖塞我手里——糖丝上竟然用糖霜写着“O(n×m)”，这乐园的主题贯彻得真是丧心病狂。&lt;/p>
&lt;hr>
&lt;h4 id="-第三战剪枝优化从-onm-到-onm">🚀 第三战：剪枝优化——从 O(n×m) 到 O(n+m)
&lt;/h4>&lt;p>“暴力双循环，数据小的时候没毛病——代码短、逻辑直、bug 藏不住。不过——” 老勇者话锋一转，“你想过没有，如果这道题出个 II，n 和 m 飙到十万，你这 O(n×m) 打算怎么收场？”&lt;/p>
&lt;p>我咬了口棉花糖，含含糊糊地说：“那……那就再想办法呗，今天才 100，先爽了再说。”&lt;/p>
&lt;p>“巧了，我有个办法，既能让你今天写得更优雅，也能给以后留条路。”老勇者把我从长椅上拽起来，指向园区地图，“你看，不管先玩陆地还是先玩水上，一旦你选定了第一类设施，第二类设施的等待时间只取决于你从第一类设施出来时的最早时间。换句话说——第一类设施里，你没必要每个都试，你只需要找到那个&lt;strong>结束得最早&lt;/strong>的，因为出来得越早，留给第二类设施的缓冲就越大。”&lt;/p>
&lt;p>我眼睛一亮：“你的意思是……如果我先玩陆地，只需要找出所有陆地项目里 &lt;code>start + duration&lt;/code> 最小的那个值，记作 &lt;code>min_finish&lt;/code>，这就是我从陆地区出来的最早时间。然后水上设施不管挑哪个，实际开始时间都是 &lt;code>max(waterStart, min_finish)&lt;/code>，再加上自己的时长就是总完成时间。我只需要在水上项目里找一个能最早收工的就行！等等——这不就把嵌套循环拆成两个独立的单循环？！”&lt;/p>
&lt;p>“一点就通。”老勇者满意地点点头，“ 第一个循环找陆地最早结束时间，第二个循环在水上设施里挑最优。先水上后陆地同理，把函数反转一下就行。总复杂度从 O(n×m) 降到了 O(n+m)，哪天数据真飙到十万，你也能面不改色。”&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">solve&lt;/span>(firstStart, firstDur, secondStart, secondDur):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 从第一类设施中找出最早结束时间&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> min_finish &lt;span style="color:#f92672">=&lt;/span> min(start &lt;span style="color:#f92672">+&lt;/span> dur &lt;span style="color:#66d9ef">for&lt;/span> start, dur &lt;span style="color:#f92672">in&lt;/span> zip(firstStart, firstDur))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 在第二类设施中，找最早完成时间 &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> min(max(start, min_finish) &lt;span style="color:#f92672">+&lt;/span> dur &lt;span style="color:#66d9ef">for&lt;/span> start, dur &lt;span style="color:#f92672">in&lt;/span> zip(secondStart, secondDur))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">earliestCompletionTime&lt;/span>(landStart, landDur, waterStart, waterDur):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> land_water &lt;span style="color:#f92672">=&lt;/span> solve(landStart, landDur, waterStart, waterDur)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> water_land &lt;span style="color:#f92672">=&lt;/span> solve(waterStart, waterDur, landStart, landDur)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> min(land_water, water_land)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我盯着这几行代码：“&lt;code>min_finish&lt;/code> 是所有陆地项目里的最早结束时间……然后在水上项目里，每个都从 &lt;code>max(start, min_finish)&lt;/code> 开始算完成时间，取最小。这不就是把我刚才内层循环的 &lt;code>land_end&lt;/code> 换成了一个全局最优的常量吗？难怪能砍掉一层循环！”&lt;/p>
&lt;p>“这就是去冗余。”老勇者把棒棒糖棍子一扔，精准投进三米外的垃圾桶，“暴力法里，你对每个陆地项目都重复计算了水上等待，但实际上最优解只可能从‘陆地最早结束’这个分支里产生——因为如果某个陆地项目结束得更晚，你从它出来再去玩水上，只有可能更晚，不会更早。所以直接把其他陆地项目剪枝掉，一步到位。这种思路就像数据库查询优化里的谓词下推——把过滤条件提前，减少后面要处理的数据量。”&lt;/p>
&lt;p>他顿了顿，又补了一刀：“你今天这趟没白来。暴力是基操，优化是觉悟。能立刻理解 O(n+m) 的优化思路，说明你对问题结构的敏感度在涨。记住，以后看到‘两类各选一个，顺序任意’这种题型，先想想能不能把其中一类的选择压缩成一个最优候选值。剪枝剪得好，复杂度直接降维打击。”&lt;/p>
&lt;p>我正想谦虚两句，旁边突然冲过来一个小孩，拽着他爸的袖子喊：“爸爸爸爸，我要玩那个‘递归过山车’！”他爸脸都绿了：“别，上次你爷爷玩了递归过山车没设 base case，到现在还在天上转呢！栈溢出一万多层，园区用 debugger 都救不下来，现在全家每天抬头都能看见爷爷在轨道上挥手！”&lt;/p>
&lt;p>我和老勇者同时倒吸一口凉气，抬头一看——好家伙，天上真有个小黑点在轨道上循环，隐约还能听见“谁来帮我 return 一下——”的喊声。&lt;/p>
&lt;p>“撤。”老勇者把棒棒糖从嘴里拔出来，起跑的脚步比 C++ 的 move 语义还快。&lt;/p>
&lt;p>我抄起魔法书，跟着老勇者就往乐园门口冲，路过正在施工的新项目时，围挡上的海报差点让我们脚底打滑：“即将推出——并发漂流，同时体验三条河道，偶尔死锁，体验真实多线程。”&lt;/p>
&lt;p>我们的脚步又加快了三成。&lt;/p>
&lt;hr>
&lt;h4 id="-回村烧锅">🎮 回村烧锅
&lt;/h4>&lt;p>回到村口，我把今天的两份战报收进魔法书——一份 O(n×m) 暴力版，一份 O(n+m) 剪枝版，旁边画了个过山车做标记，批注：“算法乐园，项目刺激，没设 base case 者慎入。”&lt;/p>
&lt;p>刚合上书，村长就扛着锅铲冲了过来，围裙上还沾着火星：“小白！你可算回来了！锅炉火都快灭了，赶紧添柴！”&lt;/p>
&lt;p>“来了来了！”我一把抓起柴火往炉膛里塞，火苗重新蹿起来，村长的眉头这才舒展开。&lt;/p>
&lt;p>“村长，”我擦了把汗，往锅炉里又扔了根柴，“下次再发福利，换个正常点的游乐园行不？那个递归过山车，有个老头没设 base case，到现在还在天上转呢。”&lt;/p>
&lt;p>村长愣了一下，抬头往乐园方向望了望——天边那个小黑点还在轨道上循环，隐约的“谁来帮我 return 啊——”随风飘来。他沉默了两秒，诚恳地点了点头：“行，下次发隔壁‘数据结构农场’的票，最多也就被二叉树的刺扎一下，至少人还能回来。”&lt;/p>
&lt;p>
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
&lt;img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png" />
&lt;/a>
&lt;br />
本作品采用
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议
&lt;/a> 进行许可。
&lt;/p></description></item><item><title>Hello_world</title><link>https://icurious-d.github.io/posts/hello_world/</link><pubDate>Mon, 01 Jun 2026 10:52:10 +0800</pubDate><guid>https://icurious-d.github.io/posts/hello_world/</guid><description>&lt;ul>
&lt;li>&lt;strong>中文&lt;/strong>：你好（nǐ hǎo）&lt;/li>
&lt;li>&lt;strong>英语&lt;/strong>：Hello（哈喽）&lt;/li>
&lt;li>&lt;strong>法语&lt;/strong>：Bonjour（布恩入）&lt;/li>
&lt;li>&lt;strong>西班牙语&lt;/strong>：¡Buenas!（哈咯）&lt;/li>
&lt;li>&lt;strong>日语&lt;/strong>：こんにちは（konnichiwa）&lt;/li>
&lt;li>&lt;strong>俄语&lt;/strong>：Привет（привет）&lt;/li>
&lt;li>&lt;strong>阿拉伯语&lt;/strong>：مرحبا（salaam）&lt;/li>
&lt;li>&lt;strong>德语&lt;/strong>：Hallo（古藤塔克）&lt;/li>
&lt;li>&lt;strong>葡萄牙语&lt;/strong>：Olá（奥拉）&lt;/li>
&lt;li>&lt;strong>希伯来语&lt;/strong>：(שלום（שלום&lt;/li>
&lt;/ul></description></item></channel></rss>